|
16 | 16 | ObsSpanHandle, |
17 | 17 | open_obs_span, |
18 | 18 | close_obs_span, |
| 19 | + tag_ambient_obs_span, |
19 | 20 | ) |
20 | 21 | from agentex.lib.core.tracing.span_error import get_span_error, set_span_error |
21 | 22 | from agentex.lib.core.tracing.span_queue import ( |
|
42 | 43 | _OBS_HANDLES: dict[str, ObsSpanHandle] = {} |
43 | 44 |
|
44 | 45 |
|
| 46 | +def _in_temporal_activity() -> bool: |
| 47 | + """True when executing inside a Temporal activity. |
| 48 | +
|
| 49 | + On the Temporal path ``start_span`` and ``end_span`` run as SEPARATE |
| 50 | + activities (START_SPAN / END_SPAN) that Temporal can route to DIFFERENT |
| 51 | + worker processes. A wrapper obs span opened in the START_SPAN activity could |
| 52 | + therefore never be closed by END_SPAN -- its handle lives in another |
| 53 | + process's ``_OBS_HANDLES`` -- so it would leak (unbounded, OOM risk) and its |
| 54 | + persisted ``obs_span_id`` would dangle (the span is never .end()ed, so never |
| 55 | + exported to Tempo). |
| 56 | +
|
| 57 | + So inside an activity we do NOT open our own wrapper. We lean on the span the |
| 58 | + Temporal OTel ``TracingInterceptor`` (see ``core/tracing/temporal.py`` + |
| 59 | + scale-agentex-python#485) already made active for this activity -- which is |
| 60 | + rooted under the turn's propagated trace -- and merely stamp the reverse tag |
| 61 | + onto it (``tag_ambient_obs_span``). That keeps trace-level correlation with |
| 62 | + no cross-process handle to leak. |
| 63 | +
|
| 64 | + Never raises; returns False when temporalio isn't importable. |
| 65 | +
|
| 66 | + TODO(obs-followup): this intentionally drops the *named per-step* wrapper on |
| 67 | + the Temporal path (obs_span_id becomes the ambient activity span, not a |
| 68 | + step-named span) and does NOT add TurnTrace RETRY/ASYNC roll-up -- retried |
| 69 | + turns still surface as N unlinked spans. Follow-up diff should (a) optionally |
| 70 | + materialize a self-contained named wrapper inside a single activity using the |
| 71 | + span's own start/end timestamps, and (b) build the TurnTrace roll-up. |
| 72 | + Test-later: on a multi-replica worker fleet, assert _OBS_HANDLES stays |
| 73 | + bounded (no leak / OOM) and that obs_trace_id resolves to the turn trace. |
| 74 | + """ |
| 75 | + try: |
| 76 | + from temporalio import activity |
| 77 | + |
| 78 | + return activity.in_activity() |
| 79 | + except Exception: |
| 80 | + return False |
| 81 | + |
| 82 | + |
45 | 83 | class Trace: |
46 | 84 | """ |
47 | 85 | Trace is a wrapper around the Agentex API for tracing. |
@@ -105,9 +143,20 @@ def start_span( |
105 | 143 | # so you can pivot obs -> business in Tempo/DD. Falls back to the ambient |
106 | 144 | # obs context (ddtrace) when not in lgtm mode. Business trace_id stays the |
107 | 145 | # run-level task id. |
| 146 | + # |
| 147 | + # Inside a Temporal activity we skip the wrapper entirely and only tag the |
| 148 | + # interceptor-propagated ambient span: opening a wrapper there would leak, |
| 149 | + # since start_span / end_span run as separate activities on possibly |
| 150 | + # different workers and the handle could never be closed. See |
| 151 | + # _in_temporal_activity(). |
108 | 152 | id = str(uuid.uuid4()) |
109 | | - obs_handle = open_obs_span(name, business_span_id=id, business_trace_id=self.trace_id) |
110 | | - obs = obs_handle.correlation if obs_handle is not None else obs_correlation() |
| 153 | + if _in_temporal_activity(): |
| 154 | + tag_ambient_obs_span(business_span_id=id, business_trace_id=self.trace_id) |
| 155 | + obs_handle = None |
| 156 | + obs = obs_correlation() |
| 157 | + else: |
| 158 | + obs_handle = open_obs_span(name, business_span_id=id, business_trace_id=self.trace_id) |
| 159 | + obs = obs_handle.correlation if obs_handle is not None else obs_correlation() |
111 | 160 | if obs: |
112 | 161 | serialized_data = {**(serialized_data or {}), **obs} |
113 | 162 |
|
@@ -274,9 +323,20 @@ async def start_span( |
274 | 323 | # so you can pivot obs -> business in Tempo/DD. Falls back to the ambient |
275 | 324 | # obs context (ddtrace) when not in lgtm mode. Business trace_id stays the |
276 | 325 | # run-level task id. |
| 326 | + # |
| 327 | + # Inside a Temporal activity we skip the wrapper entirely and only tag the |
| 328 | + # interceptor-propagated ambient span: opening a wrapper there would leak, |
| 329 | + # since start_span / end_span run as separate activities on possibly |
| 330 | + # different workers and the handle could never be closed. See |
| 331 | + # _in_temporal_activity(). |
277 | 332 | id = str(uuid.uuid4()) |
278 | | - obs_handle = open_obs_span(name, business_span_id=id, business_trace_id=self.trace_id) |
279 | | - obs = obs_handle.correlation if obs_handle is not None else obs_correlation() |
| 333 | + if _in_temporal_activity(): |
| 334 | + tag_ambient_obs_span(business_span_id=id, business_trace_id=self.trace_id) |
| 335 | + obs_handle = None |
| 336 | + obs = obs_correlation() |
| 337 | + else: |
| 338 | + obs_handle = open_obs_span(name, business_span_id=id, business_trace_id=self.trace_id) |
| 339 | + obs = obs_handle.correlation if obs_handle is not None else obs_correlation() |
280 | 340 | if obs: |
281 | 341 | serialized_data = {**(serialized_data or {}), **obs} |
282 | 342 |
|
|
0 commit comments