Skip to content

Commit 5fe9709

Browse files
committed
properly start spans
1 parent aeb5a37 commit 5fe9709

2 files changed

Lines changed: 76 additions & 65 deletions

File tree

qtype/interpreter/base/base_step_executor.py

Lines changed: 73 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,6 @@ def _resolve_secret(self, value: str | SecretReference) -> str:
9797
context = f"step '{self.step.id}'"
9898
return self._secret_manager(value, context)
9999

100-
def _enrich_with_span_metadata(
101-
self, message: FlowMessage, span: trace.Span
102-
) -> FlowMessage:
103-
"""Add span_id and trace_id to message metadata for feedback tracking."""
104-
if not span.is_recording():
105-
return message
106-
107-
span_context = span.get_span_context()
108-
updated_metadata = {
109-
**message.metadata,
110-
"span_id": format(span_context.span_id, "016x"),
111-
"trace_id": format(span_context.trace_id, "032x"),
112-
}
113-
return message.model_copy(update={"metadata": updated_metadata})
114-
115100
async def _filter_and_collect_errors(
116101
self,
117102
message_stream: AsyncIterator[FlowMessage],
@@ -191,7 +176,10 @@ async def execute(
191176
Processed messages, with failed messages emitted first
192177
"""
193178
# Start a span for tracking
194-
# Note: We manually manage the span lifecycle to allow yielding
179+
# Note: We do NOT attach this span to context here to avoid
180+
# making upstream steps children of this step when we consume
181+
# the input stream. Instead, _process_message_with_telemetry
182+
# will attach it when calling process_message().
195183
span = self._tracer.start_span(
196184
f"step.{self.step.id}",
197185
attributes={
@@ -201,10 +189,8 @@ async def execute(
201189
},
202190
)
203191

204-
# Make this span the active context so child spans will nest under it
205-
# Only attach if span is recording (i.e., real tracer is configured)
206-
ctx = trace.set_span_in_context(span)
207-
token = context.attach(ctx) if span.is_recording() else None
192+
# Store span in self so _process_message_with_telemetry can access it
193+
self._current_step_span = span
208194

209195
# Initialize the cache
210196
# this is done once per execution so re-runs are fast
@@ -259,7 +245,7 @@ async def process_item(
259245
self.progress.update_for_message(
260246
result, self.context.on_progress
261247
)
262-
yield self._enrich_with_span_metadata(result, span)
248+
yield result
263249

264250
# Emit failed messages after processing completes
265251
for msg in failed_messages:
@@ -268,14 +254,14 @@ async def process_item(
268254
self.progress.update_for_message(
269255
msg, self.context.on_progress
270256
)
271-
yield self._enrich_with_span_metadata(msg, span)
257+
yield msg
272258

273259
# Finalize and track those messages too
274260
async for msg in self.finalize():
275261
message_count += 1
276262
if msg.is_failed():
277263
error_count += 1
278-
yield self._enrich_with_span_metadata(msg, span)
264+
yield msg
279265

280266
# Close the cache
281267
if self.cache:
@@ -302,10 +288,8 @@ async def process_item(
302288
span.set_status(Status(StatusCode.ERROR, f"Step failed: {e}"))
303289
raise
304290
finally:
305-
# Detach the context and end the span
306-
# Only detach if we successfully attached (span was recording)
307-
if token is not None:
308-
context.detach(token)
291+
# Clean up step span reference and end the span
292+
self._current_step_span = None
309293
span.end()
310294

311295
@abstractmethod
@@ -383,51 +367,77 @@ async def _process_message_with_telemetry(
383367
384368
This method creates a child span for each message processing
385369
operation, automatically recording errors and success metrics.
386-
The child span will automatically be nested under the current
387-
active span in the context.
370+
The child span will be nested under the step span.
371+
372+
The step span context is attached here (not in execute()) to
373+
ensure step spans are siblings under the flow span, not nested.
388374
"""
389-
# Get current context and create child span within it
390-
span = self._tracer.start_span(
391-
f"step.{self.step.id}.process_message",
392-
attributes={
393-
"session.id": message.session.session_id,
394-
},
395-
)
375+
# Attach step span context so process_message span becomes its child
376+
step_span = getattr(self, "_current_step_span", None)
377+
if step_span and step_span.is_recording():
378+
ctx = trace.set_span_in_context(step_span)
379+
token = context.attach(ctx)
380+
else:
381+
token = None
396382

397383
try:
398-
output_count = 0
399-
error_occurred = False
400-
401-
async for output_msg in self.process_message(message):
402-
output_count += 1
403-
if output_msg.is_failed():
404-
error_occurred = True
405-
span.add_event(
406-
"message_failed",
407-
{
408-
"error": str(output_msg.error),
409-
},
384+
# Create child span for this specific message processing
385+
span = self._tracer.start_span(
386+
f"step.{self.step.id}.process_message",
387+
attributes={
388+
"session.id": message.session.session_id,
389+
},
390+
)
391+
392+
try:
393+
output_count = 0
394+
error_occurred = False
395+
396+
async for output_msg in self.process_message(message):
397+
output_count += 1
398+
if output_msg.is_failed():
399+
error_occurred = True
400+
span.add_event(
401+
"message_failed",
402+
{
403+
"error": str(output_msg.error),
404+
},
405+
)
406+
# Enrich with process_message span for feedback tracking
407+
span_context = span.get_span_context()
408+
updated_metadata = {
409+
**output_msg.metadata,
410+
"span_id": format(span_context.span_id, "016x"),
411+
"trace_id": format(span_context.trace_id, "032x"),
412+
}
413+
yield output_msg.model_copy(
414+
update={"metadata": updated_metadata}
410415
)
411-
yield output_msg
412416

413-
# Record processing metrics
414-
span.set_attribute("message.outputs", output_count)
417+
# Record processing metrics
418+
span.set_attribute("message.outputs", output_count)
415419

416-
if error_occurred:
420+
if error_occurred:
421+
span.set_status(
422+
Status(
423+
StatusCode.ERROR, "Message processing had errors"
424+
)
425+
)
426+
else:
427+
span.set_status(Status(StatusCode.OK))
428+
429+
except Exception as e:
430+
span.record_exception(e)
417431
span.set_status(
418-
Status(StatusCode.ERROR, "Message processing had errors")
432+
Status(StatusCode.ERROR, f"Processing failed: {e}")
419433
)
420-
else:
421-
span.set_status(Status(StatusCode.OK))
422-
423-
except Exception as e:
424-
span.record_exception(e)
425-
span.set_status(
426-
Status(StatusCode.ERROR, f"Processing failed: {e}")
427-
)
428-
raise
434+
raise
435+
finally:
436+
span.end()
429437
finally:
430-
span.end()
438+
# Detach step span context
439+
if token is not None:
440+
context.detach(token)
431441

432442
async def finalize(self) -> AsyncIterator[FlowMessage]:
433443
"""

tests/interpreter/test_step_executor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,10 @@ async def test_span_metadata_enrichment(self, simple_step, session):
427427
assert len(trace_id) == 32
428428
assert all(c in "0123456789abcdef" for c in trace_id)
429429

430-
# Span ID should be the same for all messages in the same step
430+
# Each message should have a unique process_message span_id
431+
# (this allows per-message feedback instead of per-step)
431432
span_ids = [r.metadata["span_id"] for r in results]
432-
assert len(set(span_ids)) == 1
433+
assert len(set(span_ids)) == len(results)
433434

434435
# Trace ID should be the same for all messages in the same execution
435436
trace_ids = [r.metadata["trace_id"] for r in results]

0 commit comments

Comments
 (0)