Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f8656f0
Add embedding invocation type
shuningc Feb 19, 2026
69d5598
pre-commit fix
shuningc Feb 19, 2026
c202f8b
Reverting unnecessary changes and dealing with comments
shuningc Feb 19, 2026
a799410
Dealing with comments
shuningc Feb 20, 2026
e16f49f
Refactoring to remove duplicate codes
shuningc Feb 25, 2026
3a4577c
Moving common attributes to GenAIInvocation
shuningc Feb 25, 2026
8e388c0
Apply suggestion from @xrmx
shuningc Mar 2, 2026
cafa8e3
Require virtualenv<21 for tox -e generate (#4265)
xrmx Feb 26, 2026
c969470
fix(threading): attribute error when run is called w/o start (#4246)
dinmukhamedm Feb 26, 2026
41166d8
[google-genai] Test instrumentation on google-genai v1.64.0 (#4253)
Rima-ag Feb 27, 2026
a026ef6
confluent-kafka: add basic autoinstrumentation tests (#4266)
xrmx Feb 27, 2026
1855b3a
instrumentation-genai: stop setting OTEL_PYTHON_LOGGING_AUTO_INSTRUME…
xrmx Feb 27, 2026
9989958
Add Keith Decker to approvers (#4273)
xrmx Mar 2, 2026
6ec1336
asyncio: fix environment variables not appearing in docs (#4261)
srikaaviya Mar 2, 2026
b71eef0
maint: Add stale github action (#4220)
MikeGoldsmith Mar 2, 2026
1bc4001
fix(flask): align http.server.active_requests metric with semconv hel…
rite7sh Mar 2, 2026
5d5b7dd
Fix falcon-instrumentation _handle_exception method to remove pylint …
srikaaviya Mar 2, 2026
2dc8bfb
gen-requirements: drop virtualenv limit (#4271)
xrmx Mar 2, 2026
45fa111
Fix psycopg2 (un)instrument_connection to use weakref, not mutate con…
tammy-baylis-swi Mar 3, 2026
8e64458
Move logger handlers to opentelemetry-instrumentation-logging (#4210)
xrmx Mar 3, 2026
5fc5874
OpenAI v2 onboard onto semantic conventions 1.37.0: chat history and …
lmolkova Mar 3, 2026
9b71c46
CHANGELOG: fix stale action entry (#4291)
xrmx Mar 4, 2026
a2612ba
Update version to 1.41.0.dev/0.62b0.dev (#4294)
otelbot[bot] Mar 4, 2026
da7d578
eachdist: add missing genai packages in independently released packag…
xrmx Mar 4, 2026
76f40dd
Refactoring attributes for embedding type
shuningc Mar 5, 2026
312c697
Merge branch 'main' into adding-embedding-type
shuningc Mar 5, 2026
1384bfd
Removing unnecessary comments
shuningc Mar 5, 2026
026f05f
Merge branch 'main' into adding-embedding-type
shuningc Mar 5, 2026
1621711
Adding error path unit test, moving operation name back to each invoc…
shuningc Mar 9, 2026
d49085c
Making operation name immutable for each type
shuningc Mar 9, 2026
bb21996
Revert "Making operation name immutable for each type"
shuningc Mar 10, 2026
cfbf30f
Merge branch 'main' into adding-embedding-type
shuningc Mar 10, 2026
7bc4233
Updating dependencies for different instrumentation packages, fixing …
shuningc Mar 12, 2026
753fee2
Merge branch 'main' into adding-embedding-type
shuningc Mar 12, 2026
7ab292d
Fixing conflict
shuningc Mar 12, 2026
28a7d93
Fixing failed tests out of dependencies and typo
shuningc Mar 12, 2026
3a9045d
Fixing failed tests by splitting large files and updating dependencies
shuningc Mar 12, 2026
9b78bc2
Keeping only generic start,stop and fail methods for all invocation t…
shuningc Mar 12, 2026
1e5f50c
Simplified error type calculation and refactor tests
shuningc Mar 12, 2026
0238075
Merge branch 'main' into adding-embedding-type
shuningc Mar 13, 2026
f7415f8
Merge branch 'main' into adding-embedding-type
shuningc Mar 17, 2026
dbce15f
Reverting start_[type] methods removals, fixing some related tests
shuningc Mar 19, 2026
a58b540
Removing start_embedding, stop_embedding and fail_embedding methods
shuningc Mar 19, 2026
6dfa5f0
Adding one missing change
shuningc Mar 19, 2026
6684f5a
Merge branch 'main' into adding-embedding-type
shuningc Mar 19, 2026
c5eb2f5
ruff format fix
shuningc Mar 19, 2026
0b93488
Merge branch 'main' into adding-embedding-type
xrmx Mar 20, 2026
c5bb5c6
Adding one format fix
shuningc Mar 20, 2026
7aae7e3
Merge branch 'main' into adding-embedding-type
keith-decker Mar 24, 2026
6a9875c
Merge branch 'main' into adding-embedding-type
shuningc Mar 25, 2026
742812a
Reverting opentelemetry-instrumentation version to 0.60b.0 and opente…
shuningc Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions util/opentelemetry-util-genai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891](#3891))
- Add parent class genAI invocation
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889](#3889))
- Add EmbeddingInvocation span lifecycle support
Comment thread
shuningc marked this conversation as resolved.
Outdated
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219](#4219))

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

Expand All @@ -37,8 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Make inputs / outputs / system instructions optional params to `on_completion`,
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3802](#3802)).
- Use a SHA256 hash of the system instructions as it's upload filename, and check
Comment thread
shuningc marked this conversation as resolved.
Outdated
if the file exists before re-uploading it, ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814](#3814)).

if the file exists before re-uploading it, ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814](#3814)).

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

Expand Down
13 changes: 13 additions & 0 deletions util/opentelemetry-util-genai/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ This package provides these span attributes:
- `gen_ai.output.messages`: Str('[{"role": "AI", "parts": [{"content": "hello back", "type": "text"}], "finish_reason": "stop"}]')
- `gen_ai.system_instructions`: Str('[{"content": "You are a helpful assistant.", "type": "text"}]') (when system instruction is provided)

This package also supports embedding invocation spans via
`EmbeddingInvocation` and `TelemetryHandler.start_embedding/stop_embedding/fail_embedding`.
For embedding invocations, common attributes include:

- `gen_ai.provider.name`: Str(openai)
- `gen_ai.operation.name`: Str(embeddings)
- `gen_ai.request.model`: Str(text-embedding-3-small)
- `gen_ai.embeddings.dimension.count`: Int(1536)
- `gen_ai.request.encoding_formats`: Slice(["float"])
- `gen_ai.usage.input_tokens`: Int(24)
- `server.address`: Str(api.openai.com)
- `server.port`: Int(443)

When `EVENT_ONLY` or `SPAN_AND_EVENT` mode is enabled and a LoggerProvider is configured,
the package also emits `gen_ai.client.inference.operation.details` events with structured
message content (as dictionaries instead of JSON strings). Note that when using `EVENT_ONLY`
Expand Down
9 changes: 3 additions & 6 deletions util/opentelemetry-util-genai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"opentelemetry-instrumentation ~= 0.58b0",
"opentelemetry-semantic-conventions ~= 0.58b0",
"opentelemetry-instrumentation ~= 0.60b0",
"opentelemetry-semantic-conventions ~= 0.60b0",
"opentelemetry-api>=1.31.0",
]

Expand All @@ -46,10 +46,7 @@ Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"
path = "src/opentelemetry/util/genai/version.py"

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
]
include = ["/src", "/tests"]

[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]
183 changes: 155 additions & 28 deletions util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

import timeit
from contextlib import contextmanager
from typing import Iterator
from typing import Iterator, TypeVar

from opentelemetry import context as otel_context
from opentelemetry._logs import (
Expand All @@ -78,15 +78,24 @@
get_tracer,
set_span_in_context,
)
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
from opentelemetry.util.genai.span_utils import (
_apply_embedding_finish_attributes,
_apply_error_attributes,
_apply_llm_finish_attributes,
_maybe_emit_llm_event,
)
from opentelemetry.util.genai.types import Error, LLMInvocation
from opentelemetry.util.genai.types import (
EmbeddingInvocation,
Error,
GenAIInvocation,
LLMInvocation,
)
Comment thread
shuningc marked this conversation as resolved.
from opentelemetry.util.genai.version import __version__

_T = TypeVar("_T", bound=GenAIInvocation)


class TelemetryHandler:
"""
Expand Down Expand Up @@ -131,14 +140,22 @@ def _record_llm_metrics(
error_type=error_type,
)

def start_llm(
def _record_embedding_metrics(
self,
invocation: LLMInvocation,
) -> LLMInvocation:
"""Start an LLM invocation and create a pending span entry."""
# Create a span and attach it as current; keep the token to detach later
invocation: EmbeddingInvocation,
span: Span | None = None,
*,
error_type: str | None = None,
) -> None:
# Metrics recorder currently supports LLMInvocation fields only.
# Keep embedding metrics as a no-op until dedicated embedding
# metric support is added.
return

def _start(self, invocation: _T) -> _T:
"""Start a GenAI invocation and create a pending span entry."""
span = self._tracer.start_span(
name=f"{invocation.operation_name} {invocation.request_model}",
name=invocation.operation_name or "",
Comment thread
shuningc marked this conversation as resolved.
Outdated
kind=SpanKind.CLIENT,
)
# Record a monotonic start timestamp (seconds) for duration
Expand All @@ -150,40 +167,108 @@ def start_llm(
)
return invocation

def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pylint: disable=no-self-use
"""Finalize an LLM invocation successfully and end its span."""
def _stop(self, invocation: _T) -> _T:
"""Finalize a GenAI invocation successfully and end its span."""
if invocation.context_token is None or invocation.span is None:
# TODO: Provide feedback that this invocation was not started
return invocation

span = invocation.span
_apply_llm_finish_attributes(span, invocation)
self._record_llm_metrics(invocation, span)
_maybe_emit_llm_event(self._logger, span, invocation)
# Detach context and end span
otel_context.detach(invocation.context_token)
span.end()
try:
if isinstance(invocation, LLMInvocation):
_apply_llm_finish_attributes(span, invocation)
self._record_llm_metrics(invocation, span)
_maybe_emit_llm_event(self._logger, span, invocation)
elif isinstance(invocation, EmbeddingInvocation):
_apply_embedding_finish_attributes(span, invocation)
self._record_embedding_metrics(invocation, span)
else:
span.set_status(
Status(
StatusCode.ERROR,
f"Unsupported invocation type: {type(invocation)!r}",
)
)
Comment thread
shuningc marked this conversation as resolved.
Outdated
raise TypeError(
Comment thread
shuningc marked this conversation as resolved.
Outdated
f"Unsupported invocation type: {type(invocation)!r}"
)
finally:
# Detach context and end span even if finishing fails
otel_context.detach(invocation.context_token)
span.end()
return invocation

def fail_llm( # pylint: disable=no-self-use
self, invocation: LLMInvocation, error: Error
) -> LLMInvocation:
"""Fail an LLM invocation and end its span with error status."""
def _fail(self, invocation: _T, error: Error) -> _T:
"""Fail a GenAI invocation and end its span with error status."""
if invocation.context_token is None or invocation.span is None:
# TODO: Provide feedback that this invocation was not started
return invocation

span = invocation.span
_apply_llm_finish_attributes(invocation.span, invocation)
_apply_error_attributes(invocation.span, error)
error_type = getattr(error.type, "__qualname__", None)
self._record_llm_metrics(invocation, span, error_type=error_type)
_maybe_emit_llm_event(self._logger, span, invocation, error)
# Detach context and end span
otel_context.detach(invocation.context_token)
span.end()
try:
if isinstance(invocation, LLMInvocation):
_apply_llm_finish_attributes(span, invocation)
_apply_error_attributes(span, error)
error_type = getattr(error.type, "__qualname__", None)
self._record_llm_metrics(
invocation, span, error_type=error_type
)
_maybe_emit_llm_event(self._logger, span, invocation, error)
elif isinstance(invocation, EmbeddingInvocation):
_apply_embedding_finish_attributes(span, invocation)
_apply_error_attributes(span, error)
error_type = getattr(error.type, "__qualname__", None)
Comment thread
shuningc marked this conversation as resolved.
Outdated
self._record_embedding_metrics(
invocation, span, error_type=error_type
)
else:
span.set_status(
Status(
StatusCode.ERROR,
f"Unsupported invocation type: {type(invocation)!r}",
)
)
raise TypeError(
f"Unsupported invocation type: {type(invocation)!r}"
)
Comment thread
shuningc marked this conversation as resolved.
Outdated
finally:
# Detach context and end span even if finishing fails
otel_context.detach(invocation.context_token)
span.end()
return invocation

def start(
self,
invocation: _T,
) -> _T:
"""Start a GenAI invocation and create a pending span entry."""
return self._start(invocation)

def stop(self, invocation: _T) -> _T:
"""Finalize a GenAI invocation successfully and end its span."""
return self._stop(invocation)

def fail(self, invocation: _T, error: Error) -> _T:
"""Fail a GenAI invocation and end its span with error status."""
return self._fail(invocation, error)

def start_llm(
self,
invocation: LLMInvocation,
) -> LLMInvocation:
"""Start an LLM invocation and create a pending span entry."""
return self.start(invocation)

def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation:
Comment thread
shuningc marked this conversation as resolved.
"""Finalize an LLM invocation successfully and end its span."""
return self.stop(invocation)

def fail_llm(
Comment thread
shuningc marked this conversation as resolved.
self, invocation: LLMInvocation, error: Error
) -> LLMInvocation:
"""Fail an LLM invocation and end its span with error status."""
return self.fail(invocation, error)

@contextmanager
def llm(
self, invocation: LLMInvocation | None = None
Expand All @@ -208,6 +293,48 @@ def llm(
raise
self.stop_llm(invocation)

@contextmanager
def embedding(
self, invocation: EmbeddingInvocation | None = None
) -> Iterator[EmbeddingInvocation]:
"""Context manager for Embedding invocations.

Only set data attributes on the invocation object, do not modify the span or context.

Starts the span on entry. On normal exit, finalizes the invocation and ends the span.
If an exception occurs inside the context, marks the span as error, ends it, and
re-raises the original exception.
"""
if invocation is None:
invocation = EmbeddingInvocation()
self.start_embedding(invocation)
try:
yield invocation
except Exception as exc:
self.fail_embedding(
invocation, Error(message=str(exc), type=type(exc))
)
raise
self.stop_embedding(invocation)

def start_embedding(
self, invocation: EmbeddingInvocation
) -> EmbeddingInvocation:
"""Start an embedding invocation and create a pending span entry."""
return self.start(invocation)

def stop_embedding(
Comment thread
shuningc marked this conversation as resolved.
Outdated
self, invocation: EmbeddingInvocation
) -> EmbeddingInvocation:
"""Finalize an embedding invocation successfully and end its span."""
return self.stop(invocation)

def fail_embedding(
Comment thread
shuningc marked this conversation as resolved.
Outdated
self, invocation: EmbeddingInvocation, error: Error
) -> EmbeddingInvocation:
"""Fail an embedding invocation and end its span with error status."""
return self.fail(invocation, error)


def get_telemetry_handler(
tracer_provider: TracerProvider | None = None,
Expand Down
Loading
Loading