Skip to content

telemetry: additional metrics (error, consistent start/success) - #778

Draft
bfjelds (bfjelds) wants to merge 18 commits into
user/bfjelds/mjolnir/appinsights-telemetryfrom
user/bfjelds/mjolnir/command-error-metric
Draft

telemetry: additional metrics (error, consistent start/success)#778
bfjelds (bfjelds) wants to merge 18 commits into
user/bfjelds/mjolnir/appinsights-telemetryfrom
user/bfjelds/mjolnir/command-error-metric

Conversation

@bfjelds

@bfjelds bfjelds (bfjelds) commented Sep 4, 2026

Copy link
Copy Markdown
Member

Implement (manual_rollback_start, runtime_update_success, manual_rollback_runtime_success) to be consistent with other operation metrics.

Adds a new command_error metric that fires whenever a command fails, for both CLI and gRPC/daemon, included in metric:

  • kind: top-level error category (e.g. internal, invalid-input, servicing)
  • subkind: the specific error within that category, when one applies
  • location: file:line where the error was originally raised

Related PRs in stack:

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@bfjelds bfjelds (bfjelds) changed the title telemetry: add command_error metric (kind/subkind/location); move manual_rollback/runtime_update metrics here telemetry: additional metrics (error, consistent start/success) Sep 4, 2026
@bfjelds
bfjelds (bfjelds) requested a balanced review from Copilot September 5, 2026 01:38

Copilot AI 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.

🟡 Changes recommended

CLI failure telemetry remains incomplete, and ACL detection failures are incorrectly reported as negative results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds expanded telemetry for command failures, servicing lifecycle events, and startup context.

Changes:

  • Adds correlated command_error metrics for CLI and gRPC operations.
  • Adds manual rollback and runtime-update lifecycle metrics.
  • Enriches startup telemetry with ACL detection and system uptime.
File summaries
File Description
Cargo.toml Enables Nix time APIs.
docs/Reference/Agent-Configuration.md Documents command-error telemetry.
crates/trident/src/main.rs Instruments CLI servicing commands.
crates/trident/src/lib.rs Re-exports instrumentation and enriches startup metrics.
crates/trident/src/logging/operation_context.rs Implements and tests command-error reporting.
crates/trident/src/server/tridentserver/mod.rs Instruments gRPC servicing commands.
crates/trident/src/engine/runtime_update.rs Emits runtime-update success.
crates/trident/src/engine/manual_rollback/mod.rs Emits rollback start and runtime success.
crates/trident/src/engine/manual_rollback/utils.rs Makes rollback request kinds loggable.
crates/trident_api/src/error.rs Exposes error-origin locations.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/trident/src/lib.rs Outdated
Comment thread crates/trident/src/main.rs Outdated

Copilot AI 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.

🟡 Changes recommended

CLI startup events lose correlation metadata, and runtime-update success is omitted from persisted metrics archives.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread crates/trident/src/engine/runtime_update.rs
Comment thread crates/trident/src/main.rs Outdated

Copilot AI 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.

🟡 Changes recommended

Correlation-ID pre-warming creates missing datastores and bypasses the host-provisioning guard for several CLI commands.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/trident/src/main.rs Outdated
@bfjelds
bfjelds (bfjelds) force-pushed the user/bfjelds/mjolnir/command-error-metric branch 2 times, most recently from f6f0da6 to 5e7b215 Compare September 5, 2026 19:51
@bfjelds
bfjelds (bfjelds) requested a balanced review from Copilot September 5, 2026 20:07

Copilot AI 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.

🟡 Changes recommended

Initial install/update telemetry can remain uncorrelated, and runtime rollback persistence is duplicated.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

crates/trident/src/main.rs:202

  • Issue: The missing-datastore check also skips pre-warming for install and update, so the first such CLI invocation still emits command_start without a correlation_id. Evidence: Those commands are intentionally exempt from the provisioning guard below, and Trident::new creates the ID only after run_command has already emitted command_start. Suggestion: Skip creation only for commands that require an existing datastore; allow install/update to create and attach the ID before entering run_command.
                    if !agent_config.datastore_path().exists() {
                        return Ok(None);
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread crates/trident/src/engine/manual_rollback/mod.rs Outdated
Comment thread crates/trident/src/server/tridentserver/mod.rs

Copilot AI 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.

🟡 Changes recommended

Several non-servicing CLI failures still bypass the promised command_error telemetry.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/trident/src/main.rs Outdated

Copilot AI 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.

🟡 Changes recommended

Read-only and response-encoded gRPC failures remain outside the new command-error instrumentation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/trident/src/server/tridentserver/mod.rs
@bfjelds
bfjelds (bfjelds) force-pushed the user/bfjelds/mjolnir/command-error-metric branch from 79b5d01 to ee0bf7c Compare September 5, 2026 21:29
@bfjelds
bfjelds (bfjelds) requested a balanced review from Copilot September 5, 2026 21:35

Copilot AI 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.

🟡 Changes recommended

Several newly wrapped CLI and read-RPC paths still omit telemetry setup or persisted correlation metadata.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

crates/trident/src/server/tridentserver/mod.rs:381

  • Issue: Read RPCs can still lose the persisted correlation ID that the new command events are supposed to carry. Evidence: if the daemon starts before the datastore exists and another process later creates it, only servicing_request re-checks the datastore (lines 209-245); reading_request reaches run_command directly here, so its command_start/command_error remain uncorrelated until a write RPC happens. Suggestion: factor the pre-warm/re-check into a shared helper and call it before both servicing and reading command wrappers.
        let f = move || operation_context::run_command(name, f);
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/trident/src/main.rs

Copilot AI 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.

🟡 Changes recommended

Diagnostic metrics are truncated, and malformed gRPC requests still bypass command-error telemetry.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

crates/trident/src/server/tridentserver/mod.rs:264

  • Issue: This wrapper still misses concrete gRPC command failures, so the advertised daemon coverage is incomplete. Evidence: multiple handlers reject malformed requests before calling servicing_request—for example install/update return Status::invalid_argument for missing stage/config/finalize fields, rollback does the same for missing stage/finalize, and streaming rejects an invalid URL. Those requests emit neither command_start nor command_error. Suggestion: establish command telemetry around the full RPC handler (for example via middleware), or move request validation into a common wrapped path and map these validation failures to the documented error dimensions.
        let f = move || operation_context::run_command(name, f);
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread crates/trident/src/main.rs
…rollback_runtime_success metrics

Moved here from PR 774 (which keeps just the correlation_id/operation_id
infrastructure) so all new-metric additions -- including the new
command_error metric added in this PR -- live together.

- manual_rollback_start (engine/manual_rollback/mod.rs::execute_rollback):
  fired unconditionally on every invocation (stage-only/finalize-only/
  combined), mirroring update_start placement/semantics. Required adding
  #[derive(Debug, Clone, Copy)] to ManualRollbackRequestKind (had no
  derives at all).
- runtime_update_success (engine/runtime_update.rs::finalize_update):
  fired synchronously on success, since runtime update needs no reboot
  and so never goes through engine::rollback post-reboot boot-validation
  flow where clean_install_success/ab_update_success/
  manual_rollback_success are fired.
- manual_rollback_runtime_success (engine/manual_rollback/mod.rs::
  finalize_rollback): closes a parallel gap -- manual rollback of a
  *runtime* update also never reached the boot-validation flow, so it
  had no success metric at all (only the A/B rollback case did, via
  manual_rollback_success).

cargo build -p trident, cargo test -p trident --lib: 417/417 pass
(unchanged from before the move).
file:line where the error was originally constructed was already tracked
internally (used by Serialize, Debug, and the ProtoTridentError
conversion) but had no public getter. Needed by the upcoming
command_error telemetry metric, which reports kind/subkind/location for
every command that fails.
…subkind/location

Works for both CLI and gRPC/daemon, since both already funnel through
operation_context (from PR 774):

- New run_command<T>() in operation_context.rs: same as run_with_operation,
  but specifically for the Result<T, TridentError> shape both funnel
  points actually use. Fires a command_error metric (kind/subkind/
  location) if the wrapped closure returns Err, while operation_id/command
  context is still active -- so it's correlated the same way command_start
  already is.
- main.rs's CLI dispatch (5 call sites: install/update/commit/rollback/
  rebuild_raid) and server/tridentserver/mod.rs's servicing_request()
  (covers every gRPC operation from one call site) both switched from
  run_with_operation to run_command.
- trident_api::error::TridentError::location(): new public accessor for
  the file:line the error was originally constructed at (already tracked
  internally for Serialize/Debug/ProtoTridentError, just had no getter).
- docs/Reference/Agent-Configuration.md: document the new command_error
  event and its kind/subkind/location fields.

Verified: cargo build --workspace, clippy --all-targets --features
functional-test, fmt --check all clean. cargo test -p trident --lib:
421/421 pass (8 new in operation_context, including a test using a
capturing tracing_subscriber::Layer to assert command_error actually
carries the right kind/subkind/location for a constructed TridentError).
Tags every trident_start event with whether the host is running the CIH
(Azure Container Linux) image, using the existing cih::is_cih() check
already used elsewhere in Trident::update(). Best-effort: a failure to
determine this reports false rather than failing startup.

Not documented in Agent-Configuration.md: that section only covers
fields attached to every event (host metadata) and the dedicated
command_error case -- acl is a one-off field on a single event, and
carving out a doc entry for every future one-off metric field would
bloat it.

cargo build -p trident, clippy --all-targets --features functional-test,
fmt --check all clean. cargo test -p trident --lib: 422/422 pass.
…nfo::System::uptime()

sysinfo::System::uptime() truncates /proc/uptime to whole seconds
(splits on the decimal point and discards the fractional part entirely,
confirmed in its vendored source -- not even a rounding, a hard
truncation). Switched to nix::time::clock_gettime(ClockId::CLOCK_BOOTTIME),
which reads the kernel clock directly at nanosecond resolution and
correctly includes any suspended time (unlike CLOCK_MONOTONIC on some
kernels). Converted via the crates already-available
impl From<TimeSpec> for Duration, then .as_secs_f64() for a fractional-
seconds value.

Enabled nix's time feature (previously default-features = false with
only fs/user/socket/signal enabled) in the workspace Cargo.toml.

Best-effort: clock_gettime with a valid clock ID essentially never fails
on Linux, but falls back to 0.0 with a warning rather than failing
startup if it somehow does.

cargo build --workspace, clippy --all-targets --features functional-test,
fmt --check all clean. cargo test -p trident --lib: 422/422 pass.
0.0 is indistinguishable from a real (if implausible) zero-second
uptime. f64::NAN is a genuine \ not available\ sentinel here: verified
against the vendored serde_json source that both Value::from(f64) (used
by every json!(value) call in tracestream.rs/appinsights.rs) and the
actual JSON serializer's serialize_f64 treat non-finite floats as
Value::Null / JSON null respectively -- no error, no special-casing
needed anywhere in this codebase's telemetry pipeline.

cargo build -p trident, clippy --all-targets --features functional-test,
fmt --check all clean. cargo test -p trident --lib: 422/422 pass.
Addresses Copilot review comment on PR 778: is_cih() failures were
reported as acl=false, misclassifying a host we simply could not check
as definitively non-ACL. Now reports the string "unknown" instead of
false on error (true/false as strings otherwise), so a detection failure
is distinguishable from a real negative result.

cargo build -p trident, clippy --all-targets --features functional-test,
fmt --check all clean. cargo test -p trident --lib: 427/427 pass.
…nnermost call

Addresses Copilot review comment on PR 778: run_command previously only
wrapped the innermost install/update/commit/rollback/rebuild-raid call,
so command_error never fired for failures in the preflight checks ahead
of it (missing config file, unprovisioned host, AgentConfig::load,
Trident::new, DataStore::open_or_create) -- those all use `?` and return
before run_command is ever entered.

Restructured so the command name is determined right at the top of the
shared match arm (before any preflight checks), and a single run_command
call now wraps everything from the config-path check through the final
res.message(...) at the bottom -- preflight failures now get a
command_error the same way the actual command's own failures already
did.

Known remaining gap (documented in a code comment): a genuine Rust panic
inside this closure still unwinds through run_command's ClearOnDrop guard
(which runs during unwinding, before panic::catch_unwind's Result is even
produced), clearing the operation context before the panic is converted
to a final TridentError further below in run_trident -- so a panic still
does not get a command_error metric. Believed rare enough, and already
surfaced via normal error logging elsewhere, to leave as a follow-up
rather than further restructuring panic handling here.

cargo build --workspace, clippy --all-targets --features functional-test,
fmt --check all clean. cargo test -p trident --lib: 427/427 pass
(unchanged -- no existing test exercises this preflight-failure path
directly).
…ess metric, not before

Addresses Copilot review comment on PR 778: runtime_update_success (and
manual_rollback_runtime_success) fired *after*
finalize_or_rollback_runtime_update had already copied the metrics file
to the target OS -- so the archived copy never included that success
event, unlike every other *_success metric (which fire before
persist_background_log_and_metrics runs, via the shared post-reboot
boot-validation flow in engine::rollback).

Moved persistence out of the shared finalize_or_rollback_runtime_update
helper (which no longer persists internally) into each of its three real
callers, each now persisting right after its own final outcome metric:
- runtime_update::finalize_update's success path (after runtime_update_success)
- runtime_update::finalize_update's auto-rollback-on-failure path (after
  the auto-rollback itself succeeds -- no dedicated metric of its own,
  but persistence should still happen)
- manual_rollback::finalize_rollback's runtime-rollback path (after
  manual_rollback_runtime_success)

cargo build --workspace, clippy --all-targets --features functional-test,
fmt --check all clean. cargo test -p trident --lib: 427/427 pass.
…llback

finalize_rollback's runtime branch persisted the archive itself right
after firing manual_rollback_runtime_success, but execute_rollback also
unconditionally persists again immediately after finalize_rollback
returns -- archiving the same (or, with second-resolution filenames, a
second) copy. The success metric already fires before either call, so
drop the inner persist and rely on the caller's.
…quest

The daemon-startup pre-warm in server_main only runs once, and skips
itself when the datastore does not exist yet. A daemon that starts
before the host is installed, then later serves a request after some
other path (e.g. a concurrent CLI invocation) has since created the
datastore, would otherwise keep firing command_start/metrics with no
correlation_id for its whole remaining lifetime. Re-check (read-only,
does not create the datastore) before firing each request's own
command_start, skipping if the datastore still does not exist -- e.g.
the very first install RPC still cannot retroactively attach an ID
that does not exist until that install creates it, same as the CLI.
…st servicing

validate, offline-initialize, get, diagnose, rollback --check, and
start-network all returned directly from run_trident before reaching
run_command, so their Err results fired neither command_start nor
command_error, despite the PR description and documentation saying the
event fires whenever a command fails. Move command name determination
and the run_command wrapper to cover the entire dispatch (both the fast
"exit early" commands and the servicing branch), using Commands::name()
(normalized to underscores) for the former and the existing
command_name() stage/finalize-aware naming for the latter.
reading_request (get_servicing_state, check_rollback, and other
grpc-preview read RPCs) returns TridentError on failure just like
servicing_request does, but bypassed run_command entirely, so it fired
neither command_start nor command_error. Wrap f the same way
servicing_request does, tagging it with command/operation_id on its
dedicated spawn_blocking thread.
…ng ones

setup_tracing only installed a subscriber for commit/daemon/grpc-client/
install/rebuild-raid/update/rollback(finalize), leaving validate, get,
diagnose, offline-initialize, start-network, and rollback --check with
no subscriber at all -- so the command_start/command_error events they
now fire (since the previous fix wrapped them in run_command) were
silently dropped by tracing's no-op default dispatcher. Cover every
command reachable from run_trident explicitly; StartNetwork's own
tracestream.disable() still applies as before, since it only suppresses
a later set_server call, not the local metrics-file/journald layers
installed here. Listed pytest-generator's Pytest variant explicitly
(a real no-op, it does no meaningful work) instead of falling back to a
wildcard arm, so the compiler forces this match to be revisited if a new
command variant is ever added.
The correlation ID re-check only ran in servicing_request, so read-only
RPCs (get_servicing_state, check_rollback, etc.) could keep reporting a
stale/missing ID indefinitely if they never happened to run after a
write request warmed it -- e.g. the daemon starts before the datastore
exists, a separate CLI invocation installs the host, and the first RPC
afterwards is a read. Factored the re-check into a shared
refresh_correlation_id helper, called from both servicing_request and
reading_request.
Including diagnose in setup_tracing's subscriber-installing commands
meant TraceSender::new truncated /var/log/trident-metrics.jsonl on every
diagnose invocation (via files::create_file), destroying the history
diagnostics::generate_and_bundle is supposed to package into the support
bundle -- the bundle ended up containing only metrics diagnose emitted
about itself. Added a truncate parameter to TraceSender/
make_trace_sender_with_metrics_path, and a make_trace_sender_appending
that opens the file in append mode instead; diagnose now uses that
specifically, while every other command keeps truncating as before.

Copilot AI 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.

🟡 Changes recommended

Invalid configurations can create datastores prematurely, while validation RPC failures still bypass command-error telemetry.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

crates/trident/src/server/tridentserver/mod.rs:230

  • Issue: The same pre-warm side effect affects first install/update RPCs with invalid configuration content. Evidence: refresh_correlation_id creates the datastore before the request closure runs, but the service handlers parse their raw host configuration only inside Trident::new; a parse failure therefore leaves behind a datastore that would not otherwise have been created. Suggestion: validate the request configuration before permitting this helper to create a missing datastore, or defer persistence of the new correlation ID until validation succeeds.
        let can_initialize_datastore = name.starts_with("install") || name.starts_with("update");
        match AgentConfig::load().and_then(|agent_config| {
            if !can_initialize_datastore && !agent_config.datastore_path().exists() {
                return Ok(None);
            }
            DataStore::open_or_create(agent_config.datastore_path())
                .and_then(|mut ds| ds.correlation_id())
                .map(Some)

crates/trident/src/server/tridentserver/mod.rs:397

  • Issue: The stated Ok-encoded failure gap already exists, so gRPC failures are still not fully covered. Evidence: validate_host_configuration in services/validation.rs:32-38 converts a TridentError into Ok(ValidateHostConfigurationResponse { ok: false, error }) and bypasses reading_request, meaning it emits neither command_start nor command_error. Suggestion: run that RPC under an operation context and report its response-level validation error, or refactor the common wrapper to support this response shape.
        // Known remaining gap: an RPC that encodes a domain-level failure
        // in an `Ok` response instead of returning `Err(TridentError)` (if
        // any -- none currently do) would still not get a command_error,
        // since that only fires on `Err`.

crates/trident/src/server/tridentserver/mod.rs:230

  • Issue: Reloading AgentConfig here can attach telemetry to a different datastore than the request actually uses. Evidence: every service closure uses the self.agent_config captured when TridentServer was constructed, but this helper rereads the on-disk configuration; if that file changes during the daemon lifetime, command_start and subsequent metrics can receive the correlation ID from the new path while servicing still targets the old path. Suggestion: use self.agent_config.datastore_path() consistently with the request handlers.
        match AgentConfig::load().and_then(|agent_config| {
            if !can_initialize_datastore && !agent_config.datastore_path().exists() {
                return Ok(None);
            }
            DataStore::open_or_create(agent_config.datastore_path())
                .and_then(|mut ds| ds.correlation_id())
                .map(Some)
  • Files reviewed: 11/11 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread crates/trident/src/main.rs
Comment thread crates/trident/src/logging/tracestream.rs Outdated
Accidental run of spaces left over from an earlier edit made this
diagnostic message unreadable.
Existence alone was not enough for Install/Update: the correlation-ID
pre-warm creates the datastore as a side effect for those two, so a
--config path that exists but is unreadable, a directory, or not valid
Host Configuration YAML still let the datastore get created before
Trident::new later rejected it, letting a later command wrongly pass
the "host not provisioned" existence check. Added a parse-only (no
semantic validate()) preflight, exposed via a new public
validation::parse_host_config_file, matching exactly what Trident::new
does with the file moments later.

Copilot AI 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.

🟡 Changes recommended

Telemetry initialization can erase prior metrics, omit existing correlation IDs, and create datastores for invalid requests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

crates/trident/src/main.rs:135

  • Issue: Invalid install/update configurations return before the persisted correlation ID is loaded. Evidence: these early run_command calls fire command_start/command_error before the pre-warm at lines 165–175; even when the datastore already exists, both events therefore omit the installation's correlation_id, contrary to the documented per-event metadata. Suggestion: load from an existing datastore before this preflight, while still deferring datastore creation until parsing succeeds when the datastore is absent.
    if let Some(path) = &config_path {
        if !path.exists() {
            return run_command(&command, || {
  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +557 to +561
let local_sender = if matches!(args.command, Commands::Diagnose { .. }) {
tracestream.make_trace_sender_appending()
} else {
tracestream.make_trace_sender()
};
// Try to acquire the connection lock in write mode
let guard = self.try_acquire_write_lock()?;

self.refresh_correlation_id(name);
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