Skip to content

Add suppress_health_degradation stream config to prevent optional streams from degrading agent health#49511

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

Add suppress_health_degradation stream config to prevent optional streams from degrading agent health#49511
Oddly wants to merge 3 commits into
elastic:mainfrom
Oddly:suppress-health-degradation

Conversation

@Oddly

@Oddly Oddly commented Mar 17, 2026

Copy link
Copy Markdown

Proposed commit message

Adds a suppress_health_degradation config flag at the stream level in the
management framework. When set, fetch failures from all inputs are still logged and tracked but do not mark the agent as Degraded. This lets operators build broad Fleet
policies with integrations that may not be running on every enrolled host,
without those absent services dragging the entire agent into a degraded state.

Unlike a per-input approach (which we tried first), this change lives in the health aggregation layer
(calcState() in unit.go) so every input type gets the flag for free: CEL,
httpjson, metricbeat modules, filebeat inputs, and any future inputs that
report per-stream health.

The flag is read from the stream's existing Source protobuf field.

Behaviour when suppress_health_degradation: true:

  • Input still retries every period
  • Errors are logged at ERROR level
  • Per-stream status still reports Degraded/Failed in the streams payload
  • The stream is excluded from the unit's aggregate health calculation
  • On recovery, stream status resets to Running as usual

Usage in Fleet integrations:

# data_stream manifest.yml
- name: suppress_health_degradation
  type: bool
  title: Suppress Health Degradation
  description: >-
    When enabled, failures collecting this data stream will not mark the
    agent as degraded. Use for data streams expected to fail on some hosts.
  required: false
  show_user: false
  default: false

# stream.yml.hbs
{{#if suppress_health_degradation}}
suppress_health_degradation: {{suppress_health_degradation}}
{{/if}}

Users can also inject suppress_health_degradation: true via Fleet's Advanced
Settings YAML on any existing integration without package changes.

Tested on a live Elastic Agent 9.3.1 cluster across three input types (CEL,
redis/metrics, nginx/metrics) with 9 permutations:

Streams Endpoint suppress State
1 stream working not set HEALTHY
1 stream dead not set DEGRADED
1 stream dead true HEALTHY
1 stream dead false DEGRADED
2 streams dead mixed (true + not set) DEGRADED
2 streams dead both true HEALTHY
2 streams dead both not set DEGRADED
2 streams working both true HEALTHY
3 streams dead mixed (not set+true+false) DEGRADED

Replaces #49492

Fixes: elastic/elastic-agent#12885

@Oddly Oddly requested a review from a team as a code owner March 17, 2026 07:48
@Oddly Oddly requested review from AndersonQ and VihasMakwana March 17, 2026 07:48
@botelastic botelastic Bot added the needs_team Indicates that the issue/PR needs a Team:* label label Mar 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 GitHub comments

Just comment with:

  • run docs-build : Re-trigger the docs validation. (use unformatted text in the comment!)

@mergify

mergify Bot commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label.
If this is a bug or security fix, could you label this PR @Oddly? 🙏.
For such, you'll need to label your PR with:

  • The upcoming major version of the Elastic Stack
  • The upcoming minor version of the Elastic Stack (if you're not pushing a breaking change)

To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-8./d is the label to automatically backport 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.

@mergify mergify Bot assigned Oddly Mar 17, 2026
@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a per-stream suppressHealthDegradation boolean to unitState and initializes it from each stream's suppress_health_degradation source config in getStreamStates. updateStateForStream and unit.update preserve and update this flag when stream entries change. calcState excludes streams with suppressHealthDegradation=true from the unit-level aggregated health and message while still reporting their individual stream status. Tests cover parsing, suppression flips, recovery, and payload publication on flag changes.

Suggested labels

Team:Elastic-Agent-Data-Plane

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed PR implements suppress_health_degradation flag addressing issue #12885's primary objective: allowing optional streams to fail without degrading agent health while preserving logging and per-stream status.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing suppress_health_degradation: unit.go health aggregation logic, comprehensive unit tests, and changelog fragment.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ Update Documentation: Commit on current branch
  • 🛠️ Update Documentation: Create PR

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@x-pack/libbeat/management/unit.go`:
- Around line 369-375: The update() path currently only mutates
u.streamStates[*].suppressHealthDegradation and never recomputes the unit
aggregate health, leaving stale Failed/Degraded unit state; after you change
existing.suppressHealthDegradation in update(), force a recompute and publish of
the unit health so suppression flips take effect immediately—either invoke
updateStateForStream(...) for that stream in a way that bypasses the "ignore
same-state" check (add a force parameter or call a new helper), or implement a
small helper that scans u.streamStates to recalculate the aggregate health and
calls the existing publish/update routine to emit the new unit state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: abb20d59-2b6e-4e8a-a10e-63d15e50284c

📥 Commits

Reviewing files that changed from the base of the PR and between 79621d9 and 6d1ee4d.

📒 Files selected for processing (2)
  • x-pack/libbeat/management/unit.go
  • x-pack/libbeat/management/unit_test.go

Comment thread x-pack/libbeat/management/unit.go
@Oddly Oddly force-pushed the suppress-health-degradation branch from dac7eef to f2ff4f3 Compare March 17, 2026 08:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
x-pack/libbeat/management/unit.go (1)

369-375: ⚠️ Potential issue | 🔴 Critical

Recompute and publish unit health when suppression flips.

update() mutates existing.suppressHealthDegradation but never recalculates/emits aggregate unit status. If a stream is already Degraded/Failed, unit health can stay stale indefinitely because same-state stream updates are ignored later (Line 323).

Proposed fix (minimal)
 func (u *agentUnit) update(cu *client.Unit) {
 	u.mtx.Lock()
 	defer u.mtx.Unlock()

 	u.softDeleted = false
 	u.clientUnit = cu
+	suppressionChanged := false

 	inputStatus := getStatus(cu.Expected().State)
 	if u.inputLevelState.state != inputStatus {
 		u.inputLevelState = unitState{
 			state: inputStatus,
 		}
 	}

 	newStreamStates, newStreamIDs := getStreamStates(cu.Expected())

 	for key, state := range newStreamStates {
 		if existing, exists := u.streamStates[key]; exists {
-			// Preserve current health state but update the suppressHealthDegradation flag
-			// in case the stream config changed.
-			existing.suppressHealthDegradation = state.suppressHealthDegradation
+			if existing.suppressHealthDegradation != state.suppressHealthDegradation {
+				suppressionChanged = true
+			}
+			existing.suppressHealthDegradation = state.suppressHealthDegradation
 			u.streamStates[key] = existing
 			continue
 		}

 		u.streamStates[key] = state
 	}
@@
 	switch {
@@
 	}
+
+	if suppressionChanged {
+		state, msg := u.calcState()
+		streamsPayload := make(map[string]interface{}, len(u.streamStates))
+		for id, streamState := range u.streamStates {
+			streamsPayload[id] = map[string]interface{}{
+				"status": getUnitState(streamState.state).String(),
+				"error":  streamState.msg,
+			}
+		}
+		if err := u.clientUnit.UpdateState(getUnitState(state), msg, map[string]interface{}{"streams": streamsPayload}); err != nil {
+			u.logger.Warnf("failed to update state for input %s: %v", u.ID(), err)
+		}
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@x-pack/libbeat/management/unit.go` around lines 369 - 375, In update(), when
you mutate existing.suppressHealthDegradation for entries in u.streamStates,
detect that the suppression flag flipped and then trigger the unit-level health
recomputation/publish path so the aggregate unit health is recalculated and
emitted; specifically, inside the loop that updates
existing.suppressHealthDegradation (in update()), after assigning
existing.suppressHealthDegradation = state.suppressHealthDegradation, call the
same routine you use for stream-state transitions to recompute and publish
aggregate unit health (i.e., the unit health recompute/publish codepath) so unit
health doesn't remain stale when suppression toggles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@x-pack/libbeat/management/unit.go`:
- Around line 369-375: In update(), when you mutate
existing.suppressHealthDegradation for entries in u.streamStates, detect that
the suppression flag flipped and then trigger the unit-level health
recomputation/publish path so the aggregate unit health is recalculated and
emitted; specifically, inside the loop that updates
existing.suppressHealthDegradation (in update()), after assigning
existing.suppressHealthDegradation = state.suppressHealthDegradation, call the
same routine you use for stream-state transitions to recompute and publish
aggregate unit health (i.e., the unit health recompute/publish codepath) so unit
health doesn't remain stale when suppression toggles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da31b39f-bea6-4a10-b13e-ec60181529fd

📥 Commits

Reviewing files that changed from the base of the PR and between dac7eef and f2ff4f3.

📒 Files selected for processing (2)
  • x-pack/libbeat/management/unit.go
  • x-pack/libbeat/management/unit_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • x-pack/libbeat/management/unit_test.go

…eams from degrading agent health

Adds a `suppress_health_degradation` config flag at the stream level in the
management framework. When set, fetch failures are still logged and tracked
but do not mark the agent as Degraded. This lets operators build broad Fleet
policies with integrations that may not be running on every enrolled host,
without those absent services dragging the entire agent into a degraded state.

Unlike a per-input approach, this change lives in the health aggregation layer
(`calcState()` in `unit.go`) so every input type gets the flag for free: CEL,
httpjson, metricbeat modules, filebeat inputs, and any future inputs that
report per-stream health.

The flag is read from the stream's existing `Source` protobuf field — no proto
changes or per-input modifications needed.

Behaviour when `suppress_health_degradation: true`:

- Input still retries every `period`
- Errors are logged at ERROR level
- Per-stream status still reports Degraded/Failed in the streams payload
- The stream is excluded from the unit's aggregate health calculation
- On recovery, stream status resets to Running as usual

Tested on a live Elastic Agent 9.3.1 cluster across three input types (CEL,
redis/metrics, nginx/metrics) with 9 permutations — all passed.

Supersedes elastic#49492

Fixes: elastic/elastic-agent#12885
@Oddly Oddly force-pushed the suppress-health-degradation branch from f2ff4f3 to 74e2856 Compare March 17, 2026 08:32
@Oddly

Oddly commented Mar 17, 2026

Copy link
Copy Markdown
Author

Live cluster validation

Tested on a patched Elastic Agent 9.3.1 built from this branch via mage otel:crossBuild.

Fresh install (policy created from scratch each time):

Endpoint suppress Result
working not set HEALTHY
dead not set DEGRADED
dead true HEALTHY
dead false DEGRADED
both dead (2 streams) mixed (true + not set) DEGRADED
both dead (2 streams) both true HEALTHY
both dead (2 streams) both not set DEGRADED
both working (2 streams) both true HEALTHY
all dead (3 streams) mixed (not set+true+false) DEGRADED

In-place update (same policy updated via PUT without agent restart, exercises the update() recompute path):

Endpoint suppress Result
dead not set DEGRADED
dead → true HEALTHY
dead → false DEGRADED
dead → true HEALTHY
→ working true HEALTHY
→ dead true HEALTHY
dead → not set DEGRADED
→ working not set HEALTHY

Cross-input types (three integrations on one agent policy):

Input type Endpoint suppress Result
CEL (Zabbix) working no HEALTHY
redis/metrics dead true suppressed
nginx/metrics dead no DEGRADED

@pierrehilbert pierrehilbert added the Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team label Mar 17, 2026
@elasticmachine

Copy link
Copy Markdown
Contributor

Pinging @elastic/elastic-agent-data-plane (Team:Elastic-Agent-Data-Plane)

@botelastic botelastic Bot removed the needs_team Indicates that the issue/PR needs a Team:* label label Mar 17, 2026
@pierrehilbert pierrehilbert requested a review from faec March 17, 2026 10:30
@AndersonQ

Copy link
Copy Markdown
Member

/test

@AndersonQ

Copy link
Copy Markdown
Member

You need to update/format the files. make update should do the trick. You can check if all is good by running make check

@AndersonQ

Copy link
Copy Markdown
Member

/test

Comment thread x-pack/libbeat/management/unit.go Outdated
// but their degraded/failed states do not drag the overall unit health down.
suppressHealth := false
if src := stream.GetSource(); src != nil {
if v, ok := src.GetFields()["suppress_health_degradation"]; ok {

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.

We try to avoid using individual booleans like this, to make it easier to add more configuration later.

The pattern we are introducing for outputs puts configuration under a status_reporting object, which came from elastic/elastic-agent#10890. This would allow us to keep adding functionality, like the ability to mute specific types of errors in the future. We hopefully will introduce something like the what the OTel hostmetrics receiver has done for process metrics to our system/metrics receiver for example.

It would be great to follow a similar convention, with a status_reporting configuration object here. I also think we should increase the granularity and allow muting the degraded and failed states separately.

status_reporting:
    report_degraded: false
    report_failed: false

It is also possible to have inputs without streams, where this configuration would apply at the input level. This could be an a follow up enhancement we do if this gets too complicated to handle in this PR. You would want the input level setting to be inherited into each stream when there are streams present, so that you can control the whole input easily without duplication in the case where there are many streams.

…gregation

Rework the stream health suppression from a single flat boolean into a
status_reporting config block with report_degraded and report_failed
as separate fields. This makes it possible to choose independently
whether degraded or failed streams affect the unit's aggregate health.

Setting status_reporting on the input applies to all its streams by
default. Individual streams can still override if they need different
behaviour.
@Oddly Oddly force-pushed the suppress-health-degradation branch from 831a240 to 95b667f Compare March 27, 2026 21:39
@Oddly

Oddly commented Mar 27, 2026

Copy link
Copy Markdown
Author

I think the change is as you requested; The possibility to set this per input and/or stream(stream takes priority) for failure or degradation separately.
Let me know if anything needs changing or further testing.

However, when testing version 9.3.2 I found out that agentbeat was replaced by elastic-otel-collector in version 9.3.0, invalidating this approach beyond 9.3.0.

Tested with an agent on a 8.19.10 cluster, in the following scenarios:

Tests
Details

Stream-level config:

  • No status_reporting set — agent DEGRADED from failing streams
  • All failing streams muted — agent HEALTHY, per-stream payloads still show DEGRADED
  • Only report_degraded: false set — unit HEALTHY since streams are degraded
  • not failed, confirming fields work independently
  • Some streams muted, others not — agent DEGRADED from the unmuted ones only

Live policy changes:

  • Removed muting without agent restart — agent went DEGRADED within 15s
  • Re-added muting — agent went HEALTHY again

Input-level inheritance:

  • Config on input only, no stream-level config — all streams inherited muting, unit HEALTHY
  • Input muted, one stream overrides back to reporting — unit DEGRADED from the overriding stream
  • Input unmuted, one stream overrides to muted — unit DEGRADED from the unmuted stream

However, in my testing of version 9.3.2 I found out that agentbeat was replaced by elastic-otel-collector in version 9.3.0, invalidating this approach beyond 9.3.0.

@cmacknz

cmacknz commented Mar 30, 2026

Copy link
Copy Markdown
Member

However, when testing version 9.3.2 I found out that agentbeat was replaced by elastic-otel-collector in version 9.3.0, invalidating this approach beyond 9.3.0.

So yes we are in the middle of an architecture change, with two ways to do things here as we transition everything to running into the OpenTelemetry collector.

The receiver variant used in 9.3+ is here:

// RunnerReporter defines an interface that returns a StatusReporter for a specific runner.
// This is used for grouping and managing statuses of multiple runners
type RunnerReporter interface {
GetReporterForRunner(id string) status.StatusReporter
// UpdateStatus updates the group status of a runnerReporter
UpdateStatus(status status.Status, msg string)
}

The main problem is how to provide the input level configuration to it, as it doesn't have access to it the way the previous implementation did. It can be passed the beat configuration when it is created though. The BeatReceiver struct has access to the instance.Beat that has the configuration in it:

func (br *BeatReceiver) Start(host component.Host) error {
var groupReporter otelstatus.RunnerReporter
if w, ok := br.beater.(cfgfile.WithOtelFactoryWrapper); ok {
groupReporter = otelstatus.NewGroupStatusReporter(host)
w.WithOtelFactoryWrapper(otelstatus.StatusReporterFactory(groupReporter))
}

@cmacknz

cmacknz commented Mar 31, 2026

Copy link
Copy Markdown
Member

It is possible to implement this instead in Elastic Agent, since it receives the status events for both the Beat sub-processes and the OpenTelemetry collector variants of this.

This would allow this feature to work even for things that are not Beats, like Elastic Defend.

The main disadvantage would be that it wouldn't work for a user using the EDOT collector without the Elastic Agent supervisor, but I think that is likely acceptable for now.

@Oddly

Oddly commented Apr 1, 2026

Copy link
Copy Markdown
Author

A small correction, this already works in the 9.3+ OTEL just like we implemented in agentbeat. Earlier I tested the patched version, but that didn't work because it had a OTEL backend and actually gave a skewed result.
I now tested with the default 9.3 OTEL agent and it actually just works with the implementation you suggested:

status_reporting:
    report_degraded: false
    report_failed: false

So, in conclusion, I think we can just merge this PR for anything lower than 9.3 and have the same functionality in both backends.

@cmacknz

cmacknz commented Apr 10, 2026

Copy link
Copy Markdown
Member

I haven't had a chance to double check that this works as described but I haven't forgotten about it.

@Oddly

Oddly commented Jun 2, 2026

Copy link
Copy Markdown
Author

Hi @cmacknz, could you take a look at this change?

@cmacknz

cmacknz commented Jun 16, 2026

Copy link
Copy Markdown
Member

In the upcoming 9.5.0 release we'll be executing most beats in the OpenTelemetry collector, so this change needs to be made in elastic-agent instead of beats so it'll still work and it'll work for things that are not Beats as mentioned in #49511 (comment).

I still like this feature but we haven't had a chance to look at implementation at all on our end.

Oddly pushed a commit to Oddly/elastic-agent that referenced this pull request Jun 27, 2026
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 pushed a commit to Oddly/elastic-agent that referenced this pull request Jun 27, 2026
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 pushed a commit to Oddly/elastic-agent that referenced this pull request Jun 27, 2026
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 added a commit to Oddly/elastic-agent that referenced this pull request Jun 27, 2026
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 commented Jun 27, 2026

Copy link
Copy Markdown
Author

@cmacknz Continuing work in elastic/elastic-agent#15192.

Oddly added a commit to Oddly/elastic-agent that referenced this pull request Jun 27, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow inputs to fail without degrading agent health

7 participants