OpenTelemetry forwarder: the edge's round-trip record is the SERVER span - #36
Merged
Merged
Conversation
What - span kind: SERVER iff the record's service is http.request (an engine edge's round-trip record); every function execution is INTERNAL - a record's "from" no longer decides it. - tests pin both kinds (a function execution encodes as kind 1; the edge record as kind 2); CHANGELOG "Unreleased" carries the behaviour change. Why The engines now record the HTTP round trip as its own span (mercury-composable and mercury fix/connected-edge-spans) and map only that record to SERVER; the four forwarders keep one rule so a mixed-runtime trace renders one consistent tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
acn-ericlaw
added a commit
that referenced
this pull request
Sep 22, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
http.request, the edge's round-trip record (HttpRouter, AsyncContextHolder): REST automation mints a span at receipt (TraceInfo.newSpanId()), the first function — and the authentication service — parent onto it instead of the inbound traceparent span, and closeContext emits the record when the response completes: the single-shot writer, the stream renderer's terminal, the error writer (SimpleHttpUtility.sendResponse now passes the status it is about to write) and the housekeeper's timeout. start is the receipt time, exec_time the round trip to 3 decimals, status the HTTP status sent — except that a stream failing in-band after its head reports the failure's own status and message (AsyncContextHolder.markError). parent_span_id is the inbound traceparent span when present.
The stream relay's client leg is parented (EventEmitter.sendWithEventHttp): the async.http.request request event carries the sender's span, so the client leg sits under its caller instead of floating.
Head and tail, never per token: EventStreamWriter stamps the producer's trace and span on the first segment and the terminal (it reads the current trace through the new thread-keyed EventEmitter.getCurrentTrace(), registered and torn down with the worker's ref-keyed entry), data segments carry no trace; AsyncHttpClient captures its own span on the worker (PostOffice.getTrace()) and stamps it on the synthesized head/eof/exception segments in both relay modes while raw token frames are delivered untraced (deliverUntraced); the reply lane (AsyncHttpResponse.trackStreamFrames) counts data segments and annotates the terminal's record with frames.
Forwarder (extensions/opentelemetry-forwarder): kind SERVER iff service == http.request; every function execution is INTERNAL (from: http.request no longer decides the kind). README table updated.
Tests: new EventOverHttpStreamTest.edgeRelaySpansAreConnected (the whole tree: edge → relay → {client leg, peer edge → event.api.service → producer → two lane records per edge}, four lane records, two tails annotated frames=2, no data-frame spans; the test route is now tracing: true and the mock relay forwards through a trace-aware PostOffice), EventHttpTest.eventApiServiceIsAVisibleSpanInTheTrace (the /api/event edge record sits between the caller and event.api.service), TraceMetricsSpanDataTest.edgeRoundTripRecordIsTheServerSpan, and the playground engine's GraphSpanPropagationTest.downstreamHttpServiceChainsToCallerSpanViaTraceparent now expects the downstream edge's round-trip span between the fetcher and the downstream service. Guides: observability.md (The edge's round-trip span, the layers table, skip.rpc.tracing), http-streaming.md (Tracing a stream), event-over-http.md, configuration-reference.md.
Report: docs/test-reports/otel-dynatrace-certification.md gains Scenario 9 — one connected tree per request: the finding, the three causes, the fix and the re-drive (four traces rebuilt as single trees rooted at the edge span; 429 error traces — the provider's quota was spent — plus the token-bearing tree from the run before, frames: 2); What remains asks for your Dynatrace view of the four re-drive traces and records Scenario 8's backend view as confirmed.
Verification: platform-core 504 tests / 0 failures; opentelemetry-forwarder 35 / 0; event-script-engine SpanPropagationTest 4 / 0; full reactor mvn clean install BUILD SUCCESS (35 modules; the contract provider's snapshot link check caught one out-of-tree relative link in the guide, replaced by the GitHub URL); mkdocs build --strict OK.
Why
Eric's Dynatrace review of the v4.12.15 four-runtime certification traces found the span trees broken: the stream relay's client leg floated without a parent, every relayed token frame became an unparented reply-lane span, and no span covered the HTTP round trip — so a service's response time in the backend read as the first function's own execution time (0.5 ms for a 6 s stream). His rulings: fix the client leg in both engines, trace the head and the tail of a token stream and annotate the tail with the frame count, and add the round-trip span. Telemetry is operational readiness; a broken span tree is a defect. Lock-step twin: mercury fix/connected-edge-spans; the Python and Node.js forwarders adopt the same kind rule.
Behaviour change to read: a trace has one more span per traced request (http.request, the SERVER span); the first function is INTERNAL and parents onto it; an Event-over-HTTP hop's callee edge records its own round trip between the caller's span and event.api.service; a Java-to-Java or Java-to-Rust stream's reply-lane records are now parented (they were orphans) and there is no longer one per token. Dashboards keyed on kind=SERVER move from the first function to the edge record.
🤖 Generated with Claude Code
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com