Skip to content

feat: add alternate-prefix mirroring and external cancellation to the metrics endpoint#3474

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

feat: add alternate-prefix mirroring and external cancellation to the metrics endpoint#3474
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3180-metricsendpoint

Conversation

@chet

@chet chet commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

The shared metrics-endpoint crate serves a caller-supplied registry, but two capabilities still lived only in per-binary copies of the server: mirroring a metric family under a second name prefix, and shutting the listener down on an external cancellation token. Folding them into the shared crate lets every binary serve /metrics through one implementation.

  • MetricsEndpointConfig gains additional_prefix: Option<(String, String)> -- when set, the exposition re-emits each family whose name starts with the first prefix under the second, so both series appear. None (every current caller) is byte-for-byte the previous output.
  • run_metrics_endpoint_with_cancellation serves until a caller-owned CancellationToken fires; run_metrics_endpoint delegates to it with a fresh, never-cancelled token, so it's unchanged.
  • The five existing callers (bmc-proxy, dhcp-server, dhcp, dns, dsx-exchange-consumer) pass additional_prefix: None, keeping their /metrics identical.

Tests cover all three: None matches a raw text-encode of the registry, Some puts each matching family under both prefixes and leaves non-matching ones alone, and the cancellation entry point returns once its token fires.

This supports #3180

… metrics endpoint

The shared `metrics-endpoint` crate serves a caller-supplied `prometheus::Registry`, but two capabilities still lived only in per-binary copies of the server: mirroring a metric family under a second name prefix, and shutting the listener down on an external cancellation token. Folding them into the shared crate lets every binary serve `/metrics` through one implementation.

- `MetricsEndpointConfig` gains an `additional_prefix: Option<(String, String)>`. When set, the exposition re-emits each family whose name starts with the first prefix under the second, so a scrape sees both the original and the aliased series. `None` -- every current caller -- is byte-for-byte the previous output.
- `run_metrics_endpoint_with_cancellation` binds the address and serves until a caller-owned `CancellationToken` fires; `run_metrics_endpoint` now delegates to it with a fresh, never-cancelled token, so its behavior is unchanged.
- The five existing callers (bmc-proxy, dhcp-server, dhcp, dns, dsx-exchange-consumer) pass `additional_prefix: None`, keeping their `/metrics` identical.

Tests cover all three paths: `None` matches a raw text-encode of the registry, `Some` puts each matching family under both prefixes and leaves non-matching families alone, and the cancellation entry point returns once its token fires.

This supports NVIDIA#3180

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet chet requested a review from a team as a code owner July 14, 2026 05:34
@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 the PR.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added support for optionally exposing Prometheus metrics under an additional (renamed) prefix, duplicating matching metric families during /metrics responses.
  • Bug Fixes
    • Ensures /metrics output stays byte-identical to the standard encoding when the additional prefix is not configured.
  • Tests
    • Added/extended assertions to verify unchanged output for the default case and correct duplication/append behavior when additional prefixing is enabled.

Walkthrough

The metrics endpoint gains optional alternate-prefix exposition for matching Prometheus metric families. Existing endpoint configurations explicitly disable this behavior with None, while encoding and tests cover unchanged output and duplicated renamed families.

Changes

Metrics endpoint prefix exposition

Layer / File(s) Summary
Endpoint configuration and service wiring
crates/metrics-endpoint/src/lib.rs, crates/bmc-proxy/src/metrics.rs, crates/dhcp-server/src/main.rs, crates/dhcp/src/metrics.rs, crates/dns/src/lib.rs, crates/dsx-exchange-consumer/src/lib.rs
MetricsEndpointConfig adds an optional prefix mapping, propagates it into handler state, and existing services initialize it as None.
Metric family encoding and validation
crates/metrics-endpoint/src/lib.rs
The /metrics handler optionally duplicates and renames matching metric families, while tests verify unchanged output when disabled and correct alternate-prefix output when configured.

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

Sequence Diagram(s)

sequenceDiagram
  participant MetricsHandler
  participant PrometheusRegistry
  participant TextEncoder
  MetricsHandler->>PrometheusRegistry: Gather metric families
  MetricsHandler->>MetricsHandler: Duplicate and rename matching families
  MetricsHandler->>TextEncoder: Encode combined families
  TextEncoder-->>MetricsHandler: Return metrics response
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main metrics-endpoint changes: alternate-prefix mirroring and external cancellation.
Description check ✅ Passed The description is directly related to the changeset and matches the shared metrics-endpoint updates and caller adjustments.
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: 1

🧹 Nitpick comments (1)
crates/metrics-endpoint/src/lib.rs (1)

90-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a named struct instead of an anonymous (String, String) tuple.

additional_prefix: Option<(String, String)> requires every caller to remember which element is "old" and which is "new" purely by positional convention; a transposed tuple compiles fine and silently corrupts exposition. A small struct PrefixMigration { old: String, new: String } (or similarly named) removes the ambiguity and is self-documenting at call sites.

As per coding guidelines, "Prefer struct literals for plain data; use new() for private or internally initialized fields, Default for all-default common cases, and builders only for genuinely large or difficult constructors." This field is brand-new in this PR (no existing callers set a real value), so the API shape is cheap to adjust now.

♻️ Suggested struct-based alternative
+/// Identifies an old/new prefix pair used to duplicate matching metric
+/// families under an alternate name during a rename migration.
+pub struct PrefixMigration {
+    pub old: String,
+    pub new: String,
+}

 pub struct MetricsEndpointConfig {
     pub address: SocketAddr,
     pub registry: prometheus::Registry,
     pub health_controller: Option<HealthController>,
-    pub additional_prefix: Option<(String, String)>,
+    pub additional_prefix: Option<PrefixMigration>,
 }
🤖 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/metrics-endpoint/src/lib.rs` around lines 90 - 104, Replace the
anonymous tuple type used by MetricsEndpointConfig.additional_prefix with a
named public PrefixMigration struct containing clearly named old and new String
fields, and update the exposition logic and all construction/destructuring sites
to use those fields instead of positional tuple elements.

Source: Coding guidelines

🤖 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/metrics-endpoint/src/lib.rs`:
- Around line 238-273: Update encode_metrics to detect when a prefixed family’s
replacement name already exists among the gathered metric families or generated
aliases, and skip or reject that alias before encoder.encode. Preserve the
original families and ensure the output never contains duplicate metric-family
names or conflicting HELP/TYPE blocks.

---

Nitpick comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 90-104: Replace the anonymous tuple type used by
MetricsEndpointConfig.additional_prefix with a named public PrefixMigration
struct containing clearly named old and new String fields, and update the
exposition logic and all construction/destructuring sites to use those fields
instead of positional tuple elements.
🪄 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: df89772e-e580-4800-8273-c4603f7bef89

📥 Commits

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

📒 Files selected for processing (6)
  • crates/bmc-proxy/src/metrics.rs
  • crates/dhcp-server/src/main.rs
  • crates/dhcp/src/metrics.rs
  • crates/dns/src/lib.rs
  • crates/dsx-exchange-consumer/src/lib.rs
  • crates/metrics-endpoint/src/lib.rs

Comment on lines +238 to +273
/// Encode the registry's metric families in the Prometheus text exposition format.
///
/// When `additional_prefix` is `Some((old, new))`, every gathered family whose name
/// starts with `old` is additionally emitted under a copy whose name has that prefix
/// replaced by `new`, so the same series appear under both names. When `None`, the
/// output is exactly `TextEncoder` over `registry.gather()`.
fn encode_metrics(
registry: &prometheus::Registry,
additional_prefix: Option<&(String, String)>,
) -> Vec<u8> {
let mut buffer = vec![];
let encoder = TextEncoder::new();
let mut metric_families = registry.gather();

if let Some((old_prefix, new_prefix)) = additional_prefix {
let alt_name_families: Vec<MetricFamily> = metric_families
.iter()
.filter_map(|family| {
if !family.name().starts_with(old_prefix) {
return None;
}

let mut alt_name_family = family.clone();
alt_name_family.set_name(family.name().replacen(old_prefix, new_prefix, 1));
Some(alt_name_family)
})
.collect();

if !alt_name_families.is_empty() {
metric_families.extend(alt_name_families);
}
}

encoder.encode(&metric_families, &mut buffer).unwrap();
buffer
}

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:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the referenced lines
sed -n '200,320p' crates/metrics-endpoint/src/lib.rs | cat -n

# Find the configuration and all call sites for additional_prefix / encode_metrics
rg -n "additional_prefix|encode_metrics|MetricsEndpointConfig" crates/metrics-endpoint -S

# Show the surrounding definitions if they exist in the same file
rg -n "struct MetricsEndpointConfig|enum .*MetricsEndpoint|additional_prefix" crates/metrics-endpoint/src/lib.rs -n -A 8 -B 8

Repository: NVIDIA/infra-controller

Length of output: 13800


Guard renamed metric-family collisions. A (old_prefix, new_prefix) pair can rename a family onto an existing name, producing duplicate HELP/TYPE blocks and invalid Prometheus text. Reject or skip collisions before encoding.

