Skip to content

Commit cbcd20a

Browse files
NiteshDhanpalclaude
andcommitted
feat(tracing): propagate trace context into Temporal via OTel interceptor
Attach temporalio.contrib.opentelemetry.TracingInterceptor to the Temporal client (both get_temporal_client entrypoints) when an OTel observability mode is active (SGP_OBS_MODE in {dual, lgtm}). This threads the caller's W3C trace context across the workflow -> activity boundary, so business spans created inside the model-loop activity see an active observability context and can be tagged with its trace_id via obs_correlation(). Gated + lazy-imported: default dd_only mode adds nothing and the temporalio OTel contrib is only imported when needed, so this is a no-op unless opted in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 24f0835 commit cbcd20a

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

src/agentex/lib/core/clients/temporal/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ def validate_worker_interceptors(interceptors: list[Any]) -> None:
8181
)
8282

8383

84+
def _build_otel_interceptors() -> list:
85+
"""Return an OpenTelemetry Temporal interceptor when an OTel observability
86+
mode is active (SGP_OBS_MODE in {dual, lgtm}), so trace context propagates
87+
across the workflow -> activity boundary. This lets business spans created
88+
inside the model-loop activity adopt the caller's observability trace_id.
89+
90+
Empty in the default dd_only mode, or when the temporalio OTel contrib is
91+
unavailable (safe no-op).
92+
"""
93+
import os
94+
95+
if os.getenv("SGP_OBS_MODE", "").strip().lower() not in ("dual", "lgtm"):
96+
return []
97+
try:
98+
from temporalio.contrib.opentelemetry import TracingInterceptor
99+
except ImportError:
100+
return []
101+
return [TracingInterceptor()]
102+
103+
84104
async def get_temporal_client(
85105
temporal_address: str,
86106
metrics_url: str | None = None,
@@ -146,6 +166,10 @@ async def get_temporal_client(
146166
dc = dataclasses.replace(dc, payload_codec=payload_codec)
147167
connect_kwargs["data_converter"] = dc
148168

169+
otel_interceptors = _build_otel_interceptors()
170+
if otel_interceptors:
171+
connect_kwargs["interceptors"] = otel_interceptors
172+
149173
if not metrics_url:
150174
client = await Client.connect(**connect_kwargs)
151175
else:

src/agentex/lib/core/temporal/workers/worker.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,24 @@ def _validate_interceptors(interceptors: list) -> None:
9191
)
9292

9393

94+
def _build_otel_interceptors() -> list:
95+
"""Return an OpenTelemetry Temporal interceptor when an OTel observability
96+
mode is active (SGP_OBS_MODE in {dual, lgtm}), so trace context propagates
97+
across the workflow -> activity boundary. This lets business spans created
98+
inside the model-loop activity adopt the caller's observability trace_id.
99+
100+
Empty in the default dd_only mode, or when the temporalio OTel contrib is
101+
unavailable (safe no-op).
102+
"""
103+
if os.getenv("SGP_OBS_MODE", "").strip().lower() not in ("dual", "lgtm"):
104+
return []
105+
try:
106+
from temporalio.contrib.opentelemetry import TracingInterceptor
107+
except ImportError:
108+
return []
109+
return [TracingInterceptor()]
110+
111+
94112
async def get_temporal_client(
95113
temporal_address: str,
96114
metrics_url: str | None = None,
@@ -136,6 +154,10 @@ async def get_temporal_client(
136154
dc = dataclasses.replace(dc, payload_codec=payload_codec)
137155
connect_kwargs["data_converter"] = dc
138156

157+
otel_interceptors = _build_otel_interceptors()
158+
if otel_interceptors:
159+
connect_kwargs["interceptors"] = otel_interceptors
160+
139161
if not metrics_url:
140162
client = await Client.connect(**connect_kwargs)
141163
else:

0 commit comments

Comments
 (0)