telemetry: additional metrics (error, consistent start/success) - #778
Conversation
|
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. |
There was a problem hiding this comment.
🟡 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_errormetrics 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.
a97f856 to
a2e921d
Compare
There was a problem hiding this comment.
🟡 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
There was a problem hiding this comment.
🟡 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
f6f0da6 to
5e7b215
Compare
There was a problem hiding this comment.
🟡 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
installandupdate, so the first such CLI invocation still emitscommand_startwithout acorrelation_id. Evidence: Those commands are intentionally exempt from the provisioning guard below, andTrident::newcreates the ID only afterrun_commandhas already emittedcommand_start. Suggestion: Skip creation only for commands that require an existing datastore; allow install/update to create and attach the ID before enteringrun_command.
if !agent_config.datastore_path().exists() {
return Ok(None);
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Balanced
5e7b215 to
e6ac4c4
Compare
There was a problem hiding this comment.
🟡 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
e6ac4c4 to
79b5d01
Compare
There was a problem hiding this comment.
🟡 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
79b5d01 to
ee0bf7c
Compare
There was a problem hiding this comment.
🟡 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_requestre-checks the datastore (lines 209-245);reading_requestreachesrun_commanddirectly here, so itscommand_start/command_errorremain 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
fe3bf3f to
fda62f2
Compare
There was a problem hiding this comment.
🟡 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 returnStatus::invalid_argumentfor missing stage/config/finalize fields, rollback does the same for missing stage/finalize, and streaming rejects an invalid URL. Those requests emit neithercommand_startnorcommand_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
…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.
fda62f2 to
caaec51
Compare
There was a problem hiding this comment.
🟡 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_idcreates the datastore before the request closure runs, but the service handlers parse their raw host configuration only insideTrident::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_configurationinservices/validation.rs:32-38converts aTridentErrorintoOk(ValidateHostConfigurationResponse { ok: false, error })and bypassesreading_request, meaning it emits neithercommand_startnorcommand_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
AgentConfighere can attach telemetry to a different datastore than the request actually uses. Evidence: every service closure uses theself.agent_configcaptured whenTridentServerwas constructed, but this helper rereads the on-disk configuration; if that file changes during the daemon lifetime,command_startand subsequent metrics can receive the correlation ID from the new path while servicing still targets the old path. Suggestion: useself.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
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.
There was a problem hiding this comment.
🟡 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_commandcalls firecommand_start/command_errorbefore the pre-warm at lines 165–175; even when the datastore already exists, both events therefore omit the installation'scorrelation_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
| 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); |
Implement (
manual_rollback_start,runtime_update_success,manual_rollback_runtime_success) to be consistent with other operation metrics.Adds a new
command_errormetric 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 applieslocation:file:linewhere the error was originally raisedRelated PRs in stack: