Skip to content

fix(hw-health): don't let BMC Ok suppress critical/fatal sensor alerts (#3402)#3440

Open
hatamzad-nv wants to merge 1 commit into
NVIDIA:mainfrom
hatamzad-nv:fix/3402-bmc-ok-suppresses-alerts
Open

fix(hw-health): don't let BMC Ok suppress critical/fatal sensor alerts (#3402)#3440
hatamzad-nv wants to merge 1 commit into
NVIDIA:mainfrom
hatamzad-nv:fix/3402-bmc-ok-suppresses-alerts

Conversation

@hatamzad-nv

@hatamzad-nv hatamzad-nv commented Jul 13, 2026

Copy link
Copy Markdown

Completes #3402 by fixing the BMC-Ok suppression. The other half of the issue
(lower_fatal bound range_min instead of health.lower_fatal) was already fixed
on main in #3401 (76357e2c5). Closes #3402.

In to_health_result, any non-Ok classification was downgraded to Success whenever
bmc_health == Ok. This meant a faulty or compromised BMC that self-reports Ok silenced
independent threshold alerting entirely — including Critical, Fatal, and SensorFailure.

This change gates the BMC-Ok suppression to the lowest severity (Warning) only.
Critical, Fatal, and SensorFailure now always alert regardless of BMC self-report,
while the noise-mitigation for mis-tuned caution thresholds is preserved for Warning.

Changes

  • crates/health/src/processor/health_report.rs: suppress on BMC Ok only when the
    calculated state is Warning.

Tests

Added unit tests covering the new policy — alert cases assert the exact emitted
Classification, not just that an alert fired (all pass):

  • bmc_ok_does_not_suppress_fatal_alert — Fatal + BMC Ok → Alert (SensorFatal)
  • bmc_ok_does_not_suppress_critical_alert — Critical + BMC Ok → Alert (SensorCritical)
  • bmc_ok_does_not_suppress_sensor_failure_alert — SensorFailure + BMC Ok → Alert (SensorFailure)
  • bmc_ok_suppresses_warning_alert — Warning + BMC Ok → Success (intended)
  • bmc_not_ok_still_alerts_on_warning — Warning + BMC not Ok → Alert (SensorWarning)

cargo test -p carbide-health and cargo clippy -p carbide-health --all-targets pass locally.

@hatamzad-nv hatamzad-nv requested a review from a team as a code owner July 13, 2026 18:29
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b7778382-ca2c-4a98-be68-f7a3ae304840

📥 Commits

Reviewing files that changed from the base of the PR and between ec2ae7e and 8b42bad.

📒 Files selected for processing (1)
  • crates/health/src/processor/health_report.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/health/src/processor/health_report.rs

Summary by CodeRabbit

  • Bug Fixes
    • Health alerts now remain active for fatal, critical, and sensor failure conditions, even when the BMC reports an “OK” status.
    • Warning-level alerts are suppressed only when the BMC confirms the sensor is healthy.
    • Improved warning messaging for health status evaluation.

Walkthrough

The health report processor now suppresses only warning-level threshold alerts when the BMC reports Ok. Fatal, critical, and sensor-failure threshold breaches remain alertable, with tests covering these combinations and non-Ok warning behavior.

Changes

Health classification

Layer / File(s) Summary
BMC threshold severity handling
crates/health/src/processor/health_report.rs
BmcHealth::Ok suppresses only SensorHealth::Warning; fatal, critical, and sensor-failure states remain unsuppressed. Warning logging was updated, and helper functions plus unit tests cover the supported severity combinations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: BMC Ok no longer suppresses critical or fatal sensor alerts.
Description check ✅ Passed The description matches the implemented fix and the added tests for BMC Ok suppression behavior.
Linked Issues check ✅ Passed The PR addresses the BMC Ok suppression bug from #3402, and the separate lower_fatal fix is explicitly out of scope here.
Out of Scope Changes check ✅ Passed The changes stay focused on health_report logic and tests, with no unrelated functionality introduced.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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.

🧹 Nitpick comments (2)
crates/health/src/processor/health_report.rs (2)

143-175: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Suppression scoping is correct.

The narrowed condition correctly restricts silencing to SensorHealth::Warning only when BmcHealth::Ok, leaving Critical/Fatal/SensorFailure unaffected — matches the stated intent, and the inline rationale comment is a nice touch for future maintainers.

One optional thought: this suppression is a decision worth trending over time (e.g., to detect systematically mis-tuned caution thresholds). Per the repo's instrumentation guideline, an occurrence "you'd trend" is the signal to declare a carbide_instrument::Event (log = warn, metric = counter) rather than a plain tracing::warn!, so a future pass could promote this into a carbide_..._total counter with sensor_id/entity_type kept in #[context] (not #[label], given their cardinality).

