Skip to content

refactor: route pxe logs through the fleet's logfmt subscriber#3460

Merged
chet merged 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3180-pxelog
Jul 14, 2026
Merged

refactor: route pxe logs through the fleet's logfmt subscriber#3460
chet merged 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3180-pxelog

Conversation

@chet

@chet chet commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PXE was the last binary logging through raw println!/eprintln! -- its lines had no level, no component, no span_id, and bypassed every filter. It now installs the same instrumentation subscriber as the rest of the fleet, tagged with the nico-pxe component, so its output is filterable and greppable like everything else and its warning/error rate feeds the fleet-wide carbide_log_events_total signal (pxe was the one binary missing from it).

  • setup_tracing() installs the fleet-standard subscriber at startup -- the logfmt layer, the LogEventsMetric counting layer, and an EnvFilter at INFO (RUST_LOG-overridable, with the usual per-dependency caps). The log-events counter is an already-documented metric joining pxe's own /metrics, so core_metrics.md is untouched.
  • The twelve logging prints become structured tracing calls at the right level: seven error! (with the error and any URL as fields), three info!, and the request logger's hand-rolled logfmt renderer becomes a real info_span!("request", ...) -- now carrying a per-request span_id (matching api-core's request middleware) so the request line and its child events share a correlation key.
  • The one surviving println! is --version, machine-readable stdout by design.

This supports #3180

PXE was the last binary logging through raw `println!`/`eprintln!` -- its lines had no level, no component, no span_id, and bypassed every filter. It now installs the same instrumentation subscriber as the rest of the fleet, tagged with the `nico-pxe` component, so its output is filterable and greppable like everything else and its warning/error rate feeds the fleet-wide `carbide_log_events_total` signal (pxe was the one binary missing from it).

Primary callouts are:

- `setup_tracing()` installs the fleet-standard subscriber at startup (`logfmt` layer + the `LogEventsMetric` counting layer + `EnvFilter` at INFO, `RUST_LOG`-overridable, with the usual per-dependency caps) -- pxe had none before. The log-events counter is an already-documented metric joining pxe's own `/metrics`, so the catalogue is untouched.
- The twelve logging prints become structured `tracing` calls at the right level: the seven error paths (`eprintln!`) are `error!` with the error and any URL as fields; the three operational prints are `info!`; and the request logger's hand-rolled logfmt renderer is replaced by a real `info_span!("request", ...)`, which now also carries the `component`, `span_id`, and timing it emitted by hand before.
- The one surviving `println!` is `--version`, which writes the bare version string to stdout by design -- machine-readable CLI output, not a log.
- `print_and_generate_generic_error` becomes `log_and_generate_generic_error` now that it logs rather than prints.

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 13, 2026 23:39
@chet

chet commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added a tracing-based startup logging pipeline with configurable verbosity.
    • Added structured per-request logging (including key request/response details) via spans.
    • Added log event counting for improved operational monitoring.
  • Bug Fixes
    • Improved error visibility across machine connection, cloud-init processing, iPXE fallback, TLS certificate serving, and metrics scraping by switching from stderr prints to structured error logs.
    • Preserved existing HTTP responses and outcome/metric behavior while enhancing diagnostics.

Walkthrough

PXE adds centralized tracing initialization, log-event metric registration, structured request spans, and structured error logging across extractors and routes while preserving existing response and fallback behavior.

Changes

PXE observability pipeline

Layer / File(s) Summary
Tracing initialization and log metrics
crates/pxe/Cargo.toml, crates/pxe/src/main.rs
Adds tracing dependencies, configures filtered logfmt output, replaces startup and listener prints, and registers log-event counting with the OTel meter.
Request span instrumentation
crates/pxe/src/middleware/logging.rs
Replaces manual logfmt rendering with request spans that capture request and response fields around downstream handling.
Operational error logging migration
crates/pxe/src/extractors/machine.rs, crates/pxe/src/routes/cloud_init.rs, crates/pxe/src/routes/ipxe.rs, crates/pxe/src/routes/metrics.rs, crates/pxe/src/routes/tls.rs
Converts extractor and route error reporting to structured tracing while preserving response, fallback, and metric behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant PXE
  participant TracingSubscriber
  participant RequestMiddleware
  participant OTelMetrics
  PXE->>TracingSubscriber: initialize filtered logfmt subscriber
  PXE->>OTelMetrics: register log-events counter
  RequestMiddleware->>TracingSubscriber: emit request span fields
  TracingSubscriber->>OTelMetrics: count log events
Loading

Possibly related issues

  • NVIDIA/infra-controller issue 3459: Adds LogEventsMetric to PXE tracing setup and registers carbide_instrument::log_events with the meter.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: routing PXE logging through the fleet-standard logfmt subscriber.
Description check ✅ Passed The description is clearly aligned with the changeset and describes the logging refactor, tracing setup, and metrics impact.
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 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@chet Sounds good — kicking off a full review of all changes in this PR now.

🐇✨

✅ Action performed

Full review finished.

@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/pxe/src/extractors/machine.rs (1)

47-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor formatting inconsistency: ? (Debug) vs. % (Display) for error/url.

Every other new tracing::error! call added in this PR (cloud_init.rs, ipxe.rs, tls.rs, main.rs) formats its error field with %. Here error and url use ?, which — if the underlying types implement Display — will render quoted/escaped Debug output in logfmt instead of the plain value used elsewhere.

♻️ Suggested fix (if `Display` is available on both types)
                 tracing::error!(
-                    error = ?err,
-                    url = ?state.runtime_config.internal_api_url,
+                    error = %err,
+                    url = %state.runtime_config.internal_api_url,
                     "error connecting to forge api from pxe"
                 );
🤖 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/pxe/src/extractors/machine.rs` around lines 47 - 51, Update the
tracing::error! call in the machine extractor to format both the error and
internal_api_url fields with Display (`%`) instead of Debug (`?`), matching the
other error logs in the diff. Keep the existing field names and message
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.

Inline comments:
In `@crates/pxe/src/main.rs`:
- Around line 163-170: Update the EnvFilter construction near the `env_filter`
binding so directives from `RUST_LOG` take precedence over the hardcoded
`hyper`, `h2`, `tower`, `rustls`, and `tokio_util::codec` caps. Apply those
default caps only when the environment filter is absent, or merge them without
overriding same-target directives loaded by `from_env_lossy()`.

---

Nitpick comments:
In `@crates/pxe/src/extractors/machine.rs`:
- Around line 47-51: Update the tracing::error! call in the machine extractor to
format both the error and internal_api_url fields with Display (`%`) instead of
Debug (`?`), matching the other error logs in the diff. Keep the existing field
names and message unchanged.
🪄 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: ef4f70de-88b5-4ed1-b47f-08308e298796

📥 Commits

Reviewing files that changed from the base of the PR and between 69cd51c and 7b16a20.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • crates/pxe/Cargo.toml
  • crates/pxe/src/extractors/machine.rs
  • crates/pxe/src/main.rs
  • crates/pxe/src/middleware/logging.rs
  • crates/pxe/src/routes/cloud_init.rs
  • crates/pxe/src/routes/ipxe.rs
  • crates/pxe/src/routes/metrics.rs
  • crates/pxe/src/routes/tls.rs

Comment thread crates/pxe/src/main.rs
Comment on lines +163 to +170
let env_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy()
.add_directive("hyper=warn".parse()?)
.add_directive("h2=warn".parse()?)
.add_directive("tower=warn".parse()?)
.add_directive("rustls=warn".parse()?)
.add_directive("tokio_util::codec=warn".parse()?);

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File context =="
git ls-files crates/pxe/src/main.rs
cat -n crates/pxe/src/main.rs | sed -n '145,185p'

echo
echo "== Search for EnvFilter usage in repo =="
rg -n "EnvFilter::builder|from_env_lossy|try_from_default_env|add_directive\\(" crates -S

echo
echo "== tracing-subscriber version / dependency context =="
rg -n 'tracing-subscriber|tracing_subscriber' Cargo.toml crates/**/Cargo.toml -S

echo
echo "== If available, show local docs/comments mentioning override behavior =="
rg -n "overrid|directive|RUST_LOG|from_env_lossy|try_from_default_env" -S .

Repository: NVIDIA/infra-controller

Length of output: 50381


🌐 Web query:

tracing-subscriber EnvFilter add_directive ordering overrides same target from_env_lossy docs

💡 Result:

In tracing-subscriber, the EnvFilter handles directive ordering and overrides based on directive specificity rather than the order in which they are added [1][2]. When you use add_directive, the new directive is added to the set of existing directives [3][1]. If you add a directive that matches exactly the same spans and events as a previous one but specifies a different level, the new directive overwrites the previous one [1][4]. Internally, EnvFilter maintains directives in a collection ordered by their specificity (e.g., target length and number of field filters) [2]. When filtering a span or event, the system searches this set starting from the most specific directive [2]. This ensures that specific overrides (such as a directive for a particular module) take precedence over more general ones (such as a global level), regardless of the order in which they were added to the filter instance [2]. Regarding from_env_lossy and related builder methods: - The builder's from_env_lossy method (and similar methods like from_env) is designed to load directives from an environment variable [5][6]. - If the environment variable is empty, these methods fall back to a default directive (if one has been configured via with_default_directive) [5][6]. - If the environment variable contains valid directives, those directives are used, and the default directive is ignored [6]. - The lossy variants specifically ignore any invalid directives found within the environment variable string [5][6]. If you need to ensure certain directives are applied as "defaults" that can be overridden by environment variables, you should configure a default directive using the Builder and then use from_env_lossy [6][7]. If you add directives manually using add_directive, they will be treated as part of the primary directive set and will participate in the specificity-based matching and overwriting behavior described above [1][2].

Citations:


Let RUST_LOG take precedence over the hardcoded caps. The add_directive("hyper=warn")-style caps can overwrite same-target directives loaded by from_env_lossy(), so RUST_LOG=hyper=debug still gets forced back to warn. That makes the “overridable via RUST_LOG” behavior misleading; apply the caps only when the env filter is absent, or merge them in a way that preserves env overrides.

🤖 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/pxe/src/main.rs` around lines 163 - 170, Update the EnvFilter
construction near the `env_filter` binding so directives from `RUST_LOG` take
precedence over the hardcoded `hyper`, `h2`, `tower`, `rustls`, and
`tokio_util::codec` caps. Apply those default caps only when the environment
filter is absent, or merge them without overriding same-target directives loaded
by `from_env_lossy()`.

@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/pxe/src/routes/cloud_init.rs (1)

65-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose interface/domain/machine as discrete structured fields instead of pre-formatting them into the error string.

Callers currently build the error argument via format!("...: {interface:?}")-style interpolation (lines 251-259, 282-285) before it ever reaches log_and_generate_generic_error (lines 65-69). The resulting logfmt line still carries the context only as embedded Debug text inside one error= field, so it isn't independently greppable/filterable by interface=, domain=, or machine= the way the PR's structured-tracing goal intends.

Consider logging the structured context at each call site and reducing the helper to just the reason/metric:

♻️ Proposed refactor
-fn log_and_generate_generic_error(
-    error: String,
-    reason: OutcomeReason,
-) -> (String, HashMap<String, String>) {
-    tracing::error!(error = %error, "cloud-init request could not be served");
+fn log_and_generate_generic_error(reason: OutcomeReason) -> (String, HashMap<String, String>) {
     emit(PxeBootOutcome {
         endpoint: BootEndpoint::CloudInit,
         reason,
     });
     let mut template_data: HashMap<String, String> = HashMap::new();
     template_data.insert(
         "error".to_string(),
         "An error occurred while rendering the request".to_string(),
     );
     ("error".to_string(), template_data) // Send a generic error back
 }
-                    None => log_and_generate_generic_error(
-                        format!("The interface ID should not be null: {interface:?}"),
-                        OutcomeReason::InterfaceNotFound,
-                    ),
+                    None => {
+                        tracing::error!(interface = ?interface, "interface ID should not be null");
+                        log_and_generate_generic_error(OutcomeReason::InterfaceNotFound)
+                    }
-                (interface, domain) => log_and_generate_generic_error(
-                    format!("The interface and domain were not found: {interface:?}, {domain:?}"),
-                    OutcomeReason::InterfaceNotFound,
-                ),
+                (interface, domain) => {
+                    tracing::error!(interface = ?interface, domain = ?domain, "interface and domain were not found");
+                    log_and_generate_generic_error(OutcomeReason::InterfaceNotFound)
+                }
-        None => log_and_generate_generic_error(
-            format!("No metadata was found for machine {machine:?}"),
-            OutcomeReason::MetadataNotFound,
-        ),
+        None => {
+            tracing::error!(machine = ?machine, "no metadata was found for machine");
+            log_and_generate_generic_error(OutcomeReason::MetadataNotFound)
+        }

Based on coding guidelines, "common searchable values should be passed as structured tracing fields rather than interpolated into log strings."

Also applies to: 251-259, 282-285

🤖 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/pxe/src/routes/cloud_init.rs` around lines 65 - 79, Update
log_and_generate_generic_error and its callers so interface, domain, and machine
are emitted as separate structured tracing fields rather than interpolated into
the error string. Add the appropriate context fields at the call sites around
the cloud-init error handling, and keep the helper focused on logging the
generic error/reason and emitting the existing outcome and response.

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.

Nitpick comments:
In `@crates/pxe/src/routes/cloud_init.rs`:
- Around line 65-79: Update log_and_generate_generic_error and its callers so
interface, domain, and machine are emitted as separate structured tracing fields
rather than interpolated into the error string. Add the appropriate context
fields at the call sites around the cloud-init error handling, and keep the
helper focused on logging the generic error/reason and emitting the existing
outcome and response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5890d30f-286f-4302-8cff-a69895351040

📥 Commits

Reviewing files that changed from the base of the PR and between 69cd51c and 7b16a20.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • crates/pxe/Cargo.toml
  • crates/pxe/src/extractors/machine.rs
  • crates/pxe/src/main.rs
  • crates/pxe/src/middleware/logging.rs
  • crates/pxe/src/routes/cloud_init.rs
  • crates/pxe/src/routes/ipxe.rs
  • crates/pxe/src/routes/metrics.rs
  • crates/pxe/src/routes/tls.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.

@chet chet merged commit f983414 into NVIDIA:main Jul 14, 2026
59 checks passed
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