Skip to content

Commit 19485cf

Browse files
feat(otel): tracing facade — unified span+log subscriber, diagnostics migration, log CI assertions (#350)
* feat(otel): tracing facade — unified span+log subscriber, diagnostics migration, log CI assertions PR3 per interview decision Q6 (docs/vrs/06-observability/open-questions.md): - Telemetry::init now installs one tracing subscriber: a stderr fmt layer runs unconditionally so migrated diagnostics stay visible with or without an endpoint (documented deviation from PR1's zero-output unset case; RUST_LOG overrides the INFO default), plus — behind the endpoint guard — a tracing-opentelemetry span layer and an opentelemetry-appender-tracing bridge over a new SDK logger provider sharing endpoint/protocol/resource. experimental_use_tracing_span_context stamps active span ids onto records. shutdown flushes logger+meter+tracer together. - st2.reconcile_pass spans move to tracing::info_span! at all three sites (up_once, up_loop_until pass, reconcile_pass_specs_with_sessions); names and st2.host/st2.crash_loops/st2.unparked attributes unchanged; metric recording calls untouched. Each pass emits a deterministic INFO record (target st2, "reconcile pass complete", result=pass|fail). - Non-user-facing eprintln! diagnostics migrate to tracing::warn!/error! with unchanged text: crash-loop give-ups and supervisor notification, park-channel setup, catalog watching, ding transport ambiguity, driver session degradation, harness-state floor writes. CLI command output, boot banners, and validation reports stay println!/eprintln!. - tests/otel_export.rs additionally asserts logs.ndjson carries the completion record (body/severity) correlated to the captured st2.reconcile_pass span via trace/span ids; existing span+metric assertions unchanged. agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@929dc21 * fix(otel): scope RUST_LOG to stderr layer; make metric setup best-effort Two review findings on the log-bridge PR: 1. The EnvFilter was attached to the base subscriber, so RUST_LOG=warn silently disabled INFO span/log export even with an endpoint set. The stderr fmt layer now carries the filter alone. The OpenTelemetry layers carry their own static filter, independent of RUST_LOG, that silences only exporter-internal targets (opentelemetry*): the SDK's BatchLogProcessor emits tracing events from inside its own emit(), and feeding those back through the unfiltered log bridge recursed until the export thread overflowed its stack (gdb-proven). App signals export unfiltered. 2. A metric exporter build failure early-returned before logger setup, killing logs along with metrics. Metric setup is now best-effort: warn and continue to the logger provider. agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@929dc21 * fix(otel): keep piped stderr diagnostics plain — fmt layer ansi follows tty tracing_subscriber's fmt layer enables ANSI whenever the 'ansi' feature is on, regardless of destination, so redirected stderr carried SGR escapes where the pre-migration eprintln! path emitted plain bytes. Gate the layer on std::io::stderr().is_terminal(). agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@929dc21 * fix(otel): emit one correlated signal set and shut providers down cleanly agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@e4789b0 * feat(otel): add semantic reconciliation trace hierarchy agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@e4789b0
1 parent af4881e commit 19485cf

22 files changed

Lines changed: 1497 additions & 358 deletions

Cargo.lock

Lines changed: 147 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ st2-wire = { path = "crates/st2-wire" }
3737
tempfile = "3"
3838
toml = "0.9"
3939
opentelemetry = "0.30"
40-
opentelemetry-otlp = { version = "0.30", default-features = false, features = ["http-json", "reqwest-blocking-client", "internal-logs"] }
41-
opentelemetry_sdk = { version = "0.30", features = ["spec_unstable_metrics_views"] }
40+
opentelemetry-otlp = { version = "0.30", default-features = false, features = ["http-json", "reqwest-blocking-client", "internal-logs", "logs"] }
41+
opentelemetry_sdk = { version = "0.30", features = ["spec_unstable_metrics_views", "logs"] }
42+
# `experimental_use_tracing_span_context` stamps the active tracing span's trace/span ids onto
43+
# emitted log records — required so logs correlate with spans without manual context plumbing.
44+
opentelemetry-appender-tracing = { version = "0.30", features = ["experimental_use_tracing_span_context"] }
45+
tracing = "0.1"
46+
tracing-opentelemetry = "0.31"
47+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "env-filter", "fmt", "registry", "std"] }
4248
tungstenite = "0.30"
4349

4450
[dev-dependencies]

docs/vrs/06-observability/.decisions/0001-all-signals-via-pr-stack.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# All three signals via a three-PR gh stack
22

3-
Status: draft
3+
Status: accepted
44

55
Recorded 2026-08-25 from the aligned observability interview (axe decision catalog Q1 + Q3).
66

@@ -17,6 +17,12 @@ is shared by every signal; once it lands, metrics and logs are incremental. Defe
1717
invites "traces shipped, rest never happens" — and the CI proof obligation (otelite capture +
1818
assertions) is signal-generic anyway.
1919

20+
## Evidence and Argument
21+
22+
The exporter/provider plumbing and otelite capture harness are shared across all three signals,
23+
while rates/durations require metrics and correlated diagnostics require logs. Splitting delivery
24+
at signal boundaries isolates review and CI failures without treating traces as the finished scope.
25+
2026
## Decision
2127

2228
**Q1 — Scope**: all three signals (traces, metrics, logs) are the target. Not traces-only; not
@@ -31,18 +37,14 @@ traces-first-with-maybe-later.
3137
Each PR lands green independently; 2 and 3 build on 1's shared plumbing
3238
(provider/resource/exporter) only.
3339

34-
## Alternatives considered
35-
36-
- **Traces-first, defer the rest** — rejected: defers most of the value (rates/durations live in
37-
metrics; correlated diagnostics in logs) for no real risk reduction, since the risky part
38-
(feature set, sync export, flush-at-exit) is identical across signals.
39-
- **Single PR** — rejected: couples an unreviewable diff (instrumentation across `run.rs`,
40-
`exec_backend.rs`, `hooks.rs`, plus unit changes and test harness) to the plumbing; a regression
41-
in any slice blocks all of it.
42-
- **Spool-files instead of direct OTLP export** — writing telemetry records to local spool files
43-
for a separate shipper to forward — rejected: adds a moving part st2 must own (rotation,
44-
retention, crash-safety) to solve a problem the fleet pipeline already solves at
45-
`127.0.0.1:4318`; the ambient-endpoint no-op contract would need re-inventing.
40+
## Options
41+
42+
| Option | Result | Reason |
43+
| --- | --- | --- |
44+
| All three signals in a three-PR stack | Selected | Shares the risky plumbing while isolating signal-specific review and CI failures. |
45+
| Traces first, defer the rest | Rejected | Defers rates, durations, and correlated diagnostics without reducing exporter risk. |
46+
| Single PR | Rejected | Couples all instrumentation sites and the capture harness into one blocking review surface. |
47+
| Spool files instead of direct OTLP | Rejected | Adds rotation, retention, and crash-safety machinery for a pipeline the fleet already provides. |
4648

4749
## Consequences
4850

docs/vrs/06-observability/.decisions/0002-subsystem-placement-and-central-reference.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Numbered subsystem under st2's VRS root, referencing dotfiles' central tree
22

3-
Status: draft
3+
Status: accepted
44

55
Recorded 2026-08-25 from the aligned observability interview (axe decision catalog Q2).
66

@@ -19,6 +19,20 @@ central tree (`01-conventions`, `09-integration/spec.md`). But st2-specific deci
1919
feature set, trace roots in `src/run.rs`, unit env propagation — have no home there and would be
2020
invisible to anyone working in this repo.
2121

22+
## Evidence and Argument
23+
24+
The repository already uses numbered VRS subsystems for product-specific lifecycle contracts,
25+
while the dotfiles observability tree owns fleet-wide semantics. A local subsystem with explicit
26+
central references preserves both contributor locality and one authority per shared rule.
27+
28+
## Options
29+
30+
| Option | Result | Reason |
31+
| --- | --- | --- |
32+
| Numbered st2 subsystem | Selected | Keeps local mechanisms and evidence beside their code while referencing central rules. |
33+
| Standalone st2 documentation root | Rejected | Duplicates the established VRS hierarchy. |
34+
| Central dotfiles tree only | Rejected | Leaves st2-specific crate, process, and CI decisions without repository-local authority. |
35+
2236
## Decision
2337

2438
This tree is a **numbered subsystem under st2's existing `docs/vrs` root**: `06-observability`

docs/vrs/06-observability/.decisions/0003-blocking-http-json-exporter.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Blocking HTTP-JSON exporter, single-client feature set, explicit flush at exit
22

3-
Status: draft
3+
Status: accepted
44

55
Recorded 2026-08-25 from the aligned observability interview, backed by prototype evidence
66
([../.experiments/2026-08-25-rust-to-otelite-capture.md](../.experiments/2026-08-25-rust-to-otelite-capture.md)).
@@ -24,6 +24,22 @@ Two traps surfaced during prototyping:
2424
2. **The async-batch trap.** The default async batch exporter requires a tokio runtime; under
2525
st2's sync process model it panicked at export time.
2626

27+
## Evidence and Argument
28+
29+
The linked otelite prototype exported real OTLP/HTTP JSON from a synchronous Rust process only
30+
with the blocking client and an exclusive reqwest-client feature selection. It reproduced both
31+
the dual-client `NoHttpClient` failure and the async exporter's missing-reactor panic, and proved
32+
that explicit shutdown delivers short-lived spans.
33+
34+
## Options
35+
36+
| Option | Result | Reason |
37+
| --- | --- | --- |
38+
| Blocking HTTP-JSON with one reqwest client feature | Selected | Proven end to end in the synchronous process model. |
39+
| Async HTTP client/batch runtime | Rejected | st2 has no tokio reactor and the prototype panicked at export time. |
40+
| Default or dual reqwest client features | Rejected | Compilation succeeds but exporter construction fails with `NoHttpClient`. |
41+
| gRPC | Rejected | The fleet ingestion contract is OTLP/HTTP JSON only. |
42+
2743
## Decision
2844

2945
st2 uses `opentelemetry-otlp` 0.30 with `default-features = false` and exactly

0 commit comments

Comments
 (0)