Based on coding guidelines: "Does the event deserve a count, rate, or duration... Declare it once as a carbide_instrument::Event and emit()."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/processor/health_report.rs` around lines 143 - 175, Replace
the plain tracing::warn! in the SensorHealth::Warning suppression branch with a
carbide_instrument::Event declared once and emitted for each suppression.
Configure it as a warning counter, keeping sensor_id and entity_type as context
fields rather than labels, while preserving the existing diagnostic fields and
Success return behavior.

Source: Coding guidelines


344-405: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing SensorFailure + BmcHealth::Ok coverage, and these four tests are a textbook table-driven candidate.

The PR description explicitly promises SensorFailure alerts remain active regardless of BMC status, but no test exercises that combination — only Fatal and Critical are covered alongside Warning. Please add a case for SensorFailure with BmcHealth::Ok to actually verify that claim.

Separately, all four new tests call the same operation (to_health_result) varying only BmcHealth/metric value and assert a discriminant on the result — exactly the case the coding guidelines call out for value_scenarios!/check_values instead of hand-rolled #[test] functions.

Based on coding guidelines: "Reach for a table whenever two or more tests call the same operation with different inputs" and "Use check_cases / check_values directly when a macro would obscure a table with several inputs or several expected fields per row." Since the exact expected SensorHealthResult isn't easily PartialEq-comparable, a small wrapper predicate keeps the table simple:

♻️ Illustrative table-driven refactor
fn is_alert(bmc_health: BmcHealth, value: f64) -> bool {
    let health = threshold_context(bmc_health);
    let metric = metric_with_value(value);
    matches!(
        HealthReportProcessor::to_health_result(&metric, &health),
        SensorHealthResult::Alert(_)
    )
}

value_scenarios!(is_alert:
    "bmc ok" {
        (BmcHealth::Ok, 3.0) => Yields(true),   // Fatal still alerts
        (BmcHealth::Ok, 85.0) => Yields(true),  // Critical still alerts
        (BmcHealth::Ok, 95.0) => Yields(true),  // SensorFailure still alerts (missing today)
        (BmcHealth::Ok, 72.0) => Yields(false), // Warning suppressed
    }
    "bmc not ok" {
        (BmcHealth::Warning, 72.0) => Yields(true), // Warning alerts when BMC disagrees
    }
);

Please confirm whether carbide-test-support is already a dev-dependency of this crate before adopting value_scenarios! here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/processor/health_report.rs` around lines 344 - 405,
Refactor the four individual tests around
HealthReportProcessor::to_health_result into a table-driven check using the
project’s existing value_scenarios!/check_values support, after confirming
carbide-test-support is available as a dev-dependency. Add the missing
BmcHealth::Ok SensorFailure case (use a value above the fatal range, such as
95.0), while preserving expected alert results for Fatal, Critical, and non-Ok
Warning and suppression for Ok Warning; use a small is_alert predicate if
needed.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/health/src/processor/health_report.rs`:
- Around line 143-175: Replace the plain tracing::warn! in the
SensorHealth::Warning suppression branch with a carbide_instrument::Event
declared once and emitted for each suppression. Configure it as a warning
counter, keeping sensor_id and entity_type as context fields rather than labels,
while preserving the existing diagnostic fields and Success return behavior.
- Around line 344-405: Refactor the four individual tests around
HealthReportProcessor::to_health_result into a table-driven check using the
project’s existing value_scenarios!/check_values support, after confirming
carbide-test-support is available as a dev-dependency. Add the missing
BmcHealth::Ok SensorFailure case (use a value above the fatal range, such as
95.0), while preserving expected alert results for Fatal, Critical, and non-Ok
Warning and suppression for Ok Warning; use a small is_alert predicate if
needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0a47ec00-b870-4566-a53a-b9ebb9cf3a12

📥 Commits

Reviewing files that changed from the base of the PR and between ae62c4b and 1b3bf1d.

📒 Files selected for processing (1)
  • crates/health/src/processor/health_report.rs

@hatamzad-nv hatamzad-nv force-pushed the fix/3402-bmc-ok-suppresses-alerts branch from 1b3bf1d to d0eed82 Compare July 13, 2026 18:48
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

🔍 Container Scan Summary

Service Total Critical High Medium Low Other
boot-artifacts-aarch64 3 0 0 3 0 0
boot-artifacts-x86_64 3 0 0 3 0 0
forge-admin-cli-x86_64 255 13 30 79 7 126
machine-validation-runner 800 40 234 292 36 198
machine_validation 800 40 234 292 36 198
machine_validation-aarch64 800 40 234 292 36 198
nvmetal-carbide 800 40 234 292 36 198
TOTAL 3461 173 966 1253 151 918

Per-CVE detail lives in the per-service grype-* artifacts (JSON + SARIF). Severity counts only — no CVE IDs published here.

@hatamzad-nv hatamzad-nv force-pushed the fix/3402-bmc-ok-suppresses-alerts branch from d0eed82 to ec2ae7e Compare July 13, 2026 21:40
@hatamzad-nv hatamzad-nv force-pushed the fix/3402-bmc-ok-suppresses-alerts branch from ec2ae7e to 8b42bad Compare July 13, 2026 21:43
@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai Would the tests here benefit from the table-driven scenario testing that is framed out by crates/test-support? I don't want to suggest a change if you don't think it would apply in this case. If so, could you recommend how to restructure the tests? Thank you!

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd . crates/test-support -t f | head -50
echo "---outline---"
ast-grep outline crates/test-support --items all 2>/dev/null | head -100
echo "---health_report test file---"
fd health_report.rs

Length of output: 1415


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n crates/test-support/src/lib.rs
echo "=== health_report.rs tests ==="
rg -n "mod tests" -A 5 crates/health/src/processor/health_report.rs
sed -n '1,400p' crates/health/src/processor/health_report.rs | grep -n "fn to_health_result\|SensorHealth\|BmcHealth\|struct\|enum" | head -60

Length of output: 25361


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,60p;80,260p' crates/health/src/processor/health_report.rs
echo "=== full test module ==="
sed -n '260,460p' crates/health/src/processor/health_report.rs

Length of output: 15770


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "test-support" crates/health/Cargo.toml crates/*/Cargo.toml 2>/dev/null
cat crates/test-support/Cargo.toml

Length of output: 12413


Good — carbide-test-support is already a dev-dependency of crates/health, so it's readily available. Now I have everything needed to give a concrete recommendation.

@chet Good question — yes, I think this is a solid fit. All five new tests funnel through the same total (non-failing) operation, classify_result(value, bmc_health) -> Option<Vec<Classification>>, and each row is just one input pair mapped to one expected output. That's exactly the shape value_scenarios!/Check/check_values was built for (the Outcome-based scenarios!/Case machinery is for fallible Result-returning operations, which doesn't apply here since to_health_result never errors).

Since there are two inputs (value and bmc_health), the cleanest approach is a small local Row struct destructured in the run = closure, per the "several inputs per row" pattern in crates/test-support/src/lib.rs. carbide-test-support is already a dev-dependency of crates/health (see crates/health/Cargo.toml), so no dependency changes are needed.

Something like this would replace the five #[test] functions:

use carbide_test_support::value_scenarios;

struct Row {
    value: f64,
    bmc_health: BmcHealth,
}

#[test]
fn bmc_ok_suppresses_only_warning_alerts() {
    value_scenarios!(run = |Row { value, bmc_health }| classify_result(value, bmc_health);
        "BMC Ok does not suppress higher-severity alerts" {
            // above upper_fatal (90.0) -> Fatal
            Row { value: 95.0, bmc_health: BmcHealth::Ok } => Some(vec![Classification::SensorFatal]),
            // above upper_critical (80.0) -> Critical
            Row { value: 85.0, bmc_health: BmcHealth::Ok } => Some(vec![Classification::SensorCritical]),
            // above range_max (100.0) -> SensorFailure
            Row { value: 150.0, bmc_health: BmcHealth::Ok } => Some(vec![Classification::SensorFailure]),
        }

        "BMC Ok suppresses Warning (treated as likely mis-tuned caution thresholds)" {
            // above upper_caution (70.0) -> Warning
            Row { value: 72.0, bmc_health: BmcHealth::Ok } => None,
        }

        "BMC not Ok still alerts on Warning" {
            Row { value: 72.0, bmc_health: BmcHealth::Warning } => Some(vec![Classification::SensorWarning]),
        }
    );
}

A couple of notes:

  • value_scenarios! labels each row with "<scenario> / <input debug repr>" on failure, so a panic will still point at the exact row (e.g. Row { value: 72.0, bmc_health: Warning }), preserving the diagnostic value of your current per-scenario test names.
  • The three "does not suppress" cases become one grouped table instead of three near-identical test functions, which reads well as a single intent ("Ok never suppresses non-Warning severities") backed by three concrete readings.
  • If you'd rather keep the granularity of separate #[test] functions (e.g., for isolated failure reporting in CI), you could instead call Check { .. }.check(...) per case, but given these are cheap, deterministic unit tests with no side effects, I'd lean toward the single grouped value_scenarios! test for conciseness.

Happy to draft this as a follow-up diff if you'd like me to open a suggestion — just let me know.

(=^・ω・^=)

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.

bug: lower_fatal not used

2 participants