🤖 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/metrics-endpoint/src/lib.rs` around lines 238 - 273, Update
encode_metrics to detect when a prefixed family’s replacement name already
exists among the gathered metric families or generated aliases, and skip or
reject that alias before encoder.encode. Preserve the original families and
ensure the output never contains duplicate metric-family names or conflicting
HELP/TYPE blocks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/metrics-endpoint/src/lib.rs (1)

244-273: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Missing guard against renamed-family collisions (unresolved from prior review).

encode_metrics renames and appends matching families without checking whether the resulting name already exists among the original families or other renamed families. If new_prefix maps a family onto an existing name (e.g. old_prefix = "carbide_", new_prefix = "" colliding with an existing unprefixed metric, or old_prefix == new_prefix), metric_families will contain two entries with the same name, producing duplicate HELP/TYPE blocks in the Prometheus text output — invalid exposition that scrapers may reject.

This exact concern was raised on a previous revision of this function and remains unaddressed.

🐛 Suggested fix
     if let Some((old_prefix, new_prefix)) = additional_prefix {
+        let existing_names: std::collections::HashSet<&str> =
+            metric_families.iter().map(|f| f.name()).collect();
         let alt_name_families: Vec<MetricFamily> = metric_families
             .iter()
             .filter_map(|family| {
                 if !family.name().starts_with(old_prefix) {
                     return None;
                 }
 
-                let mut alt_name_family = family.clone();
-                alt_name_family.set_name(family.name().replacen(old_prefix, new_prefix, 1));
-                Some(alt_name_family)
+                let alt_name = family.name().replacen(old_prefix, new_prefix, 1);
+                if existing_names.contains(alt_name.as_str()) {
+                    tracing::warn!(alt_name, "skipping alternate-prefix metric due to name collision");
+                    return None;
+                }
+                let mut alt_name_family = family.clone();
+                alt_name_family.set_name(alt_name);
+                Some(alt_name_family)
             })
             .collect();

Add a regression test exercising a collision case (e.g. old_prefix/new_prefix that produces a name already present in the registry) alongside the existing duplication test.

🤖 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/metrics-endpoint/src/lib.rs` around lines 244 - 273, Update
encode_metrics to track metric-family names and only append a renamed family
when its resulting name does not already exist among the original or previously
generated families, including when old_prefix equals new_prefix. Add a
regression test alongside the existing duplication test that registers a
colliding metric name and verifies the encoded output contains no duplicate
family blocks.
🧹 Nitpick comments (2)
crates/metrics-endpoint/src/lib.rs (2)

418-535: 📐 Maintainability & Code Quality | 🔵 Trivial

LGTM!

Coverage nicely locks in the byte-identical None path and the duplication semantics. As per coding guidelines, table-driven tests via carbide-test-support's scenarios!/value_scenarios! are preferred for input→output mapping functions like encode_metrics; given the small, distinctly-shaped assertions here, this is optional rather than a blocker.
[optional_low_priority]

🤖 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/metrics-endpoint/src/lib.rs` around lines 418 - 535, The tests for
encode_metrics use separate cases instead of the preferred table-driven
carbide-test-support scenarios. Optionally consolidate the None and
additional_prefix input/output coverage around encode_metrics using scenarios!
or value_scenarios!, preserving the existing byte-identical and duplication
assertions.

Source: Coding guidelines


94-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a named struct instead of (String, String) for the prefix pair.

A raw tuple makes it easy to accidentally swap old/new at call sites, and the doc comment has to spell out "first tuple element" / "second element" to compensate. As per coding guidelines, Rust designs should be hard to misuse and let the compiler catch mistakes.

♻️ Suggested refactor
+pub struct MetricPrefixRename {
+    pub old: String,
+    pub new: String,
+}
+
 pub struct MetricsEndpointConfig {
     pub address: SocketAddr,
     pub registry: prometheus::Registry,
     pub health_controller: Option<HealthController>,
-    pub additional_prefix: Option<(String, String)>,
+    pub additional_prefix: Option<MetricPrefixRename>,
 }

Based on learnings, this follows the "**/*.rs" guideline: "Prefer designs that are hard to misuse and allow the compiler to catch bugs."

🤖 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/metrics-endpoint/src/lib.rs` around lines 94 - 104, Replace
MetricsEndpointConfig.additional_prefix’s raw (String, String) tuple with a
named struct whose fields explicitly distinguish the old and new prefixes, and
update the associated documentation to reference those field names rather than
tuple positions. Propagate the named type through all construction and
consumption sites while preserving the existing optional behavior.

Source: Coding guidelines

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

Duplicate comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 244-273: Update encode_metrics to track metric-family names and
only append a renamed family when its resulting name does not already exist
among the original or previously generated families, including when old_prefix
equals new_prefix. Add a regression test alongside the existing duplication test
that registers a colliding metric name and verifies the encoded output contains
no duplicate family blocks.

---

Nitpick comments:
In `@crates/metrics-endpoint/src/lib.rs`:
- Around line 418-535: The tests for encode_metrics use separate cases instead
of the preferred table-driven carbide-test-support scenarios. Optionally
consolidate the None and additional_prefix input/output coverage around
encode_metrics using scenarios! or value_scenarios!, preserving the existing
byte-identical and duplication assertions.
- Around line 94-104: Replace MetricsEndpointConfig.additional_prefix’s raw
(String, String) tuple with a named struct whose fields explicitly distinguish
the old and new prefixes, and update the associated documentation to reference
those field names rather than tuple positions. Propagate the named type through
all construction and consumption sites while preserving the existing optional
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bdbe602e-ce2c-4d0b-87ac-4176a41555cb

📥 Commits

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

📒 Files selected for processing (6)
  • crates/bmc-proxy/src/metrics.rs
  • crates/dhcp-server/src/main.rs
  • crates/dhcp/src/metrics.rs
  • crates/dns/src/lib.rs
  • crates/dsx-exchange-consumer/src/lib.rs
  • crates/metrics-endpoint/src/lib.rs

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

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.

1 participant