Skip to content

Commit ac37dad

Browse files
committed
pre-commit fix
1 parent d08fa1c commit ac37dad

5 files changed

Lines changed: 96 additions & 98 deletions

File tree

util/opentelemetry-util-genai/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891](#3891))
2323
- Add parent class genAI invocation
2424
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889](#3889))
25+
- Add `EmbeddingInvocation` span lifecycle support (`start_embedding`,
26+
`stop_embedding`, `fail_embedding`), embedding span tests, and defer
27+
embedding metrics emission (current no-op) to a follow-up PR.
28+
([#<PR_NUMBER>](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/<PR_NUMBER>))
2529

2630
## Version 0.2b0 (2025-10-14)
2731

util/opentelemetry-util-genai/README.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ This package provides these span attributes:
3636
- `gen_ai.output.messages`: Str('[{"role": "AI", "parts": [{"content": "hello back", "type": "text"}], "finish_reason": "stop"}]')
3737
- `gen_ai.system_instructions`: Str('[{"content": "You are a helpful assistant.", "type": "text"}]') (when system instruction is provided)
3838

39+
This package also supports embedding invocation spans via
40+
`EmbeddingInvocation` and `TelemetryHandler.start_embedding/stop_embedding/fail_embedding`.
41+
For embedding invocations, common attributes include:
42+
43+
- `gen_ai.provider.name`: Str(openai)
44+
- `gen_ai.operation.name`: Str(embeddings)
45+
- `gen_ai.request.model`: Str(text-embedding-3-small)
46+
- `gen_ai.embedding.dimension_count`: Int(1536)
47+
- `gen_ai.request.encoding_formats`: Slice(["float"])
48+
- `gen_ai.usage.input_tokens`: Int(24)
49+
- `server.address`: Str(api.openai.com)
50+
- `server.port`: Int(443)
51+
3952
When `EVENT_ONLY` or `SPAN_AND_EVENT` mode is enabled and a LoggerProvider is configured,
4053
the package also emits `gen_ai.client.inference.operation.details` events with structured
4154
message content (as dictionaries instead of JSON strings). Note that when using `EVENT_ONLY`

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,15 @@
8080
)
8181
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
8282
from opentelemetry.util.genai.span_utils import (
83+
_apply_embedding_finish_attributes,
8384
_apply_error_attributes,
8485
_apply_llm_finish_attributes,
85-
_apply_embedding_finish_attributes,
8686
_maybe_emit_llm_event,
87-
_maybe_emit_embedding_event,
8887
)
8988
from opentelemetry.util.genai.types import (
89+
EmbeddingInvocation,
9090
Error,
9191
LLMInvocation,
92-
EmbeddingInvocation,
9392
)
9493
from opentelemetry.util.genai.version import __version__
9594

@@ -255,7 +254,6 @@ def stop_embedding(
255254
span = invocation.span
256255
_apply_embedding_finish_attributes(span, invocation)
257256
self._record_embedding_metrics(invocation, span)
258-
_maybe_emit_embedding_event(self._logger, span, invocation)
259257
# Detach context and end span
260258
otel_context.detach(invocation.context_token)
261259
span.end()
@@ -273,10 +271,7 @@ def fail_embedding(
273271
_apply_embedding_finish_attributes(invocation.span, invocation)
274272
_apply_error_attributes(invocation.span, error)
275273
error_type = getattr(error.type, "__qualname__", None)
276-
self._record_embedding_metrics(
277-
invocation, span, error_type=error_type
278-
)
279-
_maybe_emit_embedding_event(self._logger, span, invocation, error)
274+
self._record_embedding_metrics(invocation, span, error_type=error_type)
280275
# Detach context and end span
281276
otel_context.detach(invocation.context_token)
282277
span.end()

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,6 @@ class EmbeddingInvocation(GenAIInvocation):
285285
Additional attributes to set on metrics. Must be of a low cardinality.
286286
These attributes will not be set on spans or events.
287287
"""
288-
# Monotonic start time in seconds (from timeit.default_timer) used
289-
# for duration calculations to avoid mixing clock sources. This is
290-
# populated by the TelemetryHandler when starting an invocation.
291-
monotonic_start_s: float | None = None
292288

293289

294290
@dataclass

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

Lines changed: 76 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,82 @@ def test_parent_child_span_relationship(self):
501501
# Parent should not have a parent (root)
502502
assert parent_span.parent is None
503503

504+
@patch_env_vars(
505+
stability_mode="gen_ai_latest_experimental",
506+
content_capturing="SPAN_ONLY",
507+
emit_event="",
508+
)
509+
def test_embedding_parent_child_span_relationship(self):
510+
parent_invocation = EmbeddingInvocation(
511+
request_model="embed-parent-model",
512+
provider="test-provider",
513+
input_tokens=10,
514+
)
515+
child_invocation = EmbeddingInvocation(
516+
request_model="embed-child-model",
517+
provider="test-provider",
518+
input_tokens=5,
519+
)
520+
521+
self.telemetry_handler.start_embedding(parent_invocation)
522+
assert parent_invocation.span is not None
523+
self.telemetry_handler.start_embedding(child_invocation)
524+
assert child_invocation.span is not None
525+
self.telemetry_handler.stop_embedding(child_invocation)
526+
self.telemetry_handler.stop_embedding(parent_invocation)
527+
528+
spans = self.span_exporter.get_finished_spans()
529+
assert len(spans) == 2
530+
child_span = next(
531+
s for s in spans if s.name == "embeddings embed-child-model"
532+
)
533+
parent_span = next(
534+
s for s in spans if s.name == "embeddings embed-parent-model"
535+
)
536+
537+
assert child_span.context.trace_id == parent_span.context.trace_id
538+
assert child_span.parent is not None
539+
assert child_span.parent.span_id == parent_span.context.span_id
540+
assert parent_span.parent is None
541+
542+
@patch_env_vars(
543+
stability_mode="gen_ai_latest_experimental",
544+
content_capturing="SPAN_ONLY",
545+
emit_event="",
546+
)
547+
def test_llm_parent_embedding_child_span_relationship(self):
548+
message = _create_input_message("hi")
549+
chat_generation = _create_output_message("ok")
550+
child_invocation = EmbeddingInvocation(
551+
request_model="embed-child-model",
552+
provider="test-provider",
553+
input_tokens=3,
554+
)
555+
556+
with self.telemetry_handler.llm() as parent_invocation:
557+
for attr, value in {
558+
"request_model": "parent-model",
559+
"input_messages": [message],
560+
"provider": "test-provider",
561+
}.items():
562+
setattr(parent_invocation, attr, value)
563+
self.telemetry_handler.start_embedding(child_invocation)
564+
assert child_invocation.span is not None
565+
self.telemetry_handler.stop_embedding(child_invocation)
566+
parent_invocation.output_messages = [chat_generation]
567+
568+
spans = self.span_exporter.get_finished_spans()
569+
assert len(spans) == 2
570+
child_span = next(
571+
s for s in spans if s.name == "embeddings embed-child-model"
572+
)
573+
parent_span = next(s for s in spans if s.name == "chat parent-model")
574+
575+
assert child_span.context.trace_id == parent_span.context.trace_id
576+
assert child_span.parent is not None
577+
assert child_span.parent.span_id == parent_span.context.span_id
578+
assert parent_span.parent is None
579+
504580
def test_llm_context_manager_error_path_records_error_status_and_attrs(
505581
self,
506582
):
@@ -907,92 +983,6 @@ def test_embedding_manual_start_and_stop_creates_span(self):
907983
},
908984
)
909985

910-
@patch_env_vars(
911-
stability_mode="gen_ai_latest_experimental",
912-
content_capturing="EVENT_ONLY",
913-
emit_event="true",
914-
)
915-
def test_emits_embedding_event(self):
916-
invocation = EmbeddingInvocation(
917-
request_model="event-embed-model",
918-
provider="test-provider",
919-
dimension_count=1024,
920-
encoding_formats=["float"],
921-
input_tokens=10,
922-
server_address="event.server.com",
923-
server_port=8443,
924-
)
925-
926-
self.telemetry_handler.start_embedding(invocation)
927-
self.telemetry_handler.stop_embedding(invocation)
928-
929-
logs = self.log_exporter.get_finished_logs()
930-
self.assertEqual(len(logs), 1)
931-
log_record = logs[0].log_record
932-
self.assertEqual(
933-
log_record.event_name, "gen_ai.client.embedding.operation.details"
934-
)
935-
936-
attrs = log_record.attributes
937-
self.assertIsNotNone(attrs)
938-
self.assertEqual(attrs[GenAI.GEN_AI_OPERATION_NAME], "embeddings")
939-
self.assertEqual(
940-
attrs[GenAI.GEN_AI_REQUEST_MODEL], "event-embed-model"
941-
)
942-
self.assertEqual(attrs[GenAI.GEN_AI_PROVIDER_NAME], "test-provider")
943-
self.assertEqual(attrs[GenAI.GEN_AI_EMBEDDING_DIMENSION_COUNT], 1024)
944-
self.assertEqual(
945-
_normalize_to_list(attrs[GenAI.GEN_AI_REQUEST_ENCODING_FORMATS]),
946-
["float"],
947-
)
948-
self.assertEqual(attrs[GenAI.GEN_AI_USAGE_INPUT_TOKENS], 10)
949-
self.assertEqual(attrs[server_attributes.SERVER_ADDRESS], "event.server.com")
950-
self.assertEqual(attrs[server_attributes.SERVER_PORT], 8443)
951-
952-
span = _get_single_span(self.span_exporter)
953-
self.assertIsNotNone(log_record.trace_id)
954-
self.assertIsNotNone(log_record.span_id)
955-
self.assertIsNotNone(span.context)
956-
self.assertEqual(log_record.trace_id, span.context.trace_id)
957-
self.assertEqual(log_record.span_id, span.context.span_id)
958-
959-
@patch_env_vars(
960-
stability_mode="gen_ai_latest_experimental",
961-
content_capturing="EVENT_ONLY",
962-
emit_event="true",
963-
)
964-
def test_emits_embedding_event_with_error(self):
965-
class EmbeddingError(RuntimeError):
966-
pass
967-
968-
invocation = EmbeddingInvocation(
969-
request_model="error-embed-model",
970-
provider="test-provider",
971-
input_tokens=9,
972-
)
973-
self.telemetry_handler.start_embedding(invocation)
974-
error = Error(message="embedding error", type=EmbeddingError)
975-
self.telemetry_handler.fail_embedding(invocation, error)
976-
977-
logs = self.log_exporter.get_finished_logs()
978-
self.assertEqual(len(logs), 1)
979-
log_record = logs[0].log_record
980-
attrs = log_record.attributes
981-
self.assertEqual(
982-
attrs[error_attributes.ERROR_TYPE], EmbeddingError.__qualname__
983-
)
984-
self.assertEqual(attrs[GenAI.GEN_AI_OPERATION_NAME], "embeddings")
985-
self.assertEqual(
986-
attrs[GenAI.GEN_AI_REQUEST_MODEL], "error-embed-model"
987-
)
988-
989-
span = _get_single_span(self.span_exporter)
990-
self.assertIsNotNone(log_record.trace_id)
991-
self.assertIsNotNone(log_record.span_id)
992-
self.assertIsNotNone(span.context)
993-
self.assertEqual(log_record.trace_id, span.context.trace_id)
994-
self.assertEqual(log_record.span_id, span.context.span_id)
995-
996986

997987
class AnyNonNone:
998988
def __eq__(self, other):

0 commit comments

Comments
 (0)