Skip to content

Beat receiver feature flags are process-global in standalone OTel configs #15438

Description

@belimawr

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions