Skip to content

Honor per-unit status_reporting in agent health rollup#15192

Open
Oddly wants to merge 2 commits into
elastic:mainfrom
Oddly:suppress-health-degradation
Open

Honor per-unit status_reporting in agent health rollup#15192
Oddly wants to merge 2 commits into
elastic:mainfrom
Oddly:suppress-health-degradation

Conversation

@Oddly

@Oddly Oddly commented Jun 27, 2026

Copy link
Copy Markdown

The Coordinator health rollup (generateReportableState/hasState) now reads a per-unit status_reporting config from the unit's expected config. Setting report_degraded: false and/or report_failed: false excludes that unit from the agent's aggregate health, so broad policies with inputs that are not running on every enrolled host (or that fail) no longer drag the whole agent into a degraded state. The unit's own state is still reported per component; only the aggregate rollup ignores it. Works for both process (Beat) and OpenTelemetry-translated components since both flow through the Coordinator.

Ports the approach from elastic/beats#49511 into elastic-agent per maintainer guidance so it also covers non-Beats components and the OTel collector runtime.

What does this PR do?

Adds an optional per-unit status_reporting config that lets an input opt out of contributing its state to the agent's aggregate health:

status_reporting:
  report_degraded: false   # a Degraded unit does not degrade the agent
  report_failed: false     # a Failed unit does not degrade the agent

hasState (used by generateReportableState) skips units that opt out of the queried state. The two flags are independent and state-specific: report_failed only affects a Failed unit, report_degraded only a Degraded one. A component-level state with no contributing unit still counts. An absent key or true keeps the current behavior.

Scope: covers s.Components (process/Beat and OpenTelemetry-translated components, which carry unit config). The raw OTel collector aggregate (s.Collector, EDOT without the agent supervisor) and the Kubernetes liveness probe (which has its own failon opt-out) are intentionally left unchanged.

Where to set it

status_reporting is read from the top level of a unit's expected config, which maps to the input level of a policy. The rollup operates per unit, so it is set once per input rather than per stream (unlike the stream-level flag in elastic/beats#49511).

Standalone elastic-agent.yml:

inputs:
  - type: filestream
    id: my-input
    status_reporting:
      report_degraded: false
    streams:
      - ...

Fleet-managed: in the integration policy editor, expand the input's Advanced options and add the block to its custom/advanced YAML field. Keys set there are merged into the input's generated config (the input unit config the rollup reads):

status_reporting:
  report_degraded: false

Why is it important?

Agent health is a worst-state-wins rollup: a single failing or not-applicable unit degrades the whole agent. On broad/shared policies where not every input runs on every host, this produces persistently degraded agents with no supported way to opt a unit out. This adds that opt-out per unit while still reporting the unit's real state per component.

Checklist

  • I have read and understood the pull request guidelines of this project.
  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding change to the default configuration files
  • I have added tests that prove my fix is effective or that my feature works
  • I have added an entry in ./changelog/fragments using the changelog tool
  • I have added an integration test or an E2E test

Disruptive User Impact

None. The config is opt-in; an absent key or true keeps the current behavior.

How to test this PR locally

Unit tests:

go test ./internal/pkg/agent/application/coordinator/ -run SuppressHealthDegradation

Integration test (Local, builds and runs the agent with the fake component): TestFakeComponentStatusReportingSuppressesHealth in testing/integration/ess, run through the integration harness (mage integration:...). It asserts the agent stays Healthy while the units report Degraded/Failed.

Standalone: run an agent with the fake input in a Degraded/Failed state, with and without status_reporting.report_degraded/report_failed: false, and check elastic-agent status. A suppressed unit keeps the agent Healthy while still showing its own state; the wrong flag (e.g. report_degraded on a Failed unit) does not suppress.

Related issues

@Oddly Oddly requested a review from a team as a code owner June 27, 2026 21:22
@Oddly Oddly requested review from blakerouse and lorienhu June 27, 2026 21:22
@cla-checker-service

cla-checker-service Bot commented Jun 27, 2026

Copy link
Copy Markdown

💚 CLA has been signed

@mergify mergify Bot assigned Oddly Jun 27, 2026
@mergify

mergify Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label. Could you fix it @Oddly? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-./d./d is the label that automatically backports to the 8./d branch. /d is the digit
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-8 is the label that automatically backports to all active minor branches for the 8 major.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

The Coordinator health rollup (generateReportableState/hasState) now reads a
per-unit status_reporting config from the unit's expected config. Setting
report_degraded: false and/or report_failed: false excludes that unit from the
agent's aggregate health, so broad policies with inputs that are not running on
every enrolled host (or that fail) no longer drag the whole agent into a
degraded state. The unit's own state is still reported per component; only the
aggregate rollup ignores it. Works for both process (Beat) and
OpenTelemetry-translated components since both flow through the Coordinator.

Ports the approach from elastic/beats#49511 into elastic-agent per maintainer
guidance so it also covers non-Beats components and the OTel collector runtime.
@Oddly Oddly force-pushed the suppress-health-degradation branch from b47f34c to 24edd75 Compare June 27, 2026 22:24
@ebeahan

ebeahan commented Jun 29, 2026

Copy link
Copy Markdown
Member

/test

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

TL;DR

linux:amd64:tier1:sudo:fleet failed while TestNetworkTraffic was installing/enrolling the Agent: the harness waited for Fleet state HEALTHY, but elastic-agent status --output json kept reporting Fleet state STOPPED. The prefetched log tail does not include component diagnostics or the other two reported test failures, so this is not attributable to the PR diff from the available evidence.

Remediation

  • Retry the failed Buildkite step; the visible failure is a Fleet connection/status setup timeout in an existing ESS integration test, not a compile or deterministic assertion failure in the changed status-reporting code.
  • If it reproduces, inspect the uploaded build/fleet_sudo_Linux_6.17.0-1016-gcp_x86_64.integration.out.json and build/diagnostics/TestNetworkTraffic-2026-06-30T12-00-11Z-diagnostics.zip artifacts for the Agent/Fleet error that kept FleetState at STOPPED.
Investigation details

Root Cause

The failure occurs in testing/integration/ess/network_traffic_monitoring_test.go:96, where SetupSuite calls tools.InstallAgentWithPolicy(...). That helper polls agentFixture.ExecStatus(ctx) until FleetState == HEALTHY in pkg/testing/tools/tools.go:195-207. In this build, the poll did not reach HEALTHY before the setup context expired.

The PR changes internal/pkg/agent/application/coordinator/coordinator_state.go aggregate component/unit health handling and adds a local fake-component integration test, but the visible failing test is the pre-existing Fleet network traffic test and the log does not show the new status_reporting test failing.

Evidence

Successfully enrolled the Elastic Agent.
Elastic Agent has been successfully installed.
fixture.go:978: >> running binary with: [/opt/Elastic/Agent/elastic-agent status --output json]
tools.go:205: wanted fleet status to be HEALTHY, was STOPPED
fixture_install.go:325: [test TestNetworkTraffic] Inside fixture cleanup function
DONE 179 tests, 127 skipped, 3 failures in 5091.985s

The later oblt-cli teardown authentication error happened after Integration tests failed, so it is cleanup fallout rather than the primary test failure.

Verification

  • Read the prefetched Buildkite event/failure files and the job log under /tmp/gh-aw/buildkite-logs/.
  • Checked PR comments for an existing duplicate detective finding; none matched this failure.
  • Searched flaky-test issues for TestNetworkTraffic. Existing visible flaky issues cover the compare schema assertion, not this setup-time FleetState STOPPED failure.

Follow-up

The summary says 3 failures, but the prefetched log tail only exposes the TestNetworkTraffic setup failure. The uploaded integration JSON/XML artifacts are needed to identify the other two failures and the underlying Agent/Fleet reason for STOPPED if the retry also fails.


What is this? | From workflow: PR Buildkite Detective

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

// opt-out end to end: a unit with the matching report_* flag does not degrade
// the agent's aggregate health (while still reporting its own state), and the
// non-matching flag does not suppress.
func TestFakeComponentStatusReportingSuppressesHealth(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need a test where only the status_reporting value changes and nothing else. Not even the message.

We need a test that shows that the health status will change if only the status_reporting value changes and nothing else. Testing locally it looks like this will fail.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an LLM throw together a quick reproduction based on your existing test which I'll include below. This causes the test to time out when it fails which is not ideal.

+// configOnlyChangeHealthConfig is byte-for-byte identical to wrongFlagHealthConfig
+// except the status_reporting flags now match each unit's state, so both units
+// become suppressed. The unit "state"/"message" fields are deliberately left
+// unchanged from wrongFlagHealthConfig: the fake component will therefore not
+// report any new state/message for either unit, so this transition exercises a
+// config-only change with no accompanying unit state transition.
+var configOnlyChangeHealthConfig = `
+outputs:
+  default:
+    type: fake-output
+inputs:
+  - id: degraded
+    type: fake
+    state: 3
+    message: Degraded, wrong flag
+    status_reporting:
+      report_degraded: false
+  - id: failed
+    type: fake
+    state: 4
+    message: Failed, wrong flag
+    status_reporting:
+      report_failed: false
+`
+
 // TestFakeComponentStatusReportingSuppressesHealth verifies the status_reporting
 // opt-out end to end: a unit with the matching report_* flag does not degrade
 // the agent's aggregate health (while still reporting its own state), and the
 // non-matching flag does not suppress.
+//
+// It also verifies that flipping status_reporting alone -- with no accompanying
+// change to a unit's own reported state or message -- is picked up by the
+// agent's aggregate health without requiring an unrelated state transition to
+// "jog" the rollup into refreshing.
 func TestFakeComponentStatusReportingSuppressesHealth(t *testing.T) {
        define.Require(t, define.Requirements{
                Group: integration.Default,
@@ -125,6 +155,30 @@ func TestFakeComponentStatusReportingSuppressesHealth(t *testing.T) {
                                },
                        },
                },
+               // Config-only change: status_reporting now matches each unit's state
+               // (both should become suppressed), but the unit "state"/"message" values
+               // are unchanged from the previous step, so the fake component has no new
+               // state to report. The agent must still pick up the config change and
+               // become Healthy -- if the aggregate rollup only refreshes on a unit's own
+               // state/message changing, this step will never be reached and the test
+               // will time out waiting for it.
+               atesting.State{
+                       Configure:  configOnlyChangeHealthConfig,
+                       AgentState: atesting.NewClientState(client.Healthy),
+                       Components: map[string]atesting.ComponentState{
+                               "fake-default": {
+                                       State: atesting.NewClientState(client.Healthy),
+                                       Units: map[atesting.ComponentUnitKey]atesting.ComponentUnitState{
+                                               atesting.ComponentUnitKey{UnitType: client.UnitTypeInput, UnitID: "fake-default-degraded"}: {
+                                                       State: atesting.NewClientState(client.Degraded),
+                                               },
+                                               atesting.ComponentUnitKey{UnitType: client.UnitTypeInput, UnitID: "fake-default-failed"}: {
+                                                       State: atesting.NewClientState(client.Failed),
+                                               },
+                                       },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants