Skip to content

fix(dash): wire client-side log file and instrument chat lifecycle#103

Open
michaelroy-amd wants to merge 7 commits into
mainfrom
feat/dash-client-logging
Open

fix(dash): wire client-side log file and instrument chat lifecycle#103
michaelroy-amd wants to merge 7 commits into
mainfrom
feat/dash-client-logging

Conversation

@michaelroy-amd

@michaelroy-amd michaelroy-amd commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

  • tracing-subscriber was a declared dependency of rocm-dash-tui and rocm-dash-daemon, but tracing_subscriber:: was never called anywhere in the codebase — every info!/warn!/debug! call was silently dropped with no subscriber installed.
  • Adds a new apps/rocm/src/logging.rs module that installs a process-wide, file-only, non-blocking tracing subscriber writing daily-rotated files under ~/.rocm/logs (via a new AppPaths::client_log_dir()), with pruning to the 7 most recent files.
  • Adds lifecycle tracing (request-start, elapsed/TTFT-equivalent, completion, error, timeout) to the chat request path shared by all three AgentClient::complete() backends (RigAgentClient, ChatGptAgentClient, AnthropicAgentClient) in crates/rocm-dash-tui/src/agent.rs, tagged with a backend field so a hung or failing chat is traceable to a specific provider.

Root cause

The TUI owns the terminal in raw mode, so no subscriber could safely write to stdout/stderr — but no subscriber was ever wired up at all, meaning EAI-7357's client-side logging requirement was entirely unimplemented despite the dependency being present in Cargo.toml.

Implementation notes

  • The writer is strictly file-only and non-blocking (tracing_appender::non_blocking) so it never risks corrupting the TUI's raw-mode display.
  • logging::init() returns Option<WorkerGuard>; the guard is held for the process lifetime in main() since dropping it flushes and stops the writer.
  • A missing/failed AppPaths::discover() or log-dir creation degrades to no logging rather than a startup failure.
  • Default filter: warn,rocm=info,rocm_dash_tui=info,rocm_dash_daemon=info, overridable via RUST_LOG.
  • Log files are pruned to the 7 most recent daily-rotated files on each init() call, bounding disk usage for long-lived installs.

Relates to EAI-7357

Test Plan

  • cargo build clean
  • cargo clippy --all-targets --all-features -- -D warnings clean
  • cargo test -p rocm-dash-tui agent:: -- --test-threads=1 — all 28 existing agent tests pass unchanged
  • cargo test -p rocm (full suite, 3x repeated to check for flakiness) — 340/340 passed each run
  • New unit tests in apps/rocm/src/logging.rs: log-dir/file creation, idempotent re-init when a global subscriber already exists, and log pruning retains only the most recent files

Follow-up / out of scope (reviewer note)

This PR wires client-side logging for the dash/TUI client (the rocm binary), which is the scope of EAI-7357. Instrumenting the standalone rocmd daemon binary is intentionally not included here and is left as a follow-up.

tracing-subscriber is a declared dependency of rocm-dash-tui and
rocm-dash-daemon but tracing_subscriber:: is never called anywhere, so
every info!/warn!/debug! across those crates is silently dropped and
there is no client-side log file at all.

Add apps/rocm/src/logging.rs: a file-only, non-blocking tracing
subscriber (tracing_appender::non_blocking) writing daily-rotated files
under AppPaths::client_log_dir() (~/.rocm/logs), respecting RUST_LOG via
EnvFilter with an info-for-our-crates/warn-otherwise default. It never
writes to stdout/stderr, since the TUI owns the raw-mode terminal and a
stray write there would corrupt the display. Old rotated files beyond a
retention cap are pruned on startup so a long-lived install's logs
directory stays bounded.

Instrument the chat request path (crates/rocm-dash-tui/src/agent.rs):
finish_agent_request, the shared complete() tail for all three live
backends (Rig/OpenAI, ChatGPT OAuth, Anthropic), now logs request-start,
completion (elapsed ms, reply length, skills fired), error, and timeout,
tagged with the backend name — so a hung chat request is traceable in
the log file.

Relates to EAI-7357.

Signed-off-by: Michael Roy <michael.roy@amd.com>
init_creates_log_dir_and_a_log_file asserted that the guard returned
by logging::init() was always Some, but tracing::subscriber::set_global_default
can only succeed once per process. When the full test suite ran
unfiltered/multi-threaded, another test could win that race first,
making the assertion flaky.

The rolling file appender opens its current-period file eagerly at
construction time, before the subscriber install is attempted, so the
directory/file existence checks hold regardless of which test wins the
race. Only assert on filesystem state, and skip the info! log line
when the guard is None.

Signed-off-by: Michael Roy <michael.roy@amd.com>
…ering the tree

Adversarial-review follow-up for EAI-7357:

- Retention off-by-one: prune_old_logs() ran before the daily appender
  opened today's file, so the effective on-disk bound was
  MAX_RETAINED_LOGS + 1 (8), not 7. Reorder init() to construct the
  appender first (which opens today's file eagerly), then prune, so
  today's active file is counted within the retention window. Update the
  MAX_RETAINED_LOGS doc to state it is the total including today's file,
  and add a regression test asserting the ordering keeps the total at
  MAX_RETAINED_LOGS.
- Tests wrote fixtures under the repo tree (.rocm-work/tests/logging),
  cluttering the working copy. Switch temp_paths to std::env::temp_dir().

Relates to EAI-7357

Signed-off-by: Michael Roy <michael.roy@amd.com>
Comment thread apps/rocm/src/logging.rs Fixed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
Comment thread apps/rocm/src/logging.rs Dismissed
CI follow-up for EAI-7357:

- Regenerate MANIFEST.md and THIRD_PARTY_NOTICES.txt for the new
  tracing-appender dependency (and its transitive crossbeam-channel /
  symlink additions). Verified with `cargo xtask manifest --check` and
  `cargo xtask tpn --check`.
- Harden the client log directory resolution against the CodeQL
  rust/path-injection alerts: the base path is derived from $HOME /
  $ROCM_CLI_DATA_DIR / config, so route it through a single
  validated_log_dir() barrier that creates the dir, canonicalizes both it
  and the AppPaths root, rejects `..` traversal, and requires containment
  under the root — degrading to no logging on any mismatch. All fs ops
  (appender, pruner) now run on the validated, canonicalized PathBuf.
- Canonicalize the test fixtures' temp base for the same reason.

Relates to EAI-7357

Signed-off-by: Michael Roy <michael.roy@amd.com>
Comment thread apps/rocm/src/logging.rs Dismissed
…ducible

The CI "Third-party notices current" check regenerates the notices and
compares them to the committed file. ureq is dual-licensed
"MIT OR Apache-2.0", and cargo-about's choice between the two offered
licenses is not stable across environments — it flips ureq between the
Apache-2.0 and MIT sections run-to-run, so the committed file (generated
in one environment) can fail the check when CI regenerates in another.

Pin ureq to MIT (one of its own offered licenses) via a per-crate
`accepted` entry in about.toml. With the pin, four consecutive full
regenerations are byte-identical and match the committed
THIRD_PARTY_NOTICES.txt, so the check is deterministic. The generated
notices content is unchanged by this commit (ureq was already under MIT
in the committed file); the pin only guarantees CI reproduces it.

Relates to EAI-7357

Signed-off-by: Michael Roy <michael.roy@amd.com>
…otices

The previous per-crate `accepted = ["MIT"]` entry did not fix the flaky
"Third-party notices current" check: cargo-about treats per-crate
`accepted` as ADDITIVE to the global list, not restrictive, so it never
forced ureq's license. ureq ships both LICENSE-MIT and LICENSE-APACHE
and cargo-about's pick between them varies with filesystem read order, so
a notice generated locally (ureq under MIT) failed CI's regenerate-and-
compare (CI, like main, resolves ureq under Apache-2.0).

Replace it with a proper `[ureq.clarify]` entry that pins ureq to
Apache-2.0 with LICENSE-APACHE as the source of truth (checksum-verified).
Three consecutive regenerations are now byte-identical, ureq resolves to
Apache-2.0 exactly as main does, and THIRD_PARTY_NOTICES.txt now differs
from main only by the three crates the new tracing-appender dependency
adds (tracing-appender, crossbeam-channel, symlink). Verified with
`cargo xtask tpn --check`.

Relates to EAI-7357

Signed-off-by: Michael Roy <michael.roy@amd.com>
The logging test helper canonicalized the OS temp dir but fell back to
the raw `std::env::temp_dir()` on error (`.unwrap_or(base)`). That
fallback re-introduces the env-derived (tainted) path into the test's
filesystem operations, so CodeQL's rust/path-injection flow was not
fully cut for the test-code sinks. Replace the fallback with `expect`
(the OS temp dir always exists and canonicalizes), matching production's
`canonicalize(..)?` barrier which has no tainted fallback.

Relates to EAI-7357

Signed-off-by: Michael Roy <michael.roy@amd.com>
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