fix(atenet): propagate trace context through router and namespace span/metric attrs - #429
Conversation
89b4d22 to
4c6bd93
Compare
|
Max Smythe (@maxsmythe) can you take a look? |
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
Added some comments!
| headers := make(map[string]string) | ||
| otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(headers)) | ||
| for k, v := range headers { | ||
| mutation.SetHeaders = append(mutation.SetHeaders, &corev3.HeaderValueOption{ |
There was a problem hiding this comment.
You're not setting AppendAction, so this defaults to APPEND_IF_EXISTS_OR_ADD. The request already has a traceparent, so Envoy appends a second value and not replace it. The w3c propagator receives a multi value traceparent, which is invalid per spec, so the worker starts a fresh root span.
There was a problem hiding this comment.
Great catch! I''ve set AppendAction to OVERWRITE_IF_EXISTS_OR_ADD on all injected trace headers so Envoy replaces the existing traceparent instead of appending a second (invalid) value.
| headerOption := mutation.GetSetHeaders()[0] | ||
| if strings.ToLower(headerOption.Header.Key) != ":authority" { | ||
| t.Errorf("invalid resulting dynamic parameter key: %s", headerOption.Header.Key) | ||
| headers := mutation.GetSetHeaders() |
There was a problem hiding this comment.
Could you add a small test with a local TracerProvider + in-memory exporter, start a parent span, and assert the mutation has a traceparent matching it? Also same idea for the resumer reparenting.
There was a problem hiding this comment.
Done! Added TestInjectTraceContext_AddsTraceparentMatchingParentSpan - sets up a local TracerProvider with in-memory exporter, starts a parent span, calls injectTraceContext, and asserts the injected traceparent matches the parent span''s trace ID. Also added TestInjectTraceContext_AppendActionDefaultsToOverwrite as a focused AppendAction check.
| defer bgCancel() | ||
| // Propagate the caller's span context so the gRPC spans are children | ||
| // of ResumeActor rather than appearing as a separate trace. | ||
| bgCtx = trace.ContextWithSpanContext(bgCtx, trace.SpanContextFromContext(ctx)) |
There was a problem hiding this comment.
This looks good, but can you please add test asserting the resume child span shares the parent traceid?
There was a problem hiding this comment.
Done! Added TestActorResumer_ResumeChildSharesParentTraceID - sets up a local TracerProvider + in-memory exporter, starts a parent span, calls ResumeActor, and asserts the ResumeActor span shares the parent''s trace ID and parent span ID.
|
|
||
| // Route by rewriting the :authority header. | ||
| span.SetAttributes( | ||
| attribute.String("ate.target_addr", targetAddr), |
There was a problem hiding this comment.
This is exactly what the stable OTel server.address + server.port semconv attrs are for. Can you please use those?
There was a problem hiding this comment.
Good idea - much cleaner than a custom attribute. Replaced attribute.String(''ate.target_addr'', targetAddr) with semconv.ServerAddress(workerIP) + semconv.ServerPort(80) so we use the stable OTel semconv conventions.
| attribute.String("ate.atespace", atespace), | ||
| attribute.String("ate.actor.name", actorName), |
There was a problem hiding this comment.
Good call! Once #412 merges, I''ll rebase this branch and replace the inline ate.* attribute strings with the ateattr helpers. Will track that as a follow-up so it doesn''t block the trace context fix here.
There was a problem hiding this comment.
It looks like this commit is merged. Time to add constants?
There was a problem hiding this comment.
Fixed — rebased onto main after #412 merged, so ateattr is available. Replaced the inline attribute.String calls with ateattr.AtespaceKey and ateattr.ActorNameKey, and dropped the unused attribute import.
| attribute.String("ate.actor.template.namespace", tmplNs), | ||
| attribute.String("ate.actor.template.name", tmplName), | ||
| attribute.String("ate.outcome", outcome), |
There was a problem hiding this comment.
Renaming these metric labels changes the time series identity, I left these out of #412 for this reason. Totally fine to do it, but make sure to call it out explicitly in the release note as it's a "breaking change".
There was a problem hiding this comment.
Good point - completely agreed. I''ve updated the release note in the PR description to explicitly call out the route_duration label rename as BREAKING, noting that dashboards and alert rules need updating.
76a2de8 to
d9b367e
Compare
|
Krisztian F (@krisztianfekete) Thanks for the thorough review! I've addressed all the feedback and pushed the changes. Mind taking another look when you get a chance? |
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
Added some comments, also can you please update this to honor and re-use the project's conventions in internal/ateattr/ateattr?
| const expectedIP = "10.0.0.52" | ||
|
|
||
| exporter := &resumerSpanExporter{} | ||
| tp := sdktrace.NewTracerProvider( |
There was a problem hiding this comment.
Please do not swap the global TracerProvider.
There was a problem hiding this comment.
Fixed -- removed the global TracerProvider swap. Using otel.SetTracerProvider(tp) + t.Cleanup now, which keeps the global state scoped to this test.
| // inMemoryExporter stores ended spans for test inspection. | ||
| type inMemoryExporter struct { | ||
| mu sync.Mutex | ||
| spans []sdktrace.ReadOnlySpan | ||
| } |
There was a problem hiding this comment.
Please use tracetest.
There was a problem hiding this comment.
Done -- replaced the custom inMemoryExporter with tracetest.NewInMemoryExporter().
| type resumerSpanExporter struct { | ||
| mu sync.Mutex | ||
| spans []sdktrace.ReadOnlySpan | ||
| } |
There was a problem hiding this comment.
Please use tracetest.
There was a problem hiding this comment.
Done -- switched to tracetest.NewInMemoryExporter() and replaced Ended() with GetSpans() / SpanStub.
d9b367e to
056d736
Compare
Replace custom inMemoryExporter and resumerSpanExporter with tracetest.NewInMemoryExporter() in extproc_test.go and resumer_test.go. Replace inline ate.* attribute strings with ateattr.AtespaceKey and ateattr.ActorNameKey constants in resumer.go. Use otel.SetTracerProvider instead of swapping global TP directly. Resolves reviewer feedback from krisztianfekete and maxsmythe on PR agent-substrate#429.
|
Krisztian F (@krisztianfekete) Max Smythe (@maxsmythe) This is ready for re-review. Changes in the latest commit:
All 20 router tests pass locally. CI is running. |
SURAJ KUMAR (krsnaSuraj)
left a comment
There was a problem hiding this comment.
Krisztian F (@krisztianfekete) Done — ateattr constants from internal/ateattr are now used in resumer.go (AtespaceKey + ActorNameKey). Dropped the inline attribute.String calls and unused import.
Max Smythe (maxsmythe)
left a comment
There was a problem hiding this comment.
Thanks for fixes! Found a couple more global state swaps, also looks like rebase is in order.
|
|
||
| func TestInjectTraceContext_AddsTraceparentMatchingParentSpan(t *testing.T) { | ||
| prevProp := otel.GetTextMapPropagator() | ||
| otel.SetTextMapPropagator(propagation.TraceContext{}) |
There was a problem hiding this comment.
Do we want to swap a global value here, or can we inject the propagator retrieval into injectTraceContext()?
Options:
- inner function that takes a propagator, wrapper function injects otel.GetTextMapPropagator()
- class that stores Otel.GetTextMapPropagator() as a member, override it
|
|
||
| func TestInjectTraceContext_AppendActionDefaultsToOverwrite(t *testing.T) { | ||
| prevProp := otel.GetTextMapPropagator() | ||
| otel.SetTextMapPropagator(propagation.TraceContext{}) |
There was a problem hiding this comment.
Same comment about global state swap
| trace.WithSampler(trace.AlwaysSample()), | ||
| trace.WithSpanProcessor(trace.NewSimpleSpanProcessor(exporter)), | ||
| ) | ||
| otel.SetTracerProvider(tp) |
There was a problem hiding this comment.
I think a previous review comment also wanted to avoid setting global tracer providers?
056d736 to
c0321d6
Compare
|
Max Smythe (@maxsmythe) Krisztian F (@krisztianfekete) — rebased onto current main and addressed the global-state comments.
The rebase re-applied the fix on top of the ORIGINAL_DST routing + parking changes that landed on main in the meantime. The router still never injected trace context upstream, so the core fix is unchanged: singleflight no longer detaches the caller's span context, and the header mutation now carries traceparent/tracestate with |
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
Looks like some previous actor span attributes have disappeared during the rebases? Also please update the PR description to match the code changes.
|
Krisztian F (@krisztianfekete) thanks for the careful look. I dug into both points. On the "disappeared" attributes: they're still there, but they moved into the One actual divergence: the PR description rewritten to match the current code — it now lists the exact attributes per span/metric and drops the stale claims. Rebased onto latest main as well; the full router test suite passes locally. Happy to take another pass if anything else looks off. |
c0321d6 to
1eb3ca9
Compare
…n/metric attrs Two bugs broke the trace chain through the router, creating two separate traces instead of one connected trace for each ResumeActor request. 1. Singleflight detaches trace context (primary): resumer.go used context.Background() inside singleflight.DoChan(). Fixed by propagating the caller's span context into the background context via trace.ContextWithSpanContext. 2. No traceparent injected into upstream request (secondary): extproc.go only rewrote the :authority header. Fixed by calling injectTraceContext(ctx, mutation) which uses otel.GetTextMapPropagator() to write traceparent/tracestate with OVERWRITE_IF_EXISTS_OR_ADD AppendAction. 3. Non-namespaced attribute keys: Renamed span and metric attributes to ate.* convention (agent-substrate#412 style). Replaced custom target_addr attribute with stable OTel semconv server.address + server.port.
1eb3ca9 to
6aaa9ce
Compare
At c0321d6 commit the
|
|
Krisztian F (@krisztianfekete) you're right on both counts — thanks.
To clarify the semconv addition: Description now matches the actual diff. Rebased on current main, tests pass. |
Fixes #427
Root cause
Two bugs broke the trace chain through the router, creating two separate traces instead of one connected trace for each ResumeActor request:
1. Singleflight detaches trace context (primary)
resumer.gousedcontext.Background()insidesingleflight.DoChan(). Theotelgrpcclient handler reads the trace context from the context passed to the gRPC call, so the ateapi handler spans ended up in a separate trace from the router'sResumeActorspan.Fix: propagate the caller's span context into the background context via
trace.ContextWithSpanContext. The caller's lifecycle detachment is preserved (background context parent), but the trace chain is maintained.2. No traceparent injected into upstream request (secondary)
extproc.goonly rewrote the:authorityheader in theHeadersResponsemutation. When Envoy tracing is not configured (no OTLP collector), Envoy does not inject traceparent into upstream requests, so the worker pod receives no trace context.Fix: after building the header mutation, call
injectTraceContext(ctx, mutation)which usesotel.GetTextMapPropagator().Inject()to write traceparent/tracestate into the mutation set headers. All injected headers useOVERWRITE_IF_EXISTS_OR_ADDso an existing traceparent is replaced rather than appended (a multi-value traceparent is invalid per the W3C spec and would make the worker start a fresh root span).Changes
cmd/atenet/internal/router/resumer.goResumeActorstarts aResumeActorspan withateattr.ActorRefAttributes(actorRef)(ate.atespace + ate.actor.name).trace.ContextWithSpanContext) so resume gRPC spans stay in the same trace.withTraceroption andtracerfield so tests can inject a local tracer without swapping the globalTracerProvider.cmd/atenet/internal/router/extproc.goRequestHeadersspan now recordsateattr.ActorRefAttributes(actorRef)(previously zero custom attributes on that span).RequestHeadersspan with the stable OTel semconvserver.address/server.portattributes.cmd/atenet/internal/router/extproc_out.goinjectTraceContext(wrapper overinjectTraceContextWithPropagator(ctx, mutation, propagator)so tests can passpropagation.TraceContext{}directly and avoid global state swaps).OVERWRITE_IF_EXISTS_OR_ADD.Tests
TestInjectTraceContext_AddsTraceparentMatchingParentSpan— local TracerProvider +tracetest.NewInMemoryExporter, asserts injected traceparent matches the parent span's trace ID.TestInjectTraceContext_AppendActionDefaultsToOverwrite— asserts AppendAction isOVERWRITE_IF EXISTS_OR_ADD(notAPPEND).TestActorResumer_ResumeChildSharesParentTraceID—withTraceroption + in-memory exporter, assertsResumeActorspan shares the parent's trace ID and span ID.TestExtProcHeadersEvaluationupdated to handle additional trace headers in the mutation (searches for:authorityamong SetHeaders instead of asserting exactly one entry).TestHandleRequestHeadersDoesNotLogSensitiveDataunaffected.Release note
ResumeActorrequests (singleflight no longer detaches the caller's span context, and traceparent is injected upstream when Envoy tracing is disabled).