Skip to content

Commit 99e2f46

Browse files
committed
simplify and avoid circular deps
1 parent 7f2090a commit 99e2f46

File tree

7 files changed

+238
-201
lines changed

7 files changed

+238
-201
lines changed

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

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@
1414

1515
from __future__ import annotations
1616

17-
from typing import TYPE_CHECKING, Any
17+
from typing import Any
1818

19+
from opentelemetry._logs import Logger
1920
from opentelemetry.semconv._incubating.attributes import (
2021
gen_ai_attributes as GenAI,
2122
)
2223
from opentelemetry.semconv.attributes import server_attributes
23-
from opentelemetry.trace import SpanKind
24+
from opentelemetry.trace import SpanKind, Tracer
25+
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
2426
from opentelemetry.util.genai.types import Error, GenAIInvocation
25-
26-
if TYPE_CHECKING:
27-
from opentelemetry.util.genai.handler import TelemetryHandler
27+
from opentelemetry.util.types import AttributeValue
2828

2929

3030
class EmbeddingInvocation(GenAIInvocation):
@@ -34,13 +34,11 @@ class EmbeddingInvocation(GenAIInvocation):
3434
context manager rather than constructing this directly.
3535
"""
3636

37-
@property
38-
def operation_name(self) -> str:
39-
return GenAI.GenAiOperationNameValues.EMBEDDINGS.value
40-
4137
def __init__(
4238
self,
43-
handler: TelemetryHandler,
39+
tracer: Tracer,
40+
metrics_recorder: InvocationMetricsRecorder,
41+
logger: Logger,
4442
provider: str,
4543
*,
4644
request_model: str | None = None,
@@ -55,8 +53,13 @@ def __init__(
5553
) -> None:
5654
"""Use handler.start_embedding(provider) or handler.embedding(provider) instead of calling this directly."""
5755
super().__init__(
58-
handler, attributes=attributes, metric_attributes=metric_attributes
56+
tracer,
57+
metrics_recorder,
58+
logger,
59+
attributes=attributes,
60+
metric_attributes=metric_attributes,
5961
)
62+
self._operation_name = GenAI.GenAiOperationNameValues.EMBEDDINGS.value
6063
self.provider = provider # e.g., azure.ai.openai, openai, aws.bedrock
6164
self.request_model = request_model
6265
self.server_address = server_address
@@ -68,12 +71,32 @@ def __init__(
6871
self.dimension_count = dimension_count
6972
self.response_model_name = response_model_name
7073
self._span_name = (
71-
f"{self.operation_name} {request_model}"
74+
f"{self._operation_name} {request_model}"
7275
if request_model
73-
else self.operation_name
76+
else self._operation_name
7477
)
7578
self._span_kind = SpanKind.CLIENT
76-
handler._start(self)
79+
self._start()
80+
81+
def _get_metric_attributes(self) -> dict[str, Any]:
82+
optional_attrs = (
83+
(GenAI.GEN_AI_PROVIDER_NAME, self.provider),
84+
(GenAI.GEN_AI_REQUEST_MODEL, self.request_model),
85+
(GenAI.GEN_AI_RESPONSE_MODEL, self.response_model_name),
86+
(server_attributes.SERVER_ADDRESS, self.server_address),
87+
(server_attributes.SERVER_PORT, self.server_port),
88+
)
89+
attrs: dict[str, AttributeValue] = {
90+
GenAI.GEN_AI_OPERATION_NAME: self._operation_name,
91+
**{k: v for k, v in optional_attrs if v is not None},
92+
}
93+
attrs.update(self.metric_attributes)
94+
return attrs
95+
96+
def _get_metric_token_counts(self) -> dict[str, int]:
97+
if self.input_tokens is not None:
98+
return {GenAI.GenAiTokenTypeValues.INPUT.value: self.input_tokens}
99+
return {}
77100

78101
def _apply_finish(self, error: Error | None = None) -> None:
79102
optional_attrs = (
@@ -87,7 +110,7 @@ def _apply_finish(self, error: Error | None = None) -> None:
87110
(GenAI.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens),
88111
)
89112
attributes: dict[str, Any] = {
90-
GenAI.GEN_AI_OPERATION_NAME: self.operation_name,
113+
GenAI.GEN_AI_OPERATION_NAME: self._operation_name,
91114
**{
92115
key: value
93116
for key, value in optional_attrs

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

Lines changed: 23 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,11 @@
4848

4949
from __future__ import annotations
5050

51-
import timeit
5251
from contextlib import contextmanager
53-
from typing import Iterator, TypeVar
52+
from typing import Iterator
5453

5554
from typing_extensions import deprecated
5655

57-
from opentelemetry import context as otel_context
5856
from opentelemetry._logs import (
5957
LoggerProvider,
6058
get_logger,
@@ -64,7 +62,6 @@
6462
from opentelemetry.trace import (
6563
TracerProvider,
6664
get_tracer,
67-
set_span_in_context,
6865
)
6966
from opentelemetry.util.genai.embedding_invocation import EmbeddingInvocation
7067
from opentelemetry.util.genai.inference_invocation import (
@@ -73,30 +70,11 @@
7370
)
7471
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
7572
from opentelemetry.util.genai.tool_invocation import ToolInvocation
76-
from opentelemetry.util.genai.types import (
77-
Error,
78-
GenAIInvocation,
79-
)
73+
from opentelemetry.util.genai.types import Error
8074
from opentelemetry.util.genai.version import __version__
8175
from opentelemetry.util.genai.workflow_invocation import WorkflowInvocation
8276

8377

84-
def _safe_detach(invocation: GenAIInvocation) -> None:
85-
"""Detach the context token if still present, as a safety net."""
86-
if invocation._context_token is not None:
87-
try:
88-
otel_context.detach(invocation._context_token)
89-
except Exception: # pylint: disable=broad-except
90-
pass
91-
try:
92-
invocation.span.end()
93-
except Exception: # pylint: disable=broad-except
94-
pass
95-
96-
97-
_T = TypeVar("_T", bound=GenAIInvocation)
98-
99-
10078
class TelemetryHandler:
10179
"""
10280
High-level handler managing GenAI invocation lifecycles and emitting
@@ -127,40 +105,6 @@ def __init__(
127105
schema_url=schema_url,
128106
)
129107

130-
def _start(self, invocation: _T) -> _T:
131-
"""Start a GenAI invocation and create a pending span entry."""
132-
133-
invocation._handler = self
134-
135-
invocation.span = self._tracer.start_span(
136-
name=invocation._span_name,
137-
kind=invocation._span_kind,
138-
)
139-
140-
# Record a monotonic start timestamp (seconds) for duration
141-
# calculation using timeit.default_timer.
142-
invocation._monotonic_start_s = timeit.default_timer()
143-
invocation._context_token = otel_context.attach(
144-
set_span_in_context(invocation.span)
145-
)
146-
147-
return invocation
148-
149-
def _finish( # pylint: disable=no-self-use
150-
self, invocation: _T, error: Error | None = None
151-
) -> _T:
152-
"""Finalize a GenAI invocation and end its span."""
153-
if invocation._context_token is None:
154-
# TODO: Provide feedback that this invocation was not started
155-
return invocation
156-
157-
try:
158-
invocation._apply_finish(error)
159-
finally:
160-
# Detach context and end span even if finishing fails
161-
_safe_detach(invocation)
162-
return invocation
163-
164108
# New-style factory methods: construct + start in one call, handler stored on invocation
165109

166110
def start_inference(
@@ -177,7 +121,9 @@ def start_inference(
177121
returned invocation, then call invocation.stop() or invocation.fail().
178122
"""
179123
return InferenceInvocation(
180-
self,
124+
self._tracer,
125+
self._metrics_recorder,
126+
self._logger,
181127
provider,
182128
request_model=request_model,
183129
server_address=server_address,
@@ -194,9 +140,12 @@ def start_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pyright: ign
194140
Use ``handler.start_inference()`` instead.
195141
"""
196142
if invocation._context_token is not None:
197-
# Already started (e.g. via InferenceInvocation.__init__)
143+
# Already started (e.g. tracer passed to LLMInvocation.__init__)
198144
return invocation
199-
return self._start(invocation)
145+
invocation._start_with_handler(
146+
self._tracer, self._metrics_recorder, self._logger
147+
)
148+
return invocation
200149

201150
def start_embedding(
202151
self,
@@ -212,7 +161,9 @@ def start_embedding(
212161
invocation, then call invocation.stop() or invocation.fail().
213162
"""
214163
return EmbeddingInvocation(
215-
self,
164+
self._tracer,
165+
self._metrics_recorder,
166+
self._logger,
216167
provider,
217168
request_model=request_model,
218169
server_address=server_address,
@@ -234,7 +185,9 @@ def start_tool(
234185
invocation.stop() or invocation.fail().
235186
"""
236187
return ToolInvocation(
237-
self,
188+
self._tracer,
189+
self._metrics_recorder,
190+
self._logger,
238191
name,
239192
arguments=arguments,
240193
tool_call_id=tool_call_id,
@@ -251,7 +204,9 @@ def start_workflow(
251204
Set remaining attributes on the returned invocation, then call
252205
invocation.stop() or invocation.fail().
253206
"""
254-
return WorkflowInvocation(self, name)
207+
return WorkflowInvocation(
208+
self._tracer, self._metrics_recorder, self._logger, name
209+
)
255210

256211
@deprecated(
257212
"handler.stop_llm() is deprecated. Use invocation.stop() instead."
@@ -262,7 +217,8 @@ def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pyright: igno
262217
.. deprecated::
263218
Use ``invocation.stop()`` instead.
264219
"""
265-
return self._finish(invocation)
220+
invocation.stop()
221+
return invocation
266222

267223
@deprecated(
268224
"handler.fail_llm() is deprecated. Use invocation.fail(error) instead."
@@ -277,7 +233,8 @@ def fail_llm(
277233
.. deprecated::
278234
Use ``invocation.fail(error)`` instead.
279235
"""
280-
return self._finish(invocation, error)
236+
invocation.fail(error)
237+
return invocation
281238

282239
@contextmanager
283240
def inference(

0 commit comments

Comments
 (0)