Skip to content

Commit b8ca943

Browse files
shuningcdependabot[bot]xrmxemdnetoKludex
authored
GenAI Utils | Adding Embedding metrics (#4377)
* linting fix * Updating changelog * Updating changelog PR number * Removing embedding events emission * Refatoring and adding input token metric for Embedding invocation * Merging and removing unused import * build(deps): bump aiohttp from 3.13.3 to 3.13.4 (#4386) --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.13.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Revert "Build list of required jobs in generate-workflow (#4326)" (#4413) This reverts commit 22879d6. Now that we have just one job to check we don't need to build the list anymore. * Drop Python 3.9 support (#4412) * Drop Python 3.9 support Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * generate-workflows Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fixes Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * remove extra reference to pypy310 Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * changelog Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix flask tests Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix google-genai tests Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix google-genai tests Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * remove unused _ensure_gzip_single_response Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> --------- Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> * Add AGENTS.md with project structure and commands (#4233) * Add CLAUDE.md with project structure and commands * Add AGENTS.md symlink to CLAUDE.md * Move guidance to AGENTS.md and address review feedback - Move main content from CLAUDE.md to AGENTS.md so all AI agents (not only Claude) pick up the guidance; CLAUDE.md now just references it via `@AGENTS.md`. - Add general rules, PR scoping, and `Assisted-by:` commit trailer guidance (inspired by the Collector's AGENTS.md). - Clarify that only instrumentation packages live under `src/opentelemetry/instrumentation/{name}/`; other package types use their own namespace. --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> * scripts: drop update_sha (#4430) It's buggy and unused. * Fix pylint false positives for ThreadPoolExecutor (#4244) * Bump pylint to 4.0.5 to fix Python 3.14 concurrent.futures false positives * Fix too-many-positional-arguments pylint failures - Add max-positional-arguments=10, Add pylint: disable=too-many-positional-arguments to functions that legitimately exceed the limit * Bump max-positional-arguments to 12 and remove unnecessary disable comments * Address review comments: fix CHANGELOG, remove stale pylintrc comment, add openai-agents disable * Fix formatting: restore blank line in CHANGELOG and remove extra blank line in .pylintrc * Update processor.py * Update test_botocore_bedrock.py --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> * feat(util-genai): refactor and make API smaller and more user-friendly (#4391) * Refactor public API on GenAI utils * more lint * review feedback * update tests to use named params * address some of the comments * up * fix failing checks and clean up imports * lint * lint * fix lint * replace @deprecated with docstring info to avoid warnings for users * up * common code for context manager * Adding metrics call for Embedding type after refactoring * Updating metrics tests with embedding * Adding fix for markdown-link-check --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Sri Kaaviya <107148069+srikaaviya@users.noreply.github.com> Co-authored-by: Liudmila Molkova <neskazu@gmail.com>
1 parent 498b928 commit b8ca943

File tree

3 files changed

+159
-16
lines changed

3 files changed

+159
-16
lines changed

util/opentelemetry-util-genai/CHANGELOG.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Add metrics support for EmbeddingInvocation
11+
([#4377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4377))
1012
- Add support for workflow in genAI utils handler.
11-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366](#4366))
13+
([#4366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366))
1214
- Enrich ToolCall type, breaking change: usage of ToolCall class renamed to ToolCallRequest
1315
([#4218](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4218))
1416
- Add EmbeddingInvocation span lifecycle support
15-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219](#4219))
17+
([#4219](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219))
1618
- Populate schema_url on metrics
1719
([#4320](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4320))
1820
- Add workflow invocation type to genAI utils
19-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4310](#4310))
21+
([#4310](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4310))
2022
- Check if upload works at startup in initializer of the `UploadCompletionHook`, instead
21-
of repeatedly failing on every upload ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4390](#4390)).
23+
of repeatedly failing on every upload ([#4390](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4390)).
2224
- Refactor public API: add factory methods (`start_inference`, `start_embedding`, `start_tool`, `start_workflow`) and invocation-owned lifecycle (`invocation.stop()` / `invocation.fail(exc)`); rename `LLMInvocation``InferenceInvocation` and `ToolCall``ToolInvocation`. Existing usages remain fully functional via deprecated aliases.
2325
([#4391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4391))
2426

@@ -32,28 +34,28 @@ of repeatedly failing on every upload ([https://github.com/open-telemetry/opente
3234
- Log error when `fsspec` fails to be imported instead of silently failing ([#4037](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4037)).
3335
- Minor change to check LRU cache in Completion Hook before acquiring semaphore/thread ([#3907](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3907)).
3436
- Add environment variable for genai upload hook queue size
35-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3943](#3943))
37+
([#3943](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3943))
3638
- Add more Semconv attributes to LLMInvocation spans.
37-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3862](#3862))
39+
([#3862](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3862))
3840
- Limit the upload hook thread pool to 64 workers
39-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3944](#3944))
41+
([#3944](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3944))
4042
- Add metrics to LLMInvocation traces
41-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891](#3891))
43+
([#3891](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891))
4244
- Add parent class genAI invocation
43-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889](#3889))
45+
([#3889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889))
4446

4547
## Version 0.2b0 (2025-10-14)
4648

4749
- Add jsonlines support to fsspec uploader
48-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3791](#3791))
50+
([#3791](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3791))
4951
- Rename "fsspec_upload" entry point and classes to more generic "upload"
50-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3798](#3798))
52+
([#3798](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3798))
5153
- Record content-type and use canonical paths in fsspec genai uploader
52-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3795](#3795))
54+
([#3795](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3795))
5355
- Make inputs / outputs / system instructions optional params to `on_completion`,
54-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3802](#3802)).
56+
([#3802](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3802)).
5557
- Use a SHA256 hash of the system instructions as it's upload filename, and check
56-
if the file exists before re-uploading it, ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814](#3814)).
58+
if the file exists before re-uploading it, ([#3814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814)).
5759

5860
## Version 0.1b0 (2025-09-25)
5961

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,4 @@ def _apply_finish(self, error: Error | None = None) -> None:
120120
self._apply_error_attributes(error)
121121
attributes.update(self.attributes)
122122
self.span.set_attributes(attributes)
123-
# Metrics recorder currently supports InferenceInvocation fields only.
124-
# No-op until dedicated embedding metric support is added.
123+
self._metrics_recorder.record(self)

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,145 @@ def _assert_metric_scope_schema_urls(
181181
self.assertEqual(
182182
scope_metric.scope.schema_url, expected_schema_url
183183
)
184+
185+
def test_stop_embedding_records_duration_and_tokens(self) -> None:
186+
"""Verify embedding invocations record duration and input token metrics."""
187+
handler = TelemetryHandler(
188+
tracer_provider=self.tracer_provider,
189+
meter_provider=self.meter_provider,
190+
)
191+
# Patch default_timer during start to ensure monotonic_start_s
192+
with patch("timeit.default_timer", return_value=1000.0):
193+
invocation = handler.start_embedding(
194+
"embed-prov", request_model="embed-model"
195+
)
196+
invocation.input_tokens = 100
197+
198+
# Simulate 1.5 seconds of elapsed monotonic time
199+
with patch("timeit.default_timer", return_value=1001.5):
200+
invocation.stop()
201+
202+
self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL)
203+
metrics = self._harvest_metrics()
204+
205+
# Duration should be recorded
206+
self.assertIn("gen_ai.client.operation.duration", metrics)
207+
duration_points = metrics["gen_ai.client.operation.duration"]
208+
self.assertEqual(len(duration_points), 1)
209+
duration_point = duration_points[0]
210+
self.assertEqual(
211+
duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME],
212+
GenAI.GenAiOperationNameValues.EMBEDDINGS.value,
213+
)
214+
self.assertEqual(
215+
duration_point.attributes[GenAI.GEN_AI_REQUEST_MODEL],
216+
"embed-model",
217+
)
218+
self.assertEqual(
219+
duration_point.attributes[GenAI.GEN_AI_PROVIDER_NAME], "embed-prov"
220+
)
221+
self.assertAlmostEqual(duration_point.sum, 1.5, places=3)
222+
223+
# Token metrics should be recorded for embedding (input only)
224+
self.assertIn("gen_ai.client.token.usage", metrics)
225+
token_points = metrics["gen_ai.client.token.usage"]
226+
self.assertEqual(len(token_points), 1) # Only input tokens
227+
token_point = token_points[0]
228+
self.assertEqual(
229+
token_point.attributes[GenAI.GEN_AI_TOKEN_TYPE],
230+
GenAI.GenAiTokenTypeValues.INPUT.value,
231+
)
232+
self.assertAlmostEqual(token_point.sum, 100.0, places=3)
233+
234+
def test_stop_embedding_records_duration_with_additional_attributes(
235+
self,
236+
) -> None:
237+
"""Verify embedding metrics include server and custom attributes."""
238+
handler = TelemetryHandler(
239+
tracer_provider=self.tracer_provider,
240+
meter_provider=self.meter_provider,
241+
)
242+
invocation = handler.start_embedding(
243+
"embed-prov",
244+
request_model="embed-model",
245+
server_address="embed.server.com",
246+
server_port=8080,
247+
)
248+
invocation.metric_attributes = {"custom.embed.attr": "embed_value"}
249+
invocation.response_model_name = "embed-response-model"
250+
invocation.stop()
251+
252+
self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL)
253+
metrics = self._harvest_metrics()
254+
255+
self.assertIn("gen_ai.client.operation.duration", metrics)
256+
duration_points = metrics["gen_ai.client.operation.duration"]
257+
self.assertEqual(len(duration_points), 1)
258+
duration_point = duration_points[0]
259+
260+
self.assertEqual(
261+
duration_point.attributes["server.address"], "embed.server.com"
262+
)
263+
self.assertEqual(duration_point.attributes["server.port"], 8080)
264+
self.assertEqual(
265+
duration_point.attributes["custom.embed.attr"], "embed_value"
266+
)
267+
self.assertEqual(
268+
duration_point.attributes[GenAI.GEN_AI_RESPONSE_MODEL],
269+
"embed-response-model",
270+
)
271+
272+
def test_fail_embedding_records_error_and_duration(self) -> None:
273+
"""Verify embedding failure records error type and duration."""
274+
handler = TelemetryHandler(
275+
tracer_provider=self.tracer_provider,
276+
meter_provider=self.meter_provider,
277+
)
278+
with patch("timeit.default_timer", return_value=3000.0):
279+
invocation = handler.start_embedding(
280+
"embed-prov", request_model="embed-err-model"
281+
)
282+
283+
error = Error(message="embedding failed", type=RuntimeError)
284+
with patch("timeit.default_timer", return_value=3002.5):
285+
invocation.fail(error)
286+
287+
self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL)
288+
metrics = self._harvest_metrics()
289+
290+
self.assertIn("gen_ai.client.operation.duration", metrics)
291+
duration_points = metrics["gen_ai.client.operation.duration"]
292+
self.assertEqual(len(duration_points), 1)
293+
duration_point = duration_points[0]
294+
295+
self.assertEqual(
296+
duration_point.attributes.get("error.type"), "RuntimeError"
297+
)
298+
self.assertEqual(
299+
duration_point.attributes.get(GenAI.GEN_AI_REQUEST_MODEL),
300+
"embed-err-model",
301+
)
302+
self.assertAlmostEqual(duration_point.sum, 2.5, places=3)
303+
304+
# Token metrics should NOT be recorded when input_tokens is not set
305+
self.assertNotIn("gen_ai.client.token.usage", metrics)
306+
307+
def test_stop_embedding_without_tokens(self) -> None:
308+
"""Verify embedding without input_tokens does not record token metrics."""
309+
handler = TelemetryHandler(
310+
tracer_provider=self.tracer_provider,
311+
meter_provider=self.meter_provider,
312+
)
313+
invocation = handler.start_embedding(
314+
"embed-prov", request_model="embed-model"
315+
)
316+
# input_tokens is not set
317+
invocation.stop()
318+
319+
metrics = self._harvest_metrics()
320+
321+
# Duration should be recorded
322+
self.assertIn("gen_ai.client.operation.duration", metrics)
323+
324+
# Token metrics should NOT be recorded when input_tokens is not set
325+
self.assertNotIn("gen_ai.client.token.usage", metrics)

0 commit comments

Comments
 (0)