BE-3656 APM Span Enrichment
Implements deferral item 2 of BE-3482 Datadog Logs and APM Conformance (“error.* / http.* on trace spans”): copy the structured fields already carried on ERROR-level tracing events, plus per-request HTTP metadata, onto the active OpenTelemetry span as Datadog APM tags. Branch feature/BE-3656 (worktree .worktrees/BE-3656), 3 commits, originally stacked on the BE-3541 head dee24ad3; after !571 merged it was rebased zero-conflict onto the post-!571 develop 1c4c33a5 (new head 8f50ef2a) and went out as MR !578 (“feat: enrich trace spans with error and http attributes”), MERGED by Andras 2026-07-22: squash 46a9be1b, merge commit 4a0d297f = the new develop tip. Verified live on dev (error.* on errored step spans + Error Tracking pickup; http.* on GET/POST client child spans; log output byte-identical).
Review hardening landed in a0735ebb: bodies excluded from spans (deliberate spec deviation, raise with team), first-error-wins guard (marker extension, since set_attribute is append-only under the 128-attr SDK cap), u64 try_from, and shared OTel test helpers in test.rs.
For Agents
UPDATE 2026-07-22 afternoon: MR !578 MERGED (squash
46a9be1b, merge4a0d297f= develop tip); see MR !578 (merged 2026-07-22). BE-3657 was rebased onto the merge the same day (13 commits replayed zero-conflict, head2baea68abecame22ccae93, patch-id byte-identical) and gained the DD_SERVICE env fixab8cfd11; its MR still waits on Andras’s explicit yes. The dev prototype has iterated to buildb7d15e2c; current dev state lives in Dev prototype verification (build b7d15e2c). State as of 2026-07-15: branchfeature/BE-3656= BE-3541 headdee24ad3+ 2 commits (a64f9457,6d4f4395), PUSHED, no MR. New filemando-lib/src/app/span_enrichment.rsholdsSpanEnrichmentLayer. Gates green: 455 passed, only the 2 known TEST_PG_HOST archiver failures (mando-known-flaky-tests-2026-07-15 is a different flake). Log output byte-identical vs before (8/8 error-macro + 39/39 workflow capture tests). Spec + plan under repodocs/superpowers/{specs,plans}/2026-07-15-be3656-*(untracked, never commit). The four tracing/OTel gotchas below are reusable beyond this ticket.
MR !578 (merged 2026-07-22)
- Opened after the 2026-07-16 session, once MR !571 (BE-3541 Single Error Emission) merged. Title: “feat: enrich trace spans with error and http attributes”.
- Head
8f50ef2a= the same 3 commits, zero-conflict rebase onto the post-!571 develop1c4c33a5. Pipeline GREEN. - Reviewers: krisztian.fekete1, gabor.nagy6, balint.budavoelgyi, jozsef.nagy1.
- MERGED by Andras 2026-07-22 with 0 review comments: squash commit
46a9be1b“feat: enrich trace spans with error and http attributes”, merge commit4a0d297f= the new develop tip. - Successor followed the same day: BE-3657 ran
rebase --ontodevelop4a0d297f, all 13 commits replayed zero-conflict (head2baea68abecame22ccae93, patch-id verified byte-identical to the reviewed content; branch CI pipeline2696324182on22ccae93success), then gained the DD_SERVICE env fixab8cfd11. Its MR still awaits Andras’s explicit yes.
Design
SpanEnrichmentLayer(mando-lib/src/app/span_enrichment.rs): atracing_subscriber::Layerwhoseon_eventcopieserror.*andhttp.*fields off ERROR-level tracing events onto the active OTel span as attributes (which become Datadog APM tags). It reads the active span viaopentelemetry::Context::current().span(), NOTtracing::Span::current()(see gotcha 1).instrumented_http_client: recordshttp.method,http.url,http.status_code,http.version, and request/responsecontent_lengthon the adapter span once per logical send, on the final outcome only (success, 4xx, or final failure after retries).- Bodies only via error events: request/response bodies are attached solely through error events, matching the “Only Error” column of the architecture-design
logs-and-apm.mdstandard. No body tags on success spans. - No status writes: the layer never sets span
Status; tracing-opentelemetry already does that on ERROR events (gotcha 3).
Quality gates
- Log output verified byte-identical to pre-change: 8/8 error-macro capture tests + 39/39 workflow capture tests. Enrichment is additive to spans only; the log formatter is untouched.
- Full suite: 455 passed, with only the 2 known
TEST_PG_HOSTarchiver integration failures (CI-only, pre-existing per AGENTS.md section 9). - Clippy clean; scoped rustfmt on touched files.
Dev deploy
Superseded build
The prototype has since iterated: dev now runs
1.16.1-feat.2682437891.b7d15e2c(same throwaway branch, headb7d15e2c, IaC pin applied 2026-07-16). Current stack contents and the full Datadog verification live in Dev prototype verification (build b7d15e2c). The section below records the earlierd17227aciteration.
As of 2026-07-16 ~02:10Z the dev environment runs a throwaway prototype build d17227ac (branch feature/BE-3482-dev-deploy, NEVER merge) that validates the BE-3657 target field shapes before the real work lands. Stack on top of the merged BE-3656 + BE-3541 span-enrichment base:
- Full-path error codes prototype:
error.code= FULL MODULE PATH. - Startup mock deep error (
mando-bess/src/debug_error.rs): a one-shot 4-level error chain fired every boot inside adebug.mock_deep_errorspan, so the field shapes are always live to query. error.errors/error.detailsas real JSON arrays: the mock emits anerror.errorscodes array + anerror.detailsobject array; thedd_formatterrecord_strpath parses exactly those two field names into real JSON (targeted parse, not a general mechanism).flow.step.systemcherry-picked from MR !570 (7f3db1ed) onto the prototype.- IaC pin (optimization-universe-iac
feature/error-hdl):1.16.1-feat.2680023002.d17227ac, applied 2026-07-16 02:07Z, Apply complete.
Datadog array-attribute queries verified
Array element/wildcard/negation matching all confirmed working against
error.errors/error.details:
- element match:
@error.errors:"<full code>"- wildcard:
@error.errors:*MockAdapterError*- object-field:
@error.details.code:*X*- numeric:
@error.details.line:>10- negation:
-@error.errors:*MockStepError*Facets still to create (Andras click, one-time):
@error.errors,@error.details.code,@error.fingerprint.
Arrays are log-side only
Datadog flattens array SPAN attributes to
key.0/key.1tags, which breaks faceting. Arrays live on the log side only; spans keep the scalarerror.code.
Tracing / OpenTelemetry Layer gotchas (reusable)
Field-verified facts about writing enrichment layers against tracing-opentelemetry 0.32 / opentelemetry_sdk 0.31, plus tooling landmines from this session. These generalize beyond BE-3656.
tracing::Span::current()is EMPTY inside aLayer::on_eventcallback under a scoped subscriber. When the subscriber is installed withtracing::subscriber::with_default(as in tests), the scoped-dispatch reentrancy guard makesSCOPED_COUNT >= 1, so the nestedget_defaultreturnsNoneandSpan::current()yields an empty span insideon_event. Under a GLOBAL.init()subscriber it works. Fix for layer code: readopentelemetry::Context::current().span()instead. tracing-opentelemetry 0.32’scontext_activation(defaulttrue) attaches the otel context on span enter/exit, so this path is correct in BOTH scoped tests and production. Note: outside layer callbacks (normal async code),Span::current()works fine even under scoped subscribers; the trap is specific toon_event.- In-memory test exporter path changed. On
opentelemetry_sdk 0.31the exporter isopentelemetry_sdk::trace::InMemorySpanExporter(behind thetestingfeature), NOTopentelemetry_sdk::testing::trace::InMemorySpanExporter. - Do not set span status in an enrichment layer. tracing-opentelemetry 0.32 already sets span
Status::error("")on any ERROR-level event (layer.rson_event, bothOtelDataStatearms). An enrichment layer setting status too is redundant and can clobber. OtelData.stateispub(crate)in 0.32. External code cannot manipulate the span builder directly. The only public write path isOpenTelemetrySpanExt(e.g.set_attribute). Caveats:set_attributeAPPENDS, so duplicate keys are possible; Datadog keeps the last; the SDK span attribute limit defaults to 128.
Tooling landmines (this session)
- Bash cwd RESETS to the main checkout between calls. A
cherry-pickintended for the dev-deploy worktree instead ran onbugfix/BE-3333(the main checkout), producing phantom conflicts against the wrong base; recovered viacherry-pick --quit+ restoring the single touched file. ALWAYS usegit -C <worktree>(orcd <worktree> && ...in the SAME command) and absolute paths for edits. When conflicts look weird, first check WHICH repo/branch you are actually in. - RTK proxy can garble grep/sed output into misleading results (apparent old-shaped code that was not actually there). Ground-truth with the
Readtool on absolute paths, notgrep/sedthrough the RTK hook. grep "test result" | tail -1on cargo test output is a false green. It shows only the LAST test binary, which may be0 passed / N filtered out. Always read ALL test-result lines and confirm the summed passed total is nonzero (compounds with the [[Mando AGENTS.md Master Guide|RTK--test-threads=1filter trap]]).
Related
- BE-3657 error_stack Adoption - successor ticket, rebased onto this MR’s merge commit
4a0d297f(14 commits @ab8cfd11); the dev-deploy prototype validates its field shapes - BE-3541 Single Error Emission - the base this branch was stacked on; merged as !571, after which !578 rebased onto develop
- BE-3482 Datadog Logs and APM Conformance - parent conformance ticket; this closes its deferral item 2
- BE-1842 Datadog Observability - dd_formatter span-field plumbing groundwork
- BE-3117 Per-Flow Error Context Store - error-handling redesign direction
- mando-lib - crate holding the enrichment layer and workflow engine
- Agent Context