Skip to content

feat(tracing): propagate OTel trace context across Temporal boundaries - #485

Open
NiteshDhanpal wants to merge 1 commit into
nextfrom
feat/temporal-otel-trace-interceptor
Open

feat(tracing): propagate OTel trace context across Temporal boundaries#485
NiteshDhanpal wants to merge 1 commit into
nextfrom
feat/temporal-otel-trace-interceptor

Conversation

@NiteshDhanpal

@NiteshDhanpal NiteshDhanpal commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Temporal breaks OpenTelemetry context propagation. A start_workflow / execute_activity call is serialized and dispatched to a (potentially different) worker process, and the active W3C traceparent is not carried across that boundary. So any span created inside a workflow or activity becomes a new detached root — the trace shatters at every Temporal hop.

This bites agentex directly: adk.tracing.span branches on in_temporal_workflow() and, when true, creates the business span as a Temporal activity (TracingActivityName.START_SPAN). Without propagation, those business spans (and any downstream spans) detach from the turn's obs trace.

The existing ContextInterceptor threads business context (task_id) across the boundary — but nothing threads the obs-trace (W3C) context.

Change

Wire temporalio's first-party temporalio.contrib.opentelemetry.TracingInterceptor onto our Temporal client factories and worker, so client → workflow → activity is one trace. It injects the active span context into Temporal headers on outbound calls, and extracts + roots the workflow/activity execution spans under it, using the global OpenTelemetry propagator.

Wired at three points (both client factories + the worker):

  • core/temporal/workers/worker.pyget_temporal_client (worker's client) and the Worker(interceptors=...) (inbound execution, tracing outermost so business interceptors nest under it).
  • core/clients/temporal/utils.pyget_temporal_client used by TemporalClient (the ACP's workflow-starting client — the critical outbound start_workflow propagation point).
  • New core/tracing/temporal.pytemporal_tracing_interceptors() helper so every client/worker wires it identically.

Default-on with an opt-out

Enabled by default. Opt out with AGENTEX_TEMPORAL_TRACE_INTERCEPTOR_ENABLED=false (also accepts 0 / no / off). It also degrades to a no-op and never raises if temporalio's OpenTelemetry contrib isn't importable — so default-on can't break a worker. (adk already depends on opentelemetry-api/sdk, so the contrib is available in practice.)

Ordering (important)

On the worker the tracing interceptor is outermost:

TracingInterceptor          ← continues/roots the workflow/activity span first
  └─ ContextInterceptor     ← threads task_id, creates business spans as children
       └─ handler

If reversed, business spans would be created before the trace context is active → back to detached roots.

Tests

tests/lib/core/tracing/test_temporal_interceptor.py — default-on returns a TracingInterceptor, env opt-out returns [], and the contrib-missing fallback returns [] (verified against the real temporalio contrib).

Note: a full workflow→activity propagation assertion (same trace_id across the boundary) belongs in a Temporal-test-env integration test — noted as a follow-up; this PR unit-tests the wiring/flag/fallback logic.

Risk

Low — additive, config-gated, no-op when disabled or when the contrib is absent; no workflow-determinism concern (TracingInterceptor is designed for the workflow sandbox). Independent of PR #484 (correlation edge) — branched off main.

Relation to platform tracing

This is the Temporal-boundary piece of "W3C propagation everywhere": once it lands, an agent turn (or any workflow) that fans out through Temporal activities stays one trace end-to-end, and the business/anchor spans created inside activities attach to the turn.

🤖 Generated with Claude Code

Greptile Summary

The PR enables OpenTelemetry trace-context propagation across Temporal boundaries.

  • Adds a shared, default-on tracing-interceptor factory with an environment-variable opt-out and no-op fallback.
  • Installs tracing on both Temporal client factories and ahead of business interceptors on workers.
  • Adds unit coverage for default enablement, opt-out values, and unavailable-contrib fallback.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the constructor now executes inside the guarded try block, so ordinary constructor failures follow the documented no-op fallback instead of aborting Temporal startup.

Important Files Changed

Filename Overview
src/agentex/lib/core/tracing/temporal.py Adds the shared interceptor factory and now correctly catches both import and constructor failures, resolving the prior startup-failure concern.
src/agentex/lib/core/clients/temporal/utils.py Installs the tracing interceptor on the application Temporal client factory.
src/agentex/lib/core/temporal/workers/worker.py Installs tracing on the worker client and places the worker tracing interceptor before existing business interceptors.
tests/lib/core/tracing/test_temporal_interceptor.py Covers default enablement, supported opt-out values, non-falsy values, and unavailable-contrib fallback behavior.

Sequence Diagram

sequenceDiagram
    participant C as Temporal Client
    participant TW as Temporal Workflow Worker
    participant A as Temporal Activity
    C->>TW: start_workflow + injected OTel context
    activate TW
    TW->>A: execute_activity + propagated OTel context
    activate A
    A-->>TW: activity result
    deactivate A
    TW-->>C: workflow result
    deactivate TW
Loading

Reviews (2): Last reviewed commit: "feat(tracing): propagate OTel trace cont..." | Re-trigger Greptile

Comment thread src/agentex/lib/core/tracing/temporal.py Outdated
@NiteshDhanpal
NiteshDhanpal changed the base branch from main to next August 3, 2026 21:43
Temporal serializes start_workflow / execute_activity across (potentially
cross-process) boundaries and does not carry the active W3C traceparent, so
spans created inside a workflow or activity become detached roots -- the trace
shatters at every Temporal hop. This bites agentex directly: adk.tracing.span
creates the business span as a Temporal activity when in_temporal_workflow(),
so without propagation those spans detach from the turn's obs trace.

Wire temporalio's first-party TracingInterceptor onto both Temporal client
factories (worker client + the ACP's workflow-starting TemporalClient) and the
AgentexWorker, so client -> workflow -> activity is one trace. The interceptor
injects context on outbound calls and extracts + roots execution spans under it,
using the global OpenTelemetry propagator.

- ENABLED BY DEFAULT. Opt out with AGENTEX_TEMPORAL_TRACE_INTERCEPTOR_ENABLED=false
  (also 0/no/off). Safe no-op (never raises) if temporalio's OTel contrib isn't
  importable, so default-on can't break a worker.
- Tracing interceptor is placed OUTERMOST on the worker so existing business
  interceptors (and their spans) nest under the propagated span.

Tests: tests/lib/core/tracing/test_temporal_interceptor.py -- default-on returns
a TracingInterceptor, env opt-out returns [], contrib-missing returns [].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NiteshDhanpal
NiteshDhanpal force-pushed the feat/temporal-otel-trace-interceptor branch from 65c1c5f to 7b34987 Compare August 3, 2026 21:49
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