Skip to content

feat(health): Emit latency and health metrics for BMC interactions - #4720

Open
Matthias247 wants to merge 2 commits into
NVIDIA:mainfrom
Matthias247:bmclatency
Open

feat(health): Emit latency and health metrics for BMC interactions#4720
Matthias247 wants to merge 2 commits into
NVIDIA:mainfrom
Matthias247:bmclatency

Conversation

@Matthias247

Copy link
Copy Markdown
Contributor

Adds new config file flags to hw-health which make it emit a histogram metric for requests to BMCs. The metric with all properties attached will look like:

carbide_hardware_health_bmc_latency_ms_bucket{http_response_status_code="200",http_request_method="GET",http_path="/redfish/
  v1",server_address="1.3.5.12",url_scheme="https",bmc_vendor="NVIDIA",bmc_model="GB200 BMC",le="100"} 1

The metric can be explicitly enabled via setting

[metrics]
enable_bmc_latency_metrics = true

By default it will contain all available fields. If only a subset of fields should be emitted to reduce the amount of time series, the setting metrics.bmc_latency_attributes can be used to explicitly specify the attributes that should be included. Adding all to the list leads to emitting all attributes (default) again.

[metrics]
bmc_latency_attributes = ["http_response_status_code", "server_address", "url_scheme"]

Limitation:

The status code for success responses are estimated since nv-redfish does not expose it. It is always set to 200 for GET requests and 201 for creation requests. Status codes for error responses are accurate.

Related issues

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

Additional Notes

Adds new config file flags to hw-health which make it emit a histogram metric for requests to BMCs.
The metric with all properties attached will look like:
```
carbide_hardware_health_bmc_latency_ms_bucket{http_response_status_code="200",http_request_method="GET",http_path="/redfish/
  v1",server_address="1.3.5.12",url_scheme="https",bmc_vendor="NVIDIA",bmc_model="GB200 BMC",le="100"} 1
```

The metric can be explicitly enabled via setting
```toml
[metrics]
enable_bmc_latency_metrics = true
```

By default it will contain all available fields. If only a subset of fields should be emitted to reduce the amount of time series, the setting `metrics.bmc_latency_attributes` can be used to explicitly specify the attributes that should be included. Adding `all` to the list leads to emitting all attributes (default) again.

```toml
[metrics]
bmc_latency_attributes = ["http_response_status_code", "server_address", "url_scheme"]
```

Limitation:

The status code for success responses are estimated since nv-redfish does not expose it. It is always set to 200 for GET requests and 201 for creation requests. Status codes for error responses are accurate.
@Matthias247
Matthias247 requested a review from a team as a code owner August 7, 2026 20:36
@coderabbitai

coderabbitai Bot commented Aug 7, 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: 04597df0-64a3-42f3-bf21-120dafcb3c32

📥 Commits

Reviewing files that changed from the base of the PR and between 2886478 and 67bf8a8.

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

Summary by CodeRabbit

  • New Features
    • Added optional BMC request-latency metrics.
    • Metrics can include request duration, HTTP status, method, path, server address, URL scheme, BMC vendor, and model.
    • Added configurable latency buckets and selectable metric labels.
    • Metrics are disabled by default and can be enabled through configuration.
  • Bug Fixes
    • Added validation to prevent enabling metrics without selected attributes.
    • Unavailable BMC identity information is reported as “unknown.”

Walkthrough

The change adds optional, configurable BMC latency histograms. BmcClient records Redfish request dimensions and durations, captures BMC identity, and receives the shared metrics handle through all endpoint sources.

Changes

BMC latency metrics

Layer / File(s) Summary
Metrics contract and configuration
crates/health/src/metrics.rs, crates/health/src/config.rs, crates/health/example/config.example.toml
Adds selectable latency attributes, histogram buckets, observation recording, configuration defaults, validation, parsing tests, and example settings.
BMC request instrumentation
crates/health/src/bmc.rs
Adds latency recording for Redfish operations, HTTP status classification, request-path normalization, endpoint metadata, BMC identity labels, authentication-retry handling, and metric tests.
Endpoint metrics propagation
crates/health/src/lib.rs, crates/health/src/api_client.rs, crates/health/src/endpoint/*, crates/health/src/discovery/spawn.rs
Creates metrics during startup when enabled and propagates the optional handle into static, API, and cluster BmcClient instances. Existing test call sites pass None.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HealthService
  participant MetricsRegistry
  participant EndpointSource
  participant BmcClient
  HealthService->>MetricsRegistry: create BmcLatencyMetrics when enabled
  HealthService->>EndpointSource: pass optional metrics handle
  EndpointSource->>BmcClient: construct BmcClient with metrics handle
  BmcClient->>BmcClient: record Redfish request latency and labels
Loading

Suggested labels: rack health

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the new BMC latency and health metrics feature.
Description check ✅ Passed The description directly explains the new BMC latency histogram metrics, configuration, labels, limitations, and testing.
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.

@Matthias247

Copy link
Copy Markdown
Contributor Author

Implementation thoughts: This implementation generates the metrics in the Bmc client wrapper. It thereby is limited to the info that the nvredfish exposes via its public interface and involves a fair amount of code changes.

Other approaches we can consider are:

  • Let nvredfish emit the metrics directly (but that binds it to a metrics framework)
  • Let nvredfish emit a tracing::Event with the data required (request type, destination, latency). And install a tracing::Subscriber in health which transforms it into a metric. That might however lack some data that nvredfish might not have (e.g. machine_id or rack_id) and that might be harder to retrieve from the subscriber context. But it would have less code throughout health.

@poroh @yoks Let me know if there's any preference

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

🤖 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/bmc.rs`:
- Around line 790-802: Update http_error_status_code to locate the “HTTP ”
marker and parse only the immediately following three-digit status token,
avoiding unrelated numeric values such as URL IP octets; preserve validation
through http::StatusCode and add a regression test covering a message containing
https://10.0.0.100 alongside HTTP 500.

In `@crates/health/src/metrics.rs`:
- Around line 58-94: Remove BmcLatencyAttribute::ServerAddress from the default
BmcLatencyAttribute::ATTRIBUTES set so the All selector does not automatically
expose unbounded BMC IP addresses as Prometheus labels. Leave the enum and
label_name mapping available for explicit opt-in, while preserving the other
default attributes.
- Around line 113-162: Update BmcLatencyMetrics::new_with_attributes to prevent
BmcLatencyAttribute::All from being stored or used as a concrete label. Filter
All from the attributes before constructing label_names and saving the
attributes, or reject it through the constructor’s existing Result contract;
ensure direct callers cannot cause observe() to reach its unreachable! branch.
- Around line 127-134: Rename the metric constructed in the latency HistogramVec
to use the `{prefix}_bmc_latency_milliseconds` suffix, then update all existing
test and example configuration references to the new name. Extend
`test_integration` to exercise the health endpoint so the generated metric
catalogue includes this metric and its HELP text.
🪄 Autofix

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: 7cd43cfc-4a56-408a-831c-e1525610c00b

📥 Commits

Reviewing files that changed from the base of the PR and between 355618d and d34742e.

📒 Files selected for processing (10)
  • crates/health/example/config.example.toml
  • crates/health/src/api_client.rs
  • crates/health/src/bmc.rs
  • crates/health/src/config.rs
  • crates/health/src/discovery/spawn.rs
  • crates/health/src/endpoint/cluster.rs
  • crates/health/src/endpoint/mod.rs
  • crates/health/src/endpoint/sources.rs
  • crates/health/src/lib.rs
  • crates/health/src/metrics.rs

Comment thread crates/health/src/bmc.rs Outdated
Comment on lines +790 to +802
fn http_error_status_code(message: &str) -> Option<String> {
message
.split(|character: char| !character.is_ascii_digit())
.find_map(|token| {
if token.len() != 3 {
return None;
}
let code = token.parse::<u16>().ok()?;
http::StatusCode::from_u16(code)
.ok()
.map(|status| status.as_u16().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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect how HealthError::HttpError messages are constructed to confirm whether they can embed IPs or other 3-digit numbers before the real status code.
rg -nP -C4 'HttpError\s*\(' --type=rust crates/health

Repository: NVIDIA/infra-controller

Length of output: 17517


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- function and nearby tests ---'
sed -n '780,825p' crates/health/src/bmc.rs
rg -n -C6 'http_error_status_code|error_status_code|http_response_status_code' crates/health/src/bmc.rs

printf '%s\n' '--- focused behavioral probe ---'
python3 - <<'PY'
import re

def status_code_current(message):
    for token in re.split(r'[^0-9]', message):
        if len(token) != 3:
            continue
        code = int(token)
        # http::StatusCode::from_u16 accepts 100..=599.
        if 100 <= code <= 599:
            return str(code)
    return None

def status_code_proposed(message):
    parts = message.split("HTTP ", 1)
    if len(parts) != 2:
        return None
    for token in re.split(r'[^0-9]', parts[1]):
        if len(token) == 3:
            code = int(token)
            if 100 <= code <= 599:
                return str(code)
    return None

cases = [
    "https://10.0.0.100:8443: HTTP 500 for switch 12",
    "https://10.0.0.7:8443: HTTP 500 for switch 12",
    "request failed with HTTP 404",
    "request failed with HTTP 500: body contains 404",
    "request failed with HTTP 500: retry 404",
    "request failed without a status: retry 500",
]
for message in cases:
    print(f"{message!r}")
    print(f"  current={status_code_current(message)!r}")
    print(f"  proposed={status_code_proposed(message)!r}")
PY

Repository: NVIDIA/infra-controller

Length of output: 5847


Parse only the status token after HTTP .

HealthError::HttpError includes URLs and other free-form values. The current scan can report an IP octet such as 100 from https://10.0.0.100 instead of HTTP 500. Parse the 3-digit token immediately after HTTP and add a regression test for this input.

🤖 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/bmc.rs` around lines 790 - 802, Update
http_error_status_code to locate the “HTTP ” marker and parse only the
immediately following three-digit status token, avoiding unrelated numeric
values such as URL IP octets; preserve validation through http::StatusCode and
add a regression test covering a message containing https://10.0.0.100 alongside
HTTP 500.

Comment on lines +58 to +94
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BmcLatencyAttribute {
All,
HttpResponseStatusCode,
HttpRequestMethod,
HttpPath,
ServerAddress,
UrlScheme,
BmcVendor,
BmcModel,
}

impl BmcLatencyAttribute {
pub const ATTRIBUTES: [Self; 7] = [
Self::HttpResponseStatusCode,
Self::HttpRequestMethod,
Self::HttpPath,
Self::ServerAddress,
Self::UrlScheme,
Self::BmcVendor,
Self::BmcModel,
];

pub fn label_name(self) -> &'static str {
match self {
Self::All => "all",
Self::HttpResponseStatusCode => "http_response_status_code",
Self::HttpRequestMethod => "http_request_method",
Self::HttpPath => "http_path",
Self::ServerAddress => "server_address",
Self::UrlScheme => "url_scheme",
Self::BmcVendor => "bmc_vendor",
Self::BmcModel => "bmc_model",
}
}
}

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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Bound the label cardinality before enabling this by default.

BmcLatencyAttribute::ServerAddress puts the BMC IP address directly into a Prometheus label. ATTRIBUTES includes it, and the All selector (the documented default in config.rs and config.example.toml) pulls it in automatically. Combined with HttpPath, HttpRequestMethod, HttpResponseStatusCode, BmcVendor, and BmcModel, every distinct BMC endpoint multiplies the number of histogram series. On a large fleet, this can produce a very high cardinality series set once an operator sets enable_bmc_latency_metrics = true without narrowing bmc_latency_attributes.

Exclude ServerAddress from the default attribute set, or document the cardinality cost prominently next to enable_bmc_latency_metrics in config.rs and config.example.toml so operators narrow the label set deliberately before enabling this in production.

As per coding guidelines: "Keep metric label cardinality bounded; use typed bounded labels and put machine IDs, IPs, and error text in context or logs rather than labels."

🤖 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/metrics.rs` around lines 58 - 94, Remove
BmcLatencyAttribute::ServerAddress from the default
BmcLatencyAttribute::ATTRIBUTES set so the All selector does not automatically
expose unbounded BMC IP addresses as Prometheus labels. Leave the enum and
label_name mapping available for explicit opt-in, while preserving the other
default attributes.

Source: Coding guidelines

Comment on lines +113 to +162
impl BmcLatencyMetrics {
pub fn new(registry: &Registry, prefix: &str) -> Result<Self, prometheus::Error> {
Self::new_with_attributes(registry, prefix, &BmcLatencyAttribute::ATTRIBUTES)
}

pub fn new_with_attributes(
registry: &Registry,
prefix: &str,
attributes: &[BmcLatencyAttribute],
) -> Result<Self, prometheus::Error> {
let label_names = attributes
.iter()
.map(|attribute| attribute.label_name())
.collect::<Vec<_>>();
let latency_ms = HistogramVec::new(
HistogramOpts::new(
format!("{prefix}_bmc_latency_ms"),
"Duration of outbound Redfish HTTP requests to BMCs, in milliseconds",
)
.buckets(bmc_latency_buckets_ms()),
&label_names,
)?;
registry.register(Box::new(latency_ms.clone()))?;

Ok(Self {
latency_ms,
attributes: attributes.to_vec(),
})
}

pub fn observe(&self, observation: BmcLatencyObservation<'_>) {
let labels = self
.attributes
.iter()
.map(|attribute| match attribute {
BmcLatencyAttribute::All => unreachable!("all is not a concrete metric label"),
BmcLatencyAttribute::HttpResponseStatusCode => observation.status_code,
BmcLatencyAttribute::HttpRequestMethod => observation.method,
BmcLatencyAttribute::HttpPath => observation.path,
BmcLatencyAttribute::ServerAddress => observation.server_address,
BmcLatencyAttribute::UrlScheme => observation.url_scheme,
BmcLatencyAttribute::BmcVendor => observation.bmc_vendor.unwrap_or("unknown"),
BmcLatencyAttribute::BmcModel => observation.bmc_model.unwrap_or("unknown"),
})
.collect::<Vec<_>>();
self.latency_ms
.with_label_values(&labels)
.observe(observation.duration.as_secs_f64() * 1_000.0);
}
}

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

Guard against All reaching observe() as a concrete label.

new_with_attributes is pub and stores attributes without filtering BmcLatencyAttribute::All. Every current caller (config.rs::bmc_latency_attributes(), the test helpers in bmc.rs) filters All out first, but nothing in new_with_attributes itself enforces that. A caller who passes &[BmcLatencyAttribute::All] directly builds a histogram with a label literally named "all", and every call to observe() then panics at unreachable!("all is not a concrete metric label").

Filter All out of attributes inside new_with_attributes, or return an error when it is present, so this public constructor cannot be misused into a runtime panic.

🔒️ Proposed fix to make `new_with_attributes` reject/filter `All`
     pub fn new_with_attributes(
         registry: &Registry,
         prefix: &str,
         attributes: &[BmcLatencyAttribute],
     ) -> Result<Self, prometheus::Error> {
-        let label_names = attributes
+        let attributes: Vec<BmcLatencyAttribute> = attributes
             .iter()
+            .copied()
+            .filter(|attribute| *attribute != BmcLatencyAttribute::All)
+            .collect();
+        let label_names = attributes
+            .iter()
             .map(|attribute| attribute.label_name())
             .collect::<Vec<_>>();
         let latency_ms = HistogramVec::new(
             HistogramOpts::new(
                 format!("{prefix}_bmc_latency_ms"),
                 "Duration of outbound Redfish HTTP requests to BMCs, in milliseconds",
             )
             .buckets(bmc_latency_buckets_ms()),
             &label_names,
         )?;
         registry.register(Box::new(latency_ms.clone()))?;

         Ok(Self {
             latency_ms,
-            attributes: attributes.to_vec(),
+            attributes,
         })
     }
As per coding guidelines: "Design APIs to be hard to misuse and keep declarations private by default, widening visibility only for actual callers."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
impl BmcLatencyMetrics {
pub fn new(registry: &Registry, prefix: &str) -> Result<Self, prometheus::Error> {
Self::new_with_attributes(registry, prefix, &BmcLatencyAttribute::ATTRIBUTES)
}
pub fn new_with_attributes(
registry: &Registry,
prefix: &str,
attributes: &[BmcLatencyAttribute],
) -> Result<Self, prometheus::Error> {
let label_names = attributes
.iter()
.map(|attribute| attribute.label_name())
.collect::<Vec<_>>();
let latency_ms = HistogramVec::new(
HistogramOpts::new(
format!("{prefix}_bmc_latency_ms"),
"Duration of outbound Redfish HTTP requests to BMCs, in milliseconds",
)
.buckets(bmc_latency_buckets_ms()),
&label_names,
)?;
registry.register(Box::new(latency_ms.clone()))?;
Ok(Self {
latency_ms,
attributes: attributes.to_vec(),
})
}
pub fn observe(&self, observation: BmcLatencyObservation<'_>) {
let labels = self
.attributes
.iter()
.map(|attribute| match attribute {
BmcLatencyAttribute::All => unreachable!("all is not a concrete metric label"),
BmcLatencyAttribute::HttpResponseStatusCode => observation.status_code,
BmcLatencyAttribute::HttpRequestMethod => observation.method,
BmcLatencyAttribute::HttpPath => observation.path,
BmcLatencyAttribute::ServerAddress => observation.server_address,
BmcLatencyAttribute::UrlScheme => observation.url_scheme,
BmcLatencyAttribute::BmcVendor => observation.bmc_vendor.unwrap_or("unknown"),
BmcLatencyAttribute::BmcModel => observation.bmc_model.unwrap_or("unknown"),
})
.collect::<Vec<_>>();
self.latency_ms
.with_label_values(&labels)
.observe(observation.duration.as_secs_f64() * 1_000.0);
}
}
impl BmcLatencyMetrics {
pub fn new(registry: &Registry, prefix: &str) -> Result<Self, prometheus::Error> {
Self::new_with_attributes(registry, prefix, &BmcLatencyAttribute::ATTRIBUTES)
}
pub fn new_with_attributes(
registry: &Registry,
prefix: &str,
attributes: &[BmcLatencyAttribute],
) -> Result<Self, prometheus::Error> {
let attributes: Vec<BmcLatencyAttribute> = attributes
.iter()
.copied()
.filter(|attribute| *attribute != BmcLatencyAttribute::All)
.collect();
let label_names = attributes
.iter()
.map(|attribute| attribute.label_name())
.collect::<Vec<_>>();
let latency_ms = HistogramVec::new(
HistogramOpts::new(
format!("{prefix}_bmc_latency_ms"),
"Duration of outbound Redfish HTTP requests to BMCs, in milliseconds",
)
.buckets(bmc_latency_buckets_ms()),
&label_names,
)?;
registry.register(Box::new(latency_ms.clone()))?;
Ok(Self {
latency_ms,
attributes,
})
}
pub fn observe(&self, observation: BmcLatencyObservation<'_>) {
let labels = self
.attributes
.iter()
.map(|attribute| match attribute {
BmcLatencyAttribute::All => unreachable!("all is not a concrete metric label"),
BmcLatencyAttribute::HttpResponseStatusCode => observation.status_code,
BmcLatencyAttribute::HttpRequestMethod => observation.method,
BmcLatencyAttribute::HttpPath => observation.path,
BmcLatencyAttribute::ServerAddress => observation.server_address,
BmcLatencyAttribute::UrlScheme => observation.url_scheme,
BmcLatencyAttribute::BmcVendor => observation.bmc_vendor.unwrap_or("unknown"),
BmcLatencyAttribute::BmcModel => observation.bmc_model.unwrap_or("unknown"),
})
.collect::<Vec<_>>();
self.latency_ms
.with_label_values(&labels)
.observe(observation.duration.as_secs_f64() * 1_000.0);
}
}
🤖 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/metrics.rs` around lines 113 - 162, Update
BmcLatencyMetrics::new_with_attributes to prevent BmcLatencyAttribute::All from
being stored or used as a concrete label. Filter All from the attributes before
constructing label_names and saving the attributes, or reject it through the
constructor’s existing Result contract; ensure direct callers cannot cause
observe() to reach its unreachable! branch.

Source: Coding guidelines

Comment on lines +127 to +134
let latency_ms = HistogramVec::new(
HistogramOpts::new(
format!("{prefix}_bmc_latency_ms"),
"Duration of outbound Redfish HTTP requests to BMCs, in milliseconds",
)
.buckets(bmc_latency_buckets_ms()),
&label_names,
)?;

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for a metrics catalogue file that documents framework/hardware-health metrics.
rg -n --iglob '*metric*catalog*' -l . 2>/dev/null
fd -i 'metric' -e md -e toml | xargs -I{} rg -n 'bmc_latency|carbide_hardware_health' {} 2>/dev/null

Repository: NVIDIA/infra-controller

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file structure ---'
ast-grep outline crates/health/src/metrics.rs 2>/dev/null || true

printf '%s\n' '--- target implementation ---'
sed -n '1,220p' crates/health/src/metrics.rs

printf '%s\n' '--- metric references and registration ---'
rg -n -C 3 'bmc_latency|HistogramVec|metrics catalogue|metric catalogue|catalogue|Registry::register|register\(' crates .github 2>/dev/null | head -n 500

printf '%s\n' '--- candidate catalogue and integration-test files ---'
git ls-files | rg -i '(^|/)(metric|metrics|catalog|catalogue|test_integration|integration)' | head -n 300

Repository: NVIDIA/infra-controller

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- core metrics catalogue ---'
if [ -f docs/observability/core_metrics.md ]; then
  rg -n -C 3 'bmc|hardware_health|latency|component_failures|component_duration' docs/observability/core_metrics.md | head -n 300
  wc -l docs/observability/core_metrics.md
else
  printf '%s\n' 'catalogue file not found'
fi

printf '%s\n' '--- metric documentation tooling ---'
sed -n '1,260p' crates/xtask/src/metric_docs.rs
rg -n -C 5 'test_integration|core_metrics\.md|check-metric-docs|metrics scrape|/metrics' crates/xtask .github Makefile* Cargo.toml docs 2>/dev/null | head -n 500

printf '%s\n' '--- health metric construction and callers ---'
rg -n -C 5 'BmcLatencyMetrics::|enable_bmc_latency_metrics|MetricsConfig|create_.*registry|framework_registry' crates/health crates | head -n 500

Repository: NVIDIA/infra-controller

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact catalogue and health metric names ---'
rg -n 'carbide_hardware_health|bmc_latency_ms|bmc_latency_milliseconds|component_failures|component_duration' docs crates .github Makefile.toml 2>/dev/null | head -n 200

printf '%s\n' '--- integration catalogue generation implementation ---'
rg -n -C 8 'core_metrics\.md|test_integration|generate.*metric|metric.*catalog|scrape.*metrics|/metrics' --glob '*.rs' --glob '*.toml' --glob '*.md' . 2>/dev/null | rg -v 'target/' | head -n 500

printf '%s\n' '--- health tests and callers of BmcLatencyMetrics ---'
rg -n -C 8 'BmcLatencyMetrics|BmcLatencyObservation|bmc_latency' crates/health crates/*/tests crates/*/src 2>/dev/null | head -n 500

Repository: NVIDIA/infra-controller

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

metrics = Path("crates/health/src/metrics.rs").read_text()
config = Path("crates/health/src/config.rs").read_text()
catalogue = Path("docs/observability/core_metrics.md").read_text()
integration = Path("crates/api-integration-tests/tests/lib.rs").read_text()
health_bmc = Path("crates/health/src/bmc.rs").read_text()

name_expr = re.search(r'format!\("\{prefix\}_bmc_latency_ms"\)', metrics)
default_prefix = re.search(r'prefix:\s*"([^"]+)"\.to_string\(\)', config).group(1)
actual = f"{default_prefix}_bmc_latency_ms"
print(f"metric_name_expression_found={bool(name_expr)}")
print(f"default_prefix={default_prefix}")
print(f"default_metric_name={actual}")
print(f"catalogue_contains_default_metric={f'<td>{actual}</td>' in catalogue}")
print(f"catalogue_contains_canonical_candidate={f'<td>{default_prefix}_bmc_latency_milliseconds</td>' in catalogue}")
print(f"test_integration_scrapes_metrics={bool(re.search(r'generate_core_metric_docs\\(&test_env\\.carbide_metrics_addrs\\)', integration))}")
print(f"health_bmc_tests_assert_current_name={bool(re.search(r'test_health_bmc_latency_ms', health_bmc))}")
print(f"health_endpoint_in_integration_addresses={('health' in integration.lower() and 'metrics_addrs' in integration)}")
PY

printf '%s\n' '--- focused integration setup and health metric tests ---'
sed -n '260,330p' crates/api-integration-tests/tests/lib.rs
sed -n '2390,2470p' crates/health/src/bmc.rs
sed -n '400,425p' crates/health/example/config.example.toml

Repository: NVIDIA/infra-controller

Length of output: 546


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

metrics = Path("crates/health/src/metrics.rs").read_text()
config = Path("crates/health/src/config.rs").read_text()
catalogue = Path("docs/observability/core_metrics.md").read_text()
integration = Path("crates/api-integration-tests/tests/lib.rs").read_text()
health_bmc = Path("crates/health/src/bmc.rs").read_text()

name_expr = re.search(r'format!\("\{prefix\}_bmc_latency_ms"\)', metrics)
default_prefix = re.search(r'prefix:\s*"([^"]+)"\.to_string\(\)', config).group(1)
actual = f"{default_prefix}_bmc_latency_ms"
integration_scrape = bool(re.search(r'generate_core_metric_docs\(&test_env\.carbide_metrics_addrs\)', integration))
health_test_name = bool(re.search(r'test_health_bmc_latency_ms', health_bmc))
health_endpoint = 'health' in integration.lower() and 'metrics_addrs' in integration

print(f"metric_name_expression_found={bool(name_expr)}")
print(f"default_prefix={default_prefix}")
print(f"default_metric_name={actual}")
print(f"catalogue_contains_default_metric={f'<td>{actual}</td>' in catalogue}")
print(f"catalogue_contains_canonical_candidate={f'<td>{default_prefix}_bmc_latency_milliseconds</td>' in catalogue}")
print(f"test_integration_scrapes_metrics={integration_scrape}")
print(f"health_bmc_tests_assert_current_name={health_test_name}")
print(f"health_endpoint_in_integration_addresses={health_endpoint}")
PY

printf '%s\n' '--- focused integration catalogue generation ---'
sed -n '260,330p' crates/api-integration-tests/tests/lib.rs

printf '%s\n' '--- focused health metric tests ---'
sed -n '2390,2470p' crates/health/src/bmc.rs

printf '%s\n' '--- example configuration ---'
sed -n '400,425p' crates/health/example/config.example.toml

Repository: NVIDIA/infra-controller

Length of output: 6797


Use _milliseconds and add catalogue coverage.

Rename {prefix}_bmc_latency_ms to {prefix}_bmc_latency_milliseconds. Add the health endpoint to test_integration coverage so the generated catalogue includes the metric and its HELP text. Update the existing test and example configuration references.

🤖 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/metrics.rs` around lines 127 - 134, Rename the metric
constructed in the latency HistogramVec to use the
`{prefix}_bmc_latency_milliseconds` suffix, then update all existing test and
example configuration references to the new name. Extend `test_integration` to
exercise the health endpoint so the generated metric catalogue includes this
metric and its HELP text.

Source: Coding guidelines

@poroh

poroh commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Implementation thoughts: This implementation generates the metrics in the Bmc client wrapper. It thereby is limited to the info that the nvredfish exposes via its public interface and involves a fair amount of code changes.

Other approaches we can consider are:

  • Let nvredfish emit the metrics directly (but that binds it to a metrics framework)
  • Let nvredfish emit a tracing::Event with the data required (request type, destination, latency). And install a tracing::Subscriber in health which transforms it into a metric. That might however lack some data that nvredfish might not have (e.g. machine_id or rack_id) and that might be harder to retrieve from the subscriber context. But it would have less code throughout health.

@poroh @yoks Let me know if there's any preference

@Matthias247 nv-redfish by itself is transport-agnostic, you can replace BMC or HTTP client implementation, including to those that emits any metrics you want. To do this you can just create HTTP client by implementing the trait:

https://github.com/NVIDIA/nv-redfish/blob/18d52581407d346a4eaa57b85786505ff6fd9311/bmc-http/src/lib.rs#L93

And use it here:

pub type RedfishBmc = HttpBmc<RedfishReqwestClient>;

Instead of using nv_redfish::bmc_http::reqwest::Client directly.

@Matthias247

Copy link
Copy Markdown
Contributor Author

@Matthias247 nv-redfish by itself is transport-agnostic, you can replace BMC or HTTP client implementation, including to those that emits any metrics you want. To do this you can just create HTTP client by implementing the trait:

https://github.com/NVIDIA/nv-redfish/blob/18d52581407d346a4eaa57b85786505ff6fd9311/bmc-http/src/lib.rs#L93

And use it here:

pub type RedfishBmc = HttpBmc<RedfishReqwestClient>;

Instead of using nv_redfish::bmc_http::reqwest::Client directly.

That sounds like a good path! I'll try it

Wraps the nvredfish HttpClient and emits metrics in it.

Signed-off-by: Matthias Einwag <meinwag@nvidia.com>
@Matthias247

Copy link
Copy Markdown
Contributor Author

@poroh I changed to that implementation in the second commit by adding InstrumntedHttpClient. The way its implemented right now (by wrapping the nvredfish client) it unfortunately does not resolve the limitation of not observing the status code directly for 2xx codes.

This would either require rebuilding most of the client, or changing nv-redfish. I'd probably lean to the latter. And either

  • directly add the capability to emit metrics to it
  • add a hook like dyn Fn(HttpExecutionTiming) that is called after each response
  • emit a tracing::Event that one can subscribe to

But I personally can also live with the inaccuracy of the 2xx codes. It's probably more the question on what is most useful for other services which want to have the same metric.

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.

4 participants