Skip to content

Commit 80bd373

Browse files
committed
Refatoring and adding input token metric for Embedding invocation
1 parent 4ce9ee1 commit 80bd373

3 files changed

Lines changed: 73 additions & 50 deletions

File tree

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

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
create_duration_histogram,
1919
create_token_histogram,
2020
)
21-
from opentelemetry.util.genai.types import GenAIInvocation, LLMInvocation
21+
from opentelemetry.util.genai.types import (
22+
EmbeddingInvocation,
23+
GenAIInvocation,
24+
LLMInvocation,
25+
)
2226
from opentelemetry.util.types import AttributeValue
2327

2428

@@ -37,18 +41,15 @@ def _build_attributes(
3741
"""Build metric attributes from an invocation."""
3842
attributes: Dict[str, AttributeValue] = {}
3943

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+
if invocation.operation_name:
45+
attributes[GenAI.GEN_AI_OPERATION_NAME] = invocation.operation_name
4446

4547
request_model = getattr(invocation, "request_model", None)
4648
if request_model:
4749
attributes[GenAI.GEN_AI_REQUEST_MODEL] = request_model
4850

49-
provider = getattr(invocation, "provider", None)
50-
if provider:
51-
attributes[GenAI.GEN_AI_PROVIDER_NAME] = provider
51+
if invocation.provider:
52+
attributes[GenAI.GEN_AI_PROVIDER_NAME] = invocation.provider
5253

5354
response_model_name = getattr(invocation, "response_model_name", None)
5455
if response_model_name:
@@ -62,9 +63,8 @@ def _build_attributes(
6263
if server_port is not None:
6364
attributes[server_attributes.SERVER_PORT] = server_port
6465

65-
metric_attributes = getattr(invocation, "metric_attributes", None)
66-
if metric_attributes:
67-
attributes.update(metric_attributes)
66+
if invocation.metric_attributes:
67+
attributes.update(invocation.metric_attributes)
6868

6969
if error_type:
7070
attributes[error_attributes.ERROR_TYPE] = error_type
@@ -108,29 +108,27 @@ def record(
108108
context=span_context,
109109
)
110110

111-
# Only record token metrics for LLMInvocation
112-
if isinstance(invocation, LLMInvocation):
113-
token_counts: list[tuple[int, str]] = []
111+
# Record token metrics for LLMInvocation and EmbeddingInvocation
112+
if isinstance(invocation, (LLMInvocation, EmbeddingInvocation)):
114113
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-
)
114+
self._token_histogram.record(
115+
invocation.input_tokens,
116+
attributes=attributes
117+
| {
118+
GenAI.GEN_AI_TOKEN_TYPE: GenAI.GenAiTokenTypeValues.INPUT.value
119+
},
120+
context=span_context,
127121
)
128122

129-
for token_count, token_type in token_counts:
123+
# Only LLMInvocation has output tokens
124+
if isinstance(invocation, LLMInvocation):
125+
if invocation.output_tokens is not None:
130126
self._token_histogram.record(
131-
token_count,
127+
invocation.output_tokens,
132128
attributes=attributes
133-
| {GenAI.GEN_AI_TOKEN_TYPE: token_type},
129+
| {
130+
GenAI.GEN_AI_TOKEN_TYPE: GenAI.GenAiTokenTypeValues.OUTPUT.value
131+
},
134132
context=span_context,
135133
)
136134

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

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,18 @@ def _new_str_any_dict() -> dict[str, Any]:
256256

257257
@dataclass
258258
class GenAIInvocation:
259+
operation_name: str = ""
260+
provider: str | None = None
259261
context_token: ContextToken | None = None
260262
span: Span | None = None
261263
attributes: dict[str, Any] = field(default_factory=_new_str_any_dict)
264+
metric_attributes: dict[str, Any] = field(
265+
default_factory=_new_str_any_dict
266+
)
267+
"""
268+
Additional attributes to set on metrics. Must be of a low cardinality.
269+
These attributes will not be set on spans or events.
270+
"""
262271
error_type: str | None = None
263272

264273
monotonic_start_s: float | None = None
@@ -319,13 +328,6 @@ class LLMInvocation(GenAIInvocation):
319328
Additional attributes to set on spans and/or events. These attributes
320329
will not be set on metrics.
321330
"""
322-
metric_attributes: dict[str, Any] = field(
323-
default_factory=_new_str_any_dict
324-
)
325-
"""
326-
Additional attributes to set on metrics. Must be of a low cardinality.
327-
These attributes will not be set on spans or events.
328-
"""
329331
temperature: float | None = None
330332
top_p: float | None = None
331333
frequency_penalty: float | None = None
@@ -364,14 +366,6 @@ class EmbeddingInvocation(GenAIInvocation):
364366
will not be set on metrics.
365367
"""
366368

367-
metric_attributes: dict[str, Any] = field(
368-
default_factory=_new_str_any_dict
369-
)
370-
"""
371-
Additional attributes to set on metrics. Must be of a low cardinality.
372-
These attributes will not be set on spans or events.
373-
"""
374-
375369

376370
@dataclass()
377371
class ToolCall(GenAIInvocation):
@@ -399,6 +393,8 @@ class ToolCall(GenAIInvocation):
399393
- error.type: Error type if operation failed (Conditionally Required)
400394
"""
401395

396+
operation_name: str = GenAI.GenAiOperationNameValues.EXECUTE_TOOL.value
397+
402398
# Message identification fields (same as ToolCallRequest)
403399
# Note: These are required fields but must have defaults due to dataclass inheritance
404400
name: str = ""

util/opentelemetry-util-genai/tests/test_handler_metrics.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,16 @@ def _assert_metric_scope_schema_urls(
186186
scope_metric.scope.schema_url, expected_schema_url
187187
)
188188

189-
def test_stop_embedding_records_duration_only(self) -> None:
190-
"""Verify embedding invocations record duration but not token metrics."""
189+
def test_stop_embedding_records_duration_and_tokens(self) -> None:
190+
"""Verify embedding invocations record duration and input token metrics."""
191191
handler = TelemetryHandler(
192192
tracer_provider=self.tracer_provider,
193193
meter_provider=self.meter_provider,
194194
)
195195
invocation = EmbeddingInvocation(
196196
request_model="embed-model", provider="embed-prov"
197197
)
198-
invocation.input_tokens = 100 # Should NOT be recorded as token metric
198+
invocation.input_tokens = 100
199199
# Patch default_timer during start to ensure monotonic_start_s
200200
with patch("timeit.default_timer", return_value=1000.0):
201201
handler.start(invocation)
@@ -225,8 +225,16 @@ def test_stop_embedding_records_duration_only(self) -> None:
225225
)
226226
self.assertAlmostEqual(duration_point.sum, 1.5, places=3)
227227

228-
# Token metrics should NOT be recorded for embedding
229-
self.assertNotIn("gen_ai.client.token.usage", metrics)
228+
# Token metrics should be recorded for embedding (input only)
229+
self.assertIn("gen_ai.client.token.usage", metrics)
230+
token_points = metrics["gen_ai.client.token.usage"]
231+
self.assertEqual(len(token_points), 1) # Only input tokens
232+
token_point = token_points[0]
233+
self.assertEqual(
234+
token_point.attributes[GenAI.GEN_AI_TOKEN_TYPE],
235+
GenAI.GenAiTokenTypeValues.INPUT.value,
236+
)
237+
self.assertAlmostEqual(token_point.sum, 100.0, places=3)
230238

231239
def test_stop_embedding_records_duration_with_additional_attributes(
232240
self,
@@ -300,5 +308,26 @@ def test_fail_embedding_records_error_and_duration(self) -> None:
300308
)
301309
self.assertAlmostEqual(duration_point.sum, 2.5, places=3)
302310

303-
# Token metrics should NOT be recorded for embedding even on failure
311+
# Token metrics should NOT be recorded when input_tokens is not set
312+
self.assertNotIn("gen_ai.client.token.usage", metrics)
313+
314+
def test_stop_embedding_without_tokens(self) -> None:
315+
"""Verify embedding without input_tokens does not record token metrics."""
316+
handler = TelemetryHandler(
317+
tracer_provider=self.tracer_provider,
318+
meter_provider=self.meter_provider,
319+
)
320+
invocation = EmbeddingInvocation(
321+
request_model="embed-model", provider="embed-prov"
322+
)
323+
# input_tokens is not set
324+
handler.start(invocation)
325+
handler.stop(invocation)
326+
327+
metrics = self._harvest_metrics()
328+
329+
# Duration should be recorded
330+
self.assertIn("gen_ai.client.operation.duration", metrics)
331+
332+
# Token metrics should NOT be recorded when input_tokens is not set
304333
self.assertNotIn("gen_ai.client.token.usage", metrics)

0 commit comments

Comments
 (0)