Skip to content

Commit f4fc721

Browse files
committed
linting fix
1 parent a428585 commit f4fc721

4 files changed

Lines changed: 248 additions & 70 deletions

File tree

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

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
_apply_llm_finish_attributes,
8686
_get_embedding_span_name,
8787
_get_llm_span_name,
88+
_maybe_emit_embedding_event,
8889
_maybe_emit_llm_event,
8990
)
9091
from opentelemetry.util.genai.types import (
@@ -129,9 +130,9 @@ def __init__(
129130
schema_url=schema_url,
130131
)
131132

132-
def _record_llm_metrics(
133+
def _record_metrics(
133134
self,
134-
invocation: LLMInvocation,
135+
invocation: GenAIInvocation,
135136
span: Span | None = None,
136137
*,
137138
error_type: str | None = None,
@@ -144,18 +145,6 @@ def _record_llm_metrics(
144145
error_type=error_type,
145146
)
146147

147-
@staticmethod
148-
def _record_embedding_metrics(
149-
invocation: EmbeddingInvocation,
150-
span: Span | None = None,
151-
*,
152-
error_type: str | None = None,
153-
) -> None:
154-
# Metrics recorder currently supports LLMInvocation fields only.
155-
# Keep embedding metrics as a no-op until dedicated embedding
156-
# metric support is added.
157-
return
158-
159148
def _start(self, invocation: _T) -> _T:
160149
"""Start a GenAI invocation and create a pending span entry."""
161150
if isinstance(invocation, LLMInvocation):
@@ -187,11 +176,12 @@ def _stop(self, invocation: _T) -> _T:
187176
try:
188177
if isinstance(invocation, LLMInvocation):
189178
_apply_llm_finish_attributes(span, invocation)
190-
self._record_llm_metrics(invocation, span)
179+
self._record_metrics(invocation, span)
191180
_maybe_emit_llm_event(self._logger, span, invocation)
192181
elif isinstance(invocation, EmbeddingInvocation):
193182
_apply_embedding_finish_attributes(span, invocation)
194-
self._record_embedding_metrics(invocation, span)
183+
self._record_metrics(invocation, span)
184+
_maybe_emit_embedding_event(self._logger, span, invocation)
195185
finally:
196186
# Detach context and end span even if finishing fails
197187
otel_context.detach(invocation.context_token)
@@ -210,17 +200,16 @@ def _fail(self, invocation: _T, error: Error) -> _T:
210200
if isinstance(invocation, LLMInvocation):
211201
_apply_llm_finish_attributes(span, invocation)
212202
_apply_error_attributes(span, error, error_type)
213-
self._record_llm_metrics(
214-
invocation, span, error_type=error_type
215-
)
203+
self._record_metrics(invocation, span, error_type=error_type)
216204
_maybe_emit_llm_event(
217205
self._logger, span, invocation, error_type
218206
)
219207
elif isinstance(invocation, EmbeddingInvocation):
220208
_apply_embedding_finish_attributes(span, invocation)
221209
_apply_error_attributes(span, error, error_type)
222-
self._record_embedding_metrics(
223-
invocation, span, error_type=error_type
210+
self._record_metrics(invocation, span, error_type=error_type)
211+
_maybe_emit_embedding_event(
212+
self._logger, span, invocation, error_type
224213
)
225214
finally:
226215
# Detach context and end span even if finishing fails
Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Helpers for emitting GenAI metrics from LLM invocations."""
1+
"""Helpers for emitting GenAI metrics from invocations."""
22

33
from __future__ import annotations
44

@@ -18,7 +18,7 @@
1818
create_duration_histogram,
1919
create_token_histogram,
2020
)
21-
from opentelemetry.util.genai.types import LLMInvocation
21+
from opentelemetry.util.genai.types import GenAIInvocation, LLMInvocation
2222
from opentelemetry.util.types import AttributeValue
2323

2424

@@ -29,57 +29,69 @@ def __init__(self, meter: Meter):
2929
self._duration_histogram: Histogram = create_duration_histogram(meter)
3030
self._token_histogram: Histogram = create_token_histogram(meter)
3131

32+
@staticmethod
33+
def _build_attributes(
34+
invocation: GenAIInvocation,
35+
error_type: Optional[str] = None,
36+
) -> Dict[str, AttributeValue]:
37+
"""Build metric attributes from an invocation."""
38+
attributes: Dict[str, AttributeValue] = {}
39+
40+
# Set attributes using getattr for fields that may not exist on base class
41+
operation_name = getattr(invocation, "operation_name", None)
42+
if operation_name:
43+
attributes[GenAI.GEN_AI_OPERATION_NAME] = operation_name
44+
45+
request_model = getattr(invocation, "request_model", None)
46+
if request_model:
47+
attributes[GenAI.GEN_AI_REQUEST_MODEL] = request_model
48+
49+
provider = getattr(invocation, "provider", None)
50+
if provider:
51+
attributes[GenAI.GEN_AI_PROVIDER_NAME] = provider
52+
53+
response_model_name = getattr(invocation, "response_model_name", None)
54+
if response_model_name:
55+
attributes[GenAI.GEN_AI_RESPONSE_MODEL] = response_model_name
56+
57+
server_address = getattr(invocation, "server_address", None)
58+
if server_address:
59+
attributes[server_attributes.SERVER_ADDRESS] = server_address
60+
61+
server_port = getattr(invocation, "server_port", None)
62+
if server_port is not None:
63+
attributes[server_attributes.SERVER_PORT] = server_port
64+
65+
metric_attributes = getattr(invocation, "metric_attributes", None)
66+
if metric_attributes:
67+
attributes.update(metric_attributes)
68+
69+
if error_type:
70+
attributes[error_attributes.ERROR_TYPE] = error_type
71+
72+
return attributes
73+
3274
def record(
3375
self,
3476
span: Optional[Span],
35-
invocation: LLMInvocation,
77+
invocation: GenAIInvocation,
3678
*,
3779
error_type: Optional[str] = None,
3880
) -> None:
39-
"""Record duration and token metrics for an invocation if possible."""
81+
"""Record duration and token metrics for an invocation if possible.
82+
83+
For LLMInvocation: records duration and token (input/output) metrics.
84+
For EmbeddingInvocation: records duration only.
85+
"""
4086

4187
# pylint: disable=too-many-branches
4288

4389
if span is None:
4490
return
4591

46-
token_counts: list[tuple[int, str]] = []
47-
if invocation.input_tokens is not None:
48-
token_counts.append(
49-
(
50-
invocation.input_tokens,
51-
GenAI.GenAiTokenTypeValues.INPUT.value,
52-
)
53-
)
54-
if invocation.output_tokens is not None:
55-
token_counts.append(
56-
(
57-
invocation.output_tokens,
58-
GenAI.GenAiTokenTypeValues.OUTPUT.value,
59-
)
60-
)
92+
attributes = self._build_attributes(invocation, error_type)
6193

62-
attributes: Dict[str, AttributeValue] = {
63-
GenAI.GEN_AI_OPERATION_NAME: GenAI.GenAiOperationNameValues.CHAT.value
64-
}
65-
if invocation.request_model:
66-
attributes[GenAI.GEN_AI_REQUEST_MODEL] = invocation.request_model
67-
if invocation.provider:
68-
attributes[GenAI.GEN_AI_PROVIDER_NAME] = invocation.provider
69-
if invocation.response_model_name:
70-
attributes[GenAI.GEN_AI_RESPONSE_MODEL] = (
71-
invocation.response_model_name
72-
)
73-
if invocation.server_address:
74-
attributes[server_attributes.SERVER_ADDRESS] = (
75-
invocation.server_address
76-
)
77-
if invocation.server_port is not None:
78-
attributes[server_attributes.SERVER_PORT] = invocation.server_port
79-
if invocation.metric_attributes:
80-
attributes.update(invocation.metric_attributes)
81-
82-
# Calculate duration from span timing or invocation monotonic start
94+
# Calculate duration from invocation monotonic start
8395
duration_seconds: Optional[float] = None
8496
if invocation.monotonic_start_s is not None:
8597
duration_seconds = max(
@@ -88,8 +100,6 @@ def record(
88100
)
89101

90102
span_context = set_span_in_context(span)
91-
if error_type:
92-
attributes[error_attributes.ERROR_TYPE] = error_type
93103

94104
if duration_seconds is not None:
95105
self._duration_histogram.record(
@@ -98,12 +108,31 @@ def record(
98108
context=span_context,
99109
)
100110

101-
for token_count, token_type in token_counts:
102-
self._token_histogram.record(
103-
token_count,
104-
attributes=attributes | {GenAI.GEN_AI_TOKEN_TYPE: token_type},
105-
context=span_context,
106-
)
111+
# Only record token metrics for LLMInvocation
112+
if isinstance(invocation, LLMInvocation):
113+
token_counts: list[tuple[int, str]] = []
114+
if invocation.input_tokens is not None:
115+
token_counts.append(
116+
(
117+
invocation.input_tokens,
118+
GenAI.GenAiTokenTypeValues.INPUT.value,
119+
)
120+
)
121+
if invocation.output_tokens is not None:
122+
token_counts.append(
123+
(
124+
invocation.output_tokens,
125+
GenAI.GenAiTokenTypeValues.OUTPUT.value,
126+
)
127+
)
128+
129+
for token_count, token_type in token_counts:
130+
self._token_histogram.record(
131+
token_count,
132+
attributes=attributes
133+
| {GenAI.GEN_AI_TOKEN_TYPE: token_type},
134+
context=span_context,
135+
)
107136

108137

109138
__all__ = ["InvocationMetricsRecorder"]

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,44 @@ def _maybe_emit_llm_event(
227227
logger.emit(event)
228228

229229

230+
def _maybe_emit_embedding_event(
231+
logger: Logger | None,
232+
span: Span,
233+
invocation: EmbeddingInvocation,
234+
error_type: str | None = None,
235+
) -> None:
236+
"""Emit a gen_ai.client.inference.operation.details event to the logger.
237+
238+
This function creates a LogRecord event following the semantic convention
239+
for gen_ai.client.inference.operation.details as specified in the GenAI
240+
event semantic conventions.
241+
242+
For more details, see the semantic convention documentation:
243+
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#event-eventgen_aiclientinferenceoperationdetails
244+
"""
245+
if not is_experimental_mode() or not should_emit_event() or logger is None:
246+
return
247+
248+
# Build event attributes by reusing the attribute getter functions
249+
attributes: dict[str, Any] = {}
250+
attributes.update(_get_embedding_common_attributes(invocation))
251+
attributes.update(_get_embedding_request_attributes(invocation))
252+
attributes.update(_get_embedding_response_attributes(invocation))
253+
254+
# Add error.type if operation ended in error
255+
if error_type is not None:
256+
attributes[error_attributes.ERROR_TYPE] = error_type
257+
258+
# Create and emit the event
259+
context = set_span_in_context(span, get_current())
260+
event = LogRecord(
261+
event_name="gen_ai.client.embedding.operation.details",
262+
attributes=attributes,
263+
context=context,
264+
)
265+
logger.emit(event)
266+
267+
230268
def _apply_llm_finish_attributes(
231269
span: Span, invocation: LLMInvocation
232270
) -> None:
@@ -365,6 +403,7 @@ def _get_embedding_response_attributes(
365403
"_get_llm_response_attributes",
366404
"_get_llm_span_name",
367405
"_maybe_emit_llm_event",
406+
"_maybe_emit_embedding_event",
368407
"_apply_embedding_finish_attributes",
369408
"_get_embedding_common_attributes",
370409
"_get_embedding_request_attributes",

0 commit comments

Comments
 (0)