Skip to content

Commit 77d9c3c

Browse files
committed
remove span state, use otel context for parent/child
1 parent 0d749a9 commit 77d9c3c

File tree

3 files changed

+51
-77
lines changed

3 files changed

+51
-77
lines changed

util/opentelemetry-util-genai/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dynamic = ["version"]
88
description = "OpenTelemetry GenAI Utils"
99
readme = "README.rst"
1010
license = "Apache-2.0"
11-
requires-python = ">=3.8"
11+
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
1414
]

util/opentelemetry-util-genai/src/opentelemetry/util/genai/generators.py

Lines changed: 44 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
"""
3333

3434
from contextlib import contextmanager
35-
from dataclasses import dataclass, field
36-
from typing import Dict, List, Optional
35+
from contextvars import Token
36+
from typing import Dict, Optional
3737
from uuid import UUID
3838

39+
from typing_extensions import TypeAlias
40+
41+
from opentelemetry import context as otel_context
3942
from opentelemetry import trace
4043
from opentelemetry.semconv._incubating.attributes import (
4144
gen_ai_attributes as GenAI,
@@ -45,7 +48,6 @@
4548
SpanKind,
4649
Tracer,
4750
set_span_in_context,
48-
use_span,
4951
)
5052

5153
from .span_utils import (
@@ -54,11 +56,8 @@
5456
)
5557
from .types import Error, LLMInvocation
5658

57-
58-
@dataclass
59-
class _SpanState:
60-
span: Span
61-
children: List[UUID] = field(default_factory=list)
59+
# Type alias matching the token type expected by opentelemetry.context.detach
60+
ContextToken: TypeAlias = Token[otel_context.Context]
6261

6362

6463
class BaseTelemetryGenerator:
@@ -87,43 +86,17 @@ def __init__(
8786
):
8887
self._tracer: Tracer = tracer or trace.get_tracer(__name__)
8988

90-
# TODO: Map from run_id -> _SpanState, to keep track of spans and parent/child relationships
91-
self.spans: Dict[UUID, _SpanState] = {}
92-
93-
def _start_span(
94-
self,
95-
name: str,
96-
kind: SpanKind,
97-
parent_run_id: Optional[UUID] = None,
98-
) -> Span:
99-
parent_span = (
100-
self.spans.get(parent_run_id)
101-
if parent_run_id is not None
102-
else None
103-
)
104-
if parent_span is not None:
105-
ctx = set_span_in_context(parent_span.span)
106-
span = self._tracer.start_span(name=name, kind=kind, context=ctx)
107-
else:
108-
# top-level or missing parent
109-
span = self._tracer.start_span(name=name, kind=kind)
110-
set_span_in_context(span)
111-
112-
return span
113-
114-
def _end_span(self, run_id: UUID):
115-
state = self.spans[run_id]
116-
for child_id in state.children:
117-
child_state = self.spans.get(child_id)
118-
if child_state:
119-
child_state.span.end()
120-
state.span.end()
121-
del self.spans[run_id]
89+
# Store the active span and its context attachment token
90+
self._active: Dict[UUID, tuple[Span, ContextToken]] = {}
12291

12392
def start(self, invocation: LLMInvocation):
124-
# Create/register the span; keep it active but do not end it here.
125-
with self._start_span_for_invocation(invocation):
126-
pass
93+
# Create a span and attach it as current; keep the token to detach later
94+
span = self._tracer.start_span(
95+
name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {invocation.request_model}",
96+
kind=SpanKind.CLIENT,
97+
)
98+
token = otel_context.attach(set_span_in_context(span))
99+
self._active[invocation.run_id] = (span, token)
127100

128101
@contextmanager
129102
def _start_span_for_invocation(self, invocation: LLMInvocation):
@@ -132,46 +105,46 @@ def _start_span_for_invocation(self, invocation: LLMInvocation):
132105
The span is not ended automatically on exiting the context; callers
133106
must finalize via _finalize_invocation.
134107
"""
135-
# Establish parent/child relationship if a parent span exists.
136-
parent_state = (
137-
self.spans.get(invocation.parent_run_id)
138-
if invocation.parent_run_id is not None
139-
else None
140-
)
141-
if parent_state is not None:
142-
parent_state.children.append(invocation.run_id)
143-
span = self._start_span(
108+
# Create a span and attach it as current; keep the token to detach later
109+
span = self._tracer.start_span(
144110
name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {invocation.request_model}",
145111
kind=SpanKind.CLIENT,
146-
parent_run_id=invocation.parent_run_id,
147112
)
148-
with use_span(span, end_on_exit=False) as span:
149-
span_state = _SpanState(
150-
span=span,
151-
)
152-
self.spans[invocation.run_id] = span_state
153-
yield span
113+
token = otel_context.attach(set_span_in_context(span))
114+
# store active span and its context attachment token
115+
self._active[invocation.run_id] = (span, token)
116+
yield span
154117

155118
def finish(self, invocation: LLMInvocation):
156-
state = self.spans.get(invocation.run_id)
157-
if state is None:
158-
with self._start_span_for_invocation(invocation) as span:
119+
active = self._active.get(invocation.run_id)
120+
if active is None:
121+
# If missing, create a quick span to record attributes and end it
122+
with self._tracer.start_as_current_span(
123+
name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {invocation.request_model}",
124+
kind=SpanKind.CLIENT,
125+
) as span:
159126
_apply_finish_attributes(span, invocation)
160-
self._end_span(invocation.run_id)
161127
return
162128

163-
span = state.span
129+
span, token = active
164130
_apply_finish_attributes(span, invocation)
165-
self._end_span(invocation.run_id)
131+
# Detach context and end span
132+
otel_context.detach(token)
133+
span.end()
134+
del self._active[invocation.run_id]
166135

167136
def error(self, error: Error, invocation: LLMInvocation):
168-
state = self.spans.get(invocation.run_id)
169-
if state is None:
170-
with self._start_span_for_invocation(invocation) as span:
137+
active = self._active.get(invocation.run_id)
138+
if active is None:
139+
with self._tracer.start_as_current_span(
140+
name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {invocation.request_model}",
141+
kind=SpanKind.CLIENT,
142+
) as span:
171143
_apply_error_attributes(span, error)
172-
self._end_span(invocation.run_id)
173144
return
174145

175-
span = state.span
146+
span, token = active
176147
_apply_error_attributes(span, error)
177-
self._end_span(invocation.run_id)
148+
otel_context.detach(token)
149+
span.end()
150+
del self._active[invocation.run_id]

util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"""
3434

3535
import time
36+
import uuid
3637
from typing import Any, List, Optional
3738
from uuid import UUID
3839

@@ -68,20 +69,20 @@ def start_llm(
6869
self,
6970
request_model: str,
7071
prompts: List[InputMessage],
71-
run_id: UUID,
72-
parent_run_id: Optional[UUID] = None,
72+
run_id: Optional[UUID] = None,
7373
**attributes: Any,
74-
) -> LLMInvocation:
74+
) -> UUID:
75+
if run_id is None:
76+
run_id = uuid.uuid4()
7577
invocation = LLMInvocation(
7678
request_model=request_model,
7779
messages=prompts,
7880
run_id=run_id,
79-
parent_run_id=parent_run_id,
8081
attributes=attributes,
8182
)
8283
self._llm_registry[invocation.run_id] = invocation
8384
self._generator.start(invocation)
84-
return invocation
85+
return invocation.run_id
8586

8687
def stop_llm(
8788
self,

0 commit comments

Comments
 (0)