Skip to content

feat: surface the swallowed errors across the fleet#3469

Open
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3179
Open

feat: surface the swallowed errors across the fleet#3469
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3179

Conversation

@chet

@chet chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

A handful of fallible operations were silent in both pipelines -- no log, no metric -- so observability couldn't see them at all, including a rack health-report persist failure discarded with Err(_) => { /* will retry */ }. Every one now logs, and the one whose failure rate is worth watching also counts.

  • carbide_dsx_exchange_consumer_health_report_persist_failures_total -- the dsx health-report persist failure now emits a single framework event that both warns and counts (rack id and error ride as log context). It's the one failure here worth a rate.
  • The silently-dropped failures now warn: the read-only and error-unwinding txn.rollback() drops in the instance / power-shelf / expected-machine handlers; the DHCP TLS-material reads that fall back to a built-in default cert; the empty product serial fed into DPF; the preingestion subprocess kill/wait drops; and the health log-file flush, rotate, and prune drops. Each keeps its crate's logging convention, and the error-unwinding rollbacks log without clobbering the original error being returned.
  • The site-explorer bmc_reset_count counted attempts, not successes: the ipmitool reset dropped its result with .ok() while the counter ticked regardless. A shared record_bmc_reset_outcome now increments only on success and logs the failure, and both the ipmitool and Redfish paths route through it -- so the counter finally means what its name says, with no rename.

The MQTT state-change send drop the issue pointed at turned out to be a test mock; the production send path already logs and meters every outcome, so nothing was left silent there.

Note: this and #3465 both touch dsx-exchange-consumer/tests/metric_exposition.rs. Whichever merges second wants #3465's de-doubled message-events assertions plus this PR's health_report_persist_failed test -- I'll reconcile at rebase time.

This supports #3179

@chet chet requested a review from a team as a code owner July 14, 2026 02:05
@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@chet Sounds good — I'll redo the full review of all changes in this PR.

(ᵘ‿ᵘ)♡

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@chet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 993c5477-3c57-47b7-87d8-8baffc1fa1e0

📥 Commits

Reviewing files that changed from the base of the PR and between 1eb3a7f and ccaba41.

📒 Files selected for processing (17)
  • crates/api-core/src/handlers/credential.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dhcp/src/lib.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/dsx-exchange-consumer/tests/metric_exposition.rs
  • crates/health/src/sink/log_file.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/metrics.rs
  • docs/observability/core_metrics.md

Walkthrough

The changes make previously ignored rollback, cleanup, filesystem, configuration, and subprocess failures observable. They add health-report persistence telemetry, warn on empty DPU serial numbers, and ensure BMC reset metrics count only successful resets.

Changes

API transaction rollback observability

Layer / File(s) Summary
Handler rollback error reporting
crates/api-core/src/handlers/*.rs
API handlers now log warnings when transaction rollback fails across read-only lookups and expected-machine batch operations.

Runtime failure diagnostics

Layer / File(s) Summary
Configuration and filesystem failure reporting
crates/dhcp/src/lib.rs, crates/health/src/sink/log_file.rs
TLS environment-variable fallbacks and log rotation operations now warn when underlying operations fail.
Subprocess and registration cleanup
crates/preingestion-manager/src/lib.rs, crates/machine-controller/src/handler/dpf.rs
Subprocess cleanup now checks kill, wait, and stderr-task completion; DPU registration warns when the serial number is empty.

Health report persistence telemetry

Layer / File(s) Summary
Persistence failure metric
crates/dsx-exchange-consumer/src/metrics.rs, crates/dsx-exchange-consumer/src/health_updater.rs, crates/dsx-exchange-consumer/tests/metric_exposition.rs, crates/dsx-exchange-consumer/Cargo.toml
Health-report persistence failures emit a warning-level labeled metric event, with tests covering logs and counter exposition.

BMC reset outcome accounting

Layer / File(s) Summary
Reset outcome recording
crates/site-explorer/src/lib.rs, crates/site-explorer/src/metrics.rs, docs/observability/core_metrics.md
IPMI and Redfish reset handling now increments the reset counter only for successful operations, with unit coverage and updated metric descriptions.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: surfacing previously swallowed errors via logging and metrics.
Description check ✅ Passed The description is directly aligned with the changeset and accurately describes the added logging, metric, and cleanup behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
crates/site-explorer/src/lib.rs (1)

3938-3969: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the outcome test table-driven and cover both transports.

This test only exercises Ipmitool, manually repeats the two cases, and does not verify the new failure log fields. Add table-driven cases for both Ipmitool and Redfish, asserting the returned boolean, metric count, and logged method/address/error.

As per coding guidelines, Rust tests for functions mapping inputs to observable results should use table-driven cases.

🤖 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/site-explorer/src/lib.rs` around lines 3938 - 3969, Update
bmc_reset_counter_counts_successes_not_attempts to use table-driven cases
covering successful and failed outcomes for both BmcResetMethod::Ipmitool and
BmcResetMethod::Redfish. For each case, assert the returned boolean,
bmc_reset_count, and failure log fields including method, address, and error;
retain the success behavior and isolate or reset metrics/log capture per case.

Source: Coding guidelines

crates/preingestion-manager/src/lib.rs (1)

2160-2191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated kill/wait/warn cleanup sequence.

The stdout-missing and stderr-missing branches duplicate the same 4-statement kill → wait → warn cleanup verbatim. Consider a small helper to avoid drift if this cleanup logic changes later.

♻️ Proposed helper extraction
+async fn kill_and_reap(cmd: &mut tokio::process::Child, address: &str, context: &str) {
+    if let Err(e) = cmd.kill().await {
+        tracing::warn!("Upgrade script {address} cleanup: failed to kill subprocess after {context}: {e}");
+    }
+    if let Err(e) = cmd.wait().await {
+        tracing::warn!("Upgrade script {address} cleanup: failed to reap subprocess after {context}: {e}");
+    }
+}

Then call kill_and_reap(&mut cmd, &address, "STDOUT capture failure").await; and kill_and_reap(&mut cmd, &address, "STDERR capture failure").await; at the two call sites.

🤖 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/preingestion-manager/src/lib.rs` around lines 2160 - 2191, Extract the
duplicated subprocess cleanup from the stdout and stderr failure branches into a
small async helper, such as kill_and_reap, accepting the command, address, and
failure context. Preserve the existing kill-then-wait order and warning
messages, then call the helper from both branches with their respective
capture-failure descriptions before marking completion and returning.
🤖 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.

Inline comments:
In `@crates/dsx-exchange-consumer/tests/metric_exposition.rs`:
- Around line 79-84: Update the test documentation comment above the
persist-failure metric test to use the complete metric name
carbide_dsx_exchange_consumer_health_report_persist_failures_total, matching the
event definition and assertion; leave the test behavior unchanged.

In `@crates/site-explorer/src/lib.rs`:
- Around line 2565-2567: Update the documentation for
carbide_site_explorer_bmc_reset_count and all dependent dashboards or alerts to
define and interpret the metric as the number of successfully completed BMC
resets, not initiated attempts. Locate references using the metric name and
revise labels, descriptions, queries, and thresholds consistently without
changing the implementation.
- Around line 2568-2581: Separate the remote BMC reset result from timestamp
persistence errors in redfish_reset_bmc and ipmitool_reset_bmc, and update
record_bmc_reset_outcome and their callers accordingly. Treat a successful
physical reset as success for metrics and control flow even when timestamp
persistence fails, while still surfacing the bookkeeping error; ensure the
Redfish path does not fall through to a second IPMI reset after the hardware
reset succeeds.

---

Nitpick comments:
In `@crates/preingestion-manager/src/lib.rs`:
- Around line 2160-2191: Extract the duplicated subprocess cleanup from the
stdout and stderr failure branches into a small async helper, such as
kill_and_reap, accepting the command, address, and failure context. Preserve the
existing kill-then-wait order and warning messages, then call the helper from
both branches with their respective capture-failure descriptions before marking
completion and returning.

In `@crates/site-explorer/src/lib.rs`:
- Around line 3938-3969: Update bmc_reset_counter_counts_successes_not_attempts
to use table-driven cases covering successful and failed outcomes for both
BmcResetMethod::Ipmitool and BmcResetMethod::Redfish. For each case, assert the
returned boolean, bmc_reset_count, and failure log fields including method,
address, and error; retain the success behavior and isolate or reset metrics/log
capture per case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: dba974ad-5c4a-49e9-bfb5-3acecfab5634

📥 Commits

Reviewing files that changed from the base of the PR and between cdcb7d5 and 1be10a0.

📒 Files selected for processing (15)
  • crates/api-core/src/handlers/credential.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dhcp/src/lib.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/dsx-exchange-consumer/tests/metric_exposition.rs
  • crates/health/src/sink/log_file.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/site-explorer/src/lib.rs

Comment thread crates/dsx-exchange-consumer/tests/metric_exposition.rs
Comment thread crates/site-explorer/src/lib.rs
Comment thread crates/site-explorer/src/lib.rs

@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: 2

🧹 Nitpick comments (4)
crates/dhcp/src/lib.rs (1)

78-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the repository’s structured logging contract for the new warnings.

These changes introduce free-form interpolation of errors, paths, and endpoint identifiers. Use structured tracing::warn! fields consistently.

  • crates/dhcp/src/lib.rs#L78-L98: replace the three log::warn! calls with structured fields and describe VarError accurately.
  • crates/preingestion-manager/src/lib.rs#L2162-L2187: emit address and error fields for cleanup warnings.
  • crates/preingestion-manager/src/lib.rs#L2206-L2210: emit address and error fields for the stderr-task warning.

As per coding guidelines and path instructions, ordinary Rust logging should use structured tracing:: macros with common values passed as fields.

🤖 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/dhcp/src/lib.rs` around lines 78 - 98, Replace the three log::warn!
calls in crates/dhcp/src/lib.rs lines 78-98 with structured tracing::warn!
fields, recording the VarError accurately alongside the relevant fallback path.
In crates/preingestion-manager/src/lib.rs lines 2162-2187, update cleanup
warnings to emit structured address and error fields; likewise update the
stderr-task warning at lines 2206-2210 to emit address and error fields. No
direct changes are needed outside these three sites.

Sources: Coding guidelines, Path instructions

crates/site-explorer/src/lib.rs (2)

3938-3967: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cover the physical-success/bookkeeping-failure path.

This test only feeds record_bmc_reset_outcome pre-composed results; it cannot catch a successful remote reset followed by timestamp persistence failure. Add coverage ensuring that success is counted and the IPMI fallback is not triggered.

🤖 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/site-explorer/src/lib.rs` around lines 3938 - 3967, Add a test
covering the real BMC reset flow where the remote reset succeeds but timestamp
persistence fails: verify the successful reset increments bmc_reset_count and
does not trigger the IPMI fallback. Exercise the higher-level reset method
rather than only record_bmc_reset_outcome, and use the existing
persistence-failure setup and fallback-observable symbols.

3943-3967: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the repository’s table-driven test pattern.

The test maps outcomes to a boolean and counter state but encodes cases sequentially. Convert these cases to the existing check_cases/scenario style. As per coding guidelines, tests mapping inputs to outputs or observable results must be table-driven.

🤖 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/site-explorer/src/lib.rs` around lines 3943 - 3967, Convert
bmc_reset_counter_counts_successes_not_attempts into the repository’s
table-driven check_cases/scenario pattern, defining success and failure inputs
with their expected boolean result and bmc_reset_count. Iterate through the
cases while preserving the existing record_bmc_reset_outcome assertions and
expected counter behavior.

Source: Coding guidelines

crates/dsx-exchange-consumer/src/health_updater.rs (1)

215-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a handler-level assertion for the new emission.

test_api_failure_does_not_cache_state verifies only cache behavior, while metric_exposition.rs emits HealthReportPersistFailed directly. Capture the failure-path log around handle_value_message and assert the rack ID and error; otherwise this test suite can pass even if this call site stops emitting the event.

🤖 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/dsx-exchange-consumer/src/health_updater.rs` around lines 215 - 222,
Update test_api_failure_does_not_cache_state around handle_value_message to
capture the handler-level HealthReportPersistFailed emission, then assert that
the captured event contains the expected rack ID and persistence error. Keep the
existing cache-behavior assertions and ensure the test specifically covers this
call site rather than only metric_exposition.rs.
🤖 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.

Inline comments:
In `@crates/health/src/sink/log_file.rs`:
- Around line 236-254: Update shift_backups to avoid using Path::exists() before
rotating or pruning files. Call fs::rename and fs::remove_file directly,
treating only ErrorKind::NotFound as an ignorable absence and logging other
filesystem errors through the existing tracing warnings.
- Around line 211-215: The rotate_if_needed() flush path must stop rotation when
file.flush() fails instead of dropping the handle and continuing. Preserve
self.current_file on failure, propagate the I/O error immediately, and only
allow the existing ownership transfer/drop behavior after a successful flush.

---

Nitpick comments:
In `@crates/dhcp/src/lib.rs`:
- Around line 78-98: Replace the three log::warn! calls in
crates/dhcp/src/lib.rs lines 78-98 with structured tracing::warn! fields,
recording the VarError accurately alongside the relevant fallback path. In
crates/preingestion-manager/src/lib.rs lines 2162-2187, update cleanup warnings
to emit structured address and error fields; likewise update the stderr-task
warning at lines 2206-2210 to emit address and error fields. No direct changes
are needed outside these three sites.

In `@crates/dsx-exchange-consumer/src/health_updater.rs`:
- Around line 215-222: Update test_api_failure_does_not_cache_state around
handle_value_message to capture the handler-level HealthReportPersistFailed
emission, then assert that the captured event contains the expected rack ID and
persistence error. Keep the existing cache-behavior assertions and ensure the
test specifically covers this call site rather than only metric_exposition.rs.

In `@crates/site-explorer/src/lib.rs`:
- Around line 3938-3967: Add a test covering the real BMC reset flow where the
remote reset succeeds but timestamp persistence fails: verify the successful
reset increments bmc_reset_count and does not trigger the IPMI fallback.
Exercise the higher-level reset method rather than only
record_bmc_reset_outcome, and use the existing persistence-failure setup and
fallback-observable symbols.
- Around line 3943-3967: Convert bmc_reset_counter_counts_successes_not_attempts
into the repository’s table-driven check_cases/scenario pattern, defining
success and failure inputs with their expected boolean result and
bmc_reset_count. Iterate through the cases while preserving the existing
record_bmc_reset_outcome assertions and expected counter behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6e1d2c76-325f-4155-ab95-ce22450149af

📥 Commits

Reviewing files that changed from the base of the PR and between cdcb7d5 and 1be10a0.

📒 Files selected for processing (15)
  • crates/api-core/src/handlers/credential.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dhcp/src/lib.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/dsx-exchange-consumer/tests/metric_exposition.rs
  • crates/health/src/sink/log_file.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/site-explorer/src/lib.rs

Comment on lines +211 to 215
if let Some(mut file) = self.current_file.take()
&& let Err(e) = file.flush()
{
tracing::warn!(error = %e, "failed to flush log file before rotation");
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

# Map the file shape first, then inspect the relevant section.
ast-grep outline crates/health/src/sink/log_file.rs --view expanded || true

echo
wc -l crates/health/src/sink/log_file.rs
echo

sed -n '170,290p' crates/health/src/sink/log_file.rs | cat -n

Repository: NVIDIA/infra-controller

Length of output: 7202


🏁 Script executed:

set -euo pipefail

# Inspect the call path and any rotation tests.
sed -n '1,170p' crates/health/src/sink/log_file.rs | cat -n
echo
sed -n '300,560p' crates/health/src/sink/log_file.rs | cat -n
echo
rg -n "rotate_if_needed|write_line\\(" crates/health/src/sink/log_file.rs crates/health/src -g '!target'

Repository: NVIDIA/infra-controller

Length of output: 18896


Do not continue rotation after a flush error.
rotate_if_needed() takes ownership of self.current_file before checking flush(). If that flush fails, the handle is dropped and rotation still proceeds, which can strand any pending bytes and mask the underlying I/O failure. Keep the writer alive, return the error, and only drop it after a successful flush.

🤖 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/sink/log_file.rs` around lines 211 - 215, The
rotate_if_needed() flush path must stop rotation when file.flush() fails instead
of dropping the handle and continuing. Preserve self.current_file on failure,
propagate the I/O error immediately, and only allow the existing ownership
transfer/drop behavior after a successful flush.

Comment on lines +236 to +254
if from.exists()
&& let Err(e) = fs::rename(&from, &to)
{
tracing::warn!(error = %e, from = %from.display(), to = %to.display(), "failed to shift rotated log file");
}
}

// current -> .1
let backup = self.rotated_path(1);
let _ = fs::rename(current_path, &backup);
if let Err(e) = fs::rename(current_path, &backup) {
tracing::warn!(error = %e, from = %current_path.display(), to = %backup.display(), "failed to rotate current log file to backup");
}

// prune the oldest backup beyond the limit
let oldest = self.rotated_path(self.max_backups + 1);
if oldest.exists() {
let _ = fs::remove_file(&oldest);
if oldest.exists()
&& let Err(e) = fs::remove_file(&oldest)
{
tracing::warn!(error = %e, path = %oldest.display(), "failed to prune oldest rotated log file");

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='crates/health/src/sink/log_file.rs'
echo "== file size =="
wc -l "$file"
echo "== relevant slice =="
sed -n '200,290p' "$file"
echo "== search exists/try_exists in file =="
rg -n 'try_exists|\.exists\(\)|rename\(|remove_file\(' "$file"

Repository: NVIDIA/infra-controller

Length of output: 4000


Avoid gating backup rotation on Path::exists()

exists() turns permission and other metadata failures into false, so shift_backups() can skip a real error without logging it. Call rename/remove_file directly and ignore only NotFound, or switch to try_exists() with explicit Err handling.

🤖 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/sink/log_file.rs` around lines 236 - 254, Update
shift_backups to avoid using Path::exists() before rotating or pruning files.
Call fs::rename and fs::remove_file directly, treating only ErrorKind::NotFound
as an ignorable absence and logging other filesystem errors through the existing
tracing warnings.

@github-actions

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.

@github-actions

Copy link
Copy Markdown

@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai! Addressed all four actionable items in the latest push:

  • Doc comment metric name (metric_exposition.rs): now uses the full carbide_dsx_exchange_consumer_health_report_persist_failures_total.
  • bmc_reset_count describe: now reads "…resets that succeeded…" (the counter increments only on success as of this PR), and core_metrics.md is regenerated to match.
  • Preingestion cleanup: extracted the duplicated kill→wait→warn sequence into a kill_and_reap helper (order, wording, and level preserved).
  • bmc_reset test: rewritten table-driven over both transports × both outcomes, asserting the returned bool, the bmc_reset_count delta, and the failure log's method/address/error fields.

On the reset-outcome finding — a physical reset that succeeds but whose timestamp write fails is counted as a failure and, on the Redfish path, falls through to a second IPMI reset — that's a real issue, but it's pre-existing: this PR only swapped the counting mechanism, not the reset methods' ?-propagation of the timestamp write or the Redfish→IPMI fall-through. Filed #3471 to fix it properly (separating the physical-reset outcome from the timestamp bookkeeping).

@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@chet Understood — thanks for the detailed rundown of the fixes. I'll redo the full review now.

Confirmed my understanding of the addressed items:

  • Full metric name restored in the metric_exposition.rs doc comment.
  • bmc_reset_count gauge description now accurately reflects success-only semantics, with core_metrics.md in sync.
  • kill_and_reap helper cleanly de-duplicates the preingestion cleanup sequence.
  • Table-driven test for record_bmc_reset_outcome covering both transports and outcomes.

Also, good call filing #3471 to separate the physical-reset outcome from timestamp-write propagation and the Redfish→IPMI fall-through — agreed that's out of scope here and worth tracking separately.

ᕕ( ᐛ )ᕗ

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@chet Thank you for the thorough follow-through — all four items look properly addressed:

  • Doc comment now matches the full metric name carbide_dsx_exchange_consumer_health_report_persist_failures_total.
  • bmc_reset_count description accurately reflects success-only counting, with docs regenerated to match.
  • The kill_and_reap helper is a clean way to deduplicate the kill→wait→warn sequence without altering behavior.
  • The table-driven test covering both transports × both outcomes gives good coverage of the returned bool, counter delta, and log fields.

Your call on the reset-outcome issue (timestamp-write failure being counted as a reset failure, and the Redfish→IPMI fall-through) makes sense — since this PR only changed how successes/failures are counted rather than the underlying ?-propagation and fallback logic, keeping that fix scoped to #3471 is reasonable. I'll keep an eye out for that follow-up.

(๑˃̵ᴗ˂̵)و

@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

🧹 Nitpick comments (1)
crates/dhcp/src/lib.rs (1)

78-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use structured tracing fields for the new operational diagnostics.

These messages interpolate errors, paths, addresses, and cleanup context into free-form text; DHCP also uses log::warn!. Keep messages static and pass values as fields such as error = %e, fallback = %path, address = %address, and context.

  • crates/dhcp/src/lib.rs#L78-L98: replace each log::warn! with structured tracing::warn!.
  • crates/preingestion-manager/src/lib.rs#L2168-L2176: structure address, context, and cleanup errors.
  • crates/preingestion-manager/src/lib.rs#L2208-L2211: structure the endpoint and join error.

As per coding guidelines and path instructions, ordinary Rust logging should use structured tracing:: macros with common values passed as fields.

🤖 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/dhcp/src/lib.rs` around lines 78 - 98, Replace the three log::warn!
calls in crates/dhcp/src/lib.rs lines 78-98 with tracing::warn! calls using
static messages and structured fields for the environment-variable errors and
fallback paths. In crates/preingestion-manager/src/lib.rs lines 2168-2176,
structure address, context, and cleanup errors; in lines 2208-2211, structure
the endpoint and join error. Preserve the existing warning content and fallback
behavior while using fields such as error = %e, fallback = %path, address =
%address, and context.

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.

Inline comments:
In `@crates/dsx-exchange-consumer/src/health_updater.rs`:
- Around line 215-222: Extend test_api_failure_does_not_cache_state to capture
the HealthReportPersistFailed telemetry emitted by the Err branch, then assert
the WARN message, matching rack_id, persist error text, and expected counter
increment. Keep the existing assertion that failed state is not cached.

---

Nitpick comments:
In `@crates/dhcp/src/lib.rs`:
- Around line 78-98: Replace the three log::warn! calls in
crates/dhcp/src/lib.rs lines 78-98 with tracing::warn! calls using static
messages and structured fields for the environment-variable errors and fallback
paths. In crates/preingestion-manager/src/lib.rs lines 2168-2176, structure
address, context, and cleanup errors; in lines 2208-2211, structure the endpoint
and join error. Preserve the existing warning content and fallback behavior
while using fields such as error = %e, fallback = %path, address = %address, and
context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2475c6ec-8ada-491c-9f8f-42b7e4b781aa

📥 Commits

Reviewing files that changed from the base of the PR and between da30d63 and fb2914e.

📒 Files selected for processing (17)
  • crates/api-core/src/handlers/credential.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dhcp/src/lib.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/dsx-exchange-consumer/tests/metric_exposition.rs
  • crates/health/src/sink/log_file.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/metrics.rs
  • docs/observability/core_metrics.md

Comment thread crates/dsx-exchange-consumer/src/health_updater.rs
@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — on the structured-logging finding:

  • Preingestion (the kill_and_reap cleanup warnings + the STDERR-join warning): addressed. They now pass address, context, and error as structured tracing fields instead of interpolating them into the message.
  • dhcp log::warn!: deferred to a follow-up (Migrate the dhcp crate from the log facade to structured tracing #3475). dhcp is the codebase's lone log:: facade island — 47 of the workspace's 50 log:: calls, against ~3,380 tracing:: calls elsewhere — so converting just these three lines would leave them the only tracing:: calls among ~50 log:: ones. It's better done as a crate-wide migration (including confirming whether dhcp-server bridges logtracing today), which Migrate the dhcp crate from the log facade to structured tracing #3475 tracks.

@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@chet Understood — I'll redo the full review of all changes in this PR now.

ヽ(´▽`)/

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@chet Thank you for the update — both points make sense.

  • Preingestion structured logging: confirmed — passing address, context, and error as structured tracing fields (rather than string-interpolating them into the message) is the correct approach and keeps these warnings queryable/filterable in log aggregation, consistent with the rest of the codebase.
  • dhcp log::warn! deferral to #3475: agreed, this is the right call. Converting just those three call sites would create a worse inconsistency (3 lone tracing:: calls among ~50 log:: calls) than leaving them as-is temporarily. A crate-wide migration — with the added step of confirming whether dhcp-server already bridges logtracing — is the more disciplined approach and keeps this PR properly scoped.

Nothing further from me on this thread.

ᕦ(ò_óˇ)ᕤ

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/dhcp/src/lib.rs`:
- Around line 78-98: Update the TLS path configuration fields
forge_root_ca_path, forge_client_cert_path, and forge_client_key_path to reject
empty values returned by std::env::var, accepting only non-empty paths and
falling back to the corresponding tls_default constants with the existing
warnings. Apply identical validation to all three paths before they reach
build_forge_client_config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 64b1080c-9343-4c87-95a2-995f6e47aab9

📥 Commits

Reviewing files that changed from the base of the PR and between fb2914e and 8dd85cc.

📒 Files selected for processing (17)
  • crates/api-core/src/handlers/credential.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dhcp/src/lib.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/dsx-exchange-consumer/tests/metric_exposition.rs
  • crates/health/src/sink/log_file.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/metrics.rs
  • docs/observability/core_metrics.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/api-core/src/handlers/credential.rs
  • crates/site-explorer/src/metrics.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/health/src/sink/log_file.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/site-explorer/src/lib.rs

Comment thread crates/dhcp/src/lib.rs
Comment on lines +78 to +98
forge_root_ca_path: std::env::var("FORGE_ROOT_CAFILE_PATH").unwrap_or_else(|e| {
log::warn!(
"FORGE_ROOT_CAFILE_PATH unset or unreadable ({e}); falling back to the built-in default root CA at {}",
tls_default::ROOT_CA
);
tls_default::ROOT_CA.to_string()
}),
forge_client_cert_path: std::env::var("FORGE_CLIENT_CERT_PATH").unwrap_or_else(|e| {
log::warn!(
"FORGE_CLIENT_CERT_PATH unset or unreadable ({e}); falling back to the built-in default client certificate at {}",
tls_default::CLIENT_CERT
);
tls_default::CLIENT_CERT.to_string()
}),
forge_client_key_path: std::env::var("FORGE_CLIENT_KEY_PATH").unwrap_or_else(|e| {
log::warn!(
"FORGE_CLIENT_KEY_PATH unset or unreadable ({e}); falling back to the built-in default client key at {}",
tls_default::CLIENT_KEY
);
tls_default::CLIENT_KEY.to_string()
}),

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Treat empty TLS-path environment variables as invalid.

std::env::var returns Ok("") for an explicitly empty variable, so these branches skip the fallback and pass empty certificate/key paths to build_forge_client_config. Validate the returned paths before accepting them, and use the built-in defaults when they are empty.

Proposed approach
-            forge_root_ca_path: std::env::var("FORGE_ROOT_CAFILE_PATH").unwrap_or_else(|e| {
+            forge_root_ca_path: match std::env::var("FORGE_ROOT_CAFILE_PATH") {
+                Ok(path) if !path.is_empty() => path,
+                Ok(_) | Err(_) => {
+                    // log the appropriate reason, then:
+                    tls_default::ROOT_CA.to_string()
+                }
+            },

Apply the same validation to the client certificate and key paths.

As per path instructions, DHCP TLS-path fallbacks must be reviewed for configuration correctness and runtime behavior.

🤖 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/dhcp/src/lib.rs` around lines 78 - 98, Update the TLS path
configuration fields forge_root_ca_path, forge_client_cert_path, and
forge_client_key_path to reject empty values returned by std::env::var,
accepting only non-empty paths and falling back to the corresponding tls_default
constants with the existing warnings. Apply identical validation to all three
paths before they reach build_forge_client_config.

Source: Path instructions

@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 (1)
crates/preingestion-manager/src/lib.rs (1)

2160-2196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use start_kill() for separate kill/reap diagnostics.

tokio::process::Child::kill().await already performs the kill and waits, so the following wait().await is not an independent reap step. Use start_kill() followed by wait().await, or keep only kill().await and log one cleanup failure. Tokio documents kill as SIGKILL followed by wait. (docs.rs)

🤖 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/preingestion-manager/src/lib.rs` around lines 2160 - 2196, Update the
local kill_and_reap helper to call Child::start_kill() before
Child::wait().await, preserving the separate warning diagnostics for kill
initiation and process reaping. Keep the existing address, context, and error
fields and cleanup call sites unchanged.
🤖 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/preingestion-manager/src/lib.rs`:
- Around line 2160-2196: Update the local kill_and_reap helper to call
Child::start_kill() before Child::wait().await, preserving the separate warning
diagnostics for kill initiation and process reaping. Keep the existing address,
context, and error fields and cleanup call sites unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 30d872a5-7572-4f7e-83f9-01cda9c528b0

📥 Commits

Reviewing files that changed from the base of the PR and between 5d8e008 and 8dd85cc.

📒 Files selected for processing (17)
  • crates/api-core/src/handlers/credential.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/instance.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/rack.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/dhcp/src/lib.rs
  • crates/dsx-exchange-consumer/Cargo.toml
  • crates/dsx-exchange-consumer/src/health_updater.rs
  • crates/dsx-exchange-consumer/src/metrics.rs
  • crates/dsx-exchange-consumer/tests/metric_exposition.rs
  • crates/health/src/sink/log_file.rs
  • crates/machine-controller/src/handler/dpf.rs
  • crates/preingestion-manager/src/lib.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/metrics.rs
  • docs/observability/core_metrics.md

A handful of fallible operations were silent in both pipelines -- no log, no metric -- so observability couldn't see them at all, including a rack health-report persist failure discarded with `Err(_) => { /* will retry */ }`. Every one now logs, and the one whose failure rate is worth watching also counts.

Primary callouts are:

- `carbide_dsx_exchange_consumer_health_report_persist_failures_total` -- the dsx health-report persist failure now emits a single framework event that both warns and counts (the rack id and error ride as log context). It's the one failure here worth a rate, so it's the one that gets a counter.
- The silently-dropped failures now warn: the read-only and error-unwinding `txn.rollback()` drops in the instance, power-shelf, and expected-machine handlers; the DHCP TLS-material reads that fall back to a built-in default cert; the empty product serial fed into DPF; the preingestion subprocess kill/wait drops; and the health log-file flush, rotate, and prune drops. Each keeps its crate's logging convention, and the error-unwinding rollbacks log without clobbering the original error being returned.
- The site-explorer `bmc_reset_count` counted attempts, not successes: the ipmitool reset dropped its result with `.ok()` while the counter ticked regardless. A shared `record_bmc_reset_outcome` now increments only on success and logs the failure, and both the ipmitool and Redfish paths route through it -- so the counter finally means what its name says, with no rename.

The MQTT state-change send drop the issue pointed at turned out to be a test mock; the production send path already logs and meters every outcome, so nothing was left silent there.

Tests added!

This supports NVIDIA#3179

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet

chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@chet Understood — I'll redo the full review of all changes in this PR now.

(๑˃̵ᴗ˂̵)و

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 20 minutes.

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.

2 participants