Scope
This issue affects only standalone / hand-crafted OTel receiver configurations where multiple Beat receivers are defined in the same Elastic Agent process and those receiver definitions set conflicting features.* values.
Fleet-managed policies and Agent-generated receiver configs should normally provide one consistent agent.features.* set for the process. The problem described here is about raw standalone OTel configs such as manually authored filebeatreceiver definitions.
Problem
Beat receiver feature flags are parsed from each receiver's raw config, but the parsed values are stored in package-global libbeat/features state. If two Beat receiver definitions in the same Elastic Agent process configure different values for the same feature flag, each receiver overwrites the same global flag.
This creates a "data race" at the configuration semantics level: the final behavior depends on receiver construction, startup, or reload ordering. It is not necessarily a Go race-detector issue because the current values are stored with atomics. The bug is that receiver-local configuration mutates process-global behavior.
Example standalone OTel config:
receivers:
filebeatreceiver/log_as_log:
filebeat:
inputs:
- type: log
id: logs-a
paths: [/var/log/a.log]
features:
log_input_run_as_filestream:
enabled: false
filebeatreceiver/log_as_filestream:
filebeat:
inputs:
- type: log
id: logs-b
paths: [/var/log/b.log]
features:
log_input_run_as_filestream:
enabled: true
In this configuration, each receiver appears to define an independent feature flag value. In practice, both receivers share the same process-global features.log_input_run_as_filestream.enabled value.
Current behavior
Each Beat receiver creates a Beat instance through NewBeatForReceiver. During initialization, the receiver's raw config is passed to features.UpdateFromConfig:
https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/x-pack/libbeat/cmd/instance/beat.go#L152-L173
b.RawConfig = cfg
// ...
if err := features.UpdateFromConfig(b.RawConfig); err != nil {
return nil, fmt.Errorf("could not parse features: %w", err)
}
b.RegisterHostname(features.FQDN())
features.UpdateFromConfig parses features.fqdn, features.log_input_run_as_filestream, and features.aws_s3_v2, then stores their values in package-global state:
https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/libbeat/features/features.go#L34-L67
type fflags struct {
callbackMut sync.RWMutex
fqdnEnabled atomic.Bool
fqdnCallbacks map[string]boolValueOnChangeCallback
logRunAsFilestream atomic.Bool
awsS3V2 atomic.Bool
}
// ...
flags.SetFQDNEnabled(parsedFlags.Features.FQDN.Enabled())
flags.SetLogInputRunFilestream(parsedFlags.Features.LogRunAsFilestream.Enabled())
flags.SetAwsS3V2(parsedFlags.Features.AwsS3V2.Enabled())
Those global values are later read by Beat code paths such as the Filebeat log input redirect:
https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/filebeat/input/logv2/input.go#L58-L63
// Global feature flag that forces all Log input instances
// to run as Filestream, even when not running under
// Elastic Agent
if features.LogInputRunFilestream() {
return true, nil
}
and the aws-s3 input implementation switch:
https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/x-pack/filebeat/input/awss3/input.go#L43-L51
func (im *s3InputManager) Create(cfg *conf.C) (v2.Input, error) {
config := defaultConfig()
if err := cfg.Unpack(&config); err != nil {
return nil, err
}
if features.AwsS3V2() {
return newInputV2(config, im.store, im.path, im.logger)
}
Why this is unsafe
The receiver config gives the impression that features.* is scoped to the receiver instance. The implementation scopes it to the process.
As a result:
- The receiver initialized last can overwrite feature values used by previously initialized receivers.
- Reloads can change behavior for unrelated receivers because every reload can re-apply a receiver's local config into shared global state.
Expected behavior
Receiver-local configuration should not mutate behavior for other receiver instances in the same Elastic Agent process.
Either:
- Beat receiver feature flags should be represented as receiver-local / Beat-instance-local state and passed to the code paths that need them, or
- standalone OTel validation should reject multiple Beat receiver definitions with conflicting process-global feature flag values, making the unsupported configuration explicit.
Acceptance criteria
- Document or fix the current process-global behavior for Beat receiver
features.*.
- Add test coverage with two
filebeatreceiver definitions in one collector process using conflicting features.log_input_run_as_filestream.enabled values.
- Ensure one receiver's feature flag config cannot silently change another receiver's input behavior.
- If receiver-local feature flags are not feasible short term, reject conflicting feature flag values during standalone OTel config validation with a clear error.
Related issues
Scope
This issue affects only standalone / hand-crafted OTel receiver configurations where multiple Beat receivers are defined in the same Elastic Agent process and those receiver definitions set conflicting
features.*values.Fleet-managed policies and Agent-generated receiver configs should normally provide one consistent
agent.features.*set for the process. The problem described here is about raw standalone OTel configs such as manually authoredfilebeatreceiverdefinitions.Problem
Beat receiver feature flags are parsed from each receiver's raw config, but the parsed values are stored in package-global
libbeat/featuresstate. If two Beat receiver definitions in the same Elastic Agent process configure different values for the same feature flag, each receiver overwrites the same global flag.This creates a "data race" at the configuration semantics level: the final behavior depends on receiver construction, startup, or reload ordering. It is not necessarily a Go race-detector issue because the current values are stored with atomics. The bug is that receiver-local configuration mutates process-global behavior.
Example standalone OTel config:
In this configuration, each receiver appears to define an independent feature flag value. In practice, both receivers share the same process-global
features.log_input_run_as_filestream.enabledvalue.Current behavior
Each Beat receiver creates a Beat instance through
NewBeatForReceiver. During initialization, the receiver's raw config is passed tofeatures.UpdateFromConfig:https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/x-pack/libbeat/cmd/instance/beat.go#L152-L173
features.UpdateFromConfigparsesfeatures.fqdn,features.log_input_run_as_filestream, andfeatures.aws_s3_v2, then stores their values in package-global state:https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/libbeat/features/features.go#L34-L67
Those global values are later read by Beat code paths such as the Filebeat log input redirect:
https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/filebeat/input/logv2/input.go#L58-L63
and the
aws-s3input implementation switch:https://github.com/elastic/beats/blob/af686c255e1f319b47524835f63b06d31365d47b/x-pack/filebeat/input/awss3/input.go#L43-L51
Why this is unsafe
The receiver config gives the impression that
features.*is scoped to the receiver instance. The implementation scopes it to the process.As a result:
Expected behavior
Receiver-local configuration should not mutate behavior for other receiver instances in the same Elastic Agent process.
Either:
Acceptance criteria
features.*.filebeatreceiverdefinitions in one collector process using conflictingfeatures.log_input_run_as_filestream.enabledvalues.Related issues
agent.features.*flags to Beat receivers #15437