Skip to content

Commit f9c081f

Browse files
committed
util-genai-inference-clean merge
1 parent 60a670f commit f9c081f

File tree

11 files changed

+705
-13
lines changed

11 files changed

+705
-13
lines changed

docs/nitpick-exceptions.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ py-class=
4545
psycopg.AsyncConnection
4646
ObjectProxy
4747
fastapi.applications.FastAPI
48+
_contextvars.Token
4849

4950
any=
5051
; API

util/opentelemetry-util-genai/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
([#3763](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3763))
1717
- Add a utility to parse the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable.
1818
Add `gen_ai_latest_experimental` as a new value to the Sem Conv stability flag ([#3716](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3716)).
19+
20+
### Added
21+
22+
- Generate Spans for LLM invocations
23+
- Helper functions for starting and finishing LLM invocations

util/opentelemetry-util-genai/README.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ The GenAI Utils package will include boilerplate and helpers to standardize inst
66
This package will provide APIs and decorators to minimize the work needed to instrument genai libraries,
77
while providing standardization for generating both types of otel, "spans and metrics" and "spans, metrics and events"
88

9+
This package relies on environment variables to configure capturing of message content.
10+
By default, message content will not be captured.
11+
Set the environment variable `OTEL_SEMCONV_STABILITY_OPT_IN` to `gen_ai_latest_experimental` to enable experimental features.
12+
And set the environment variable `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to `SPAN_ONLY` or `SPAN_AND_EVENT` to capture message content in spans.
13+
14+
This package provides these span attributes:
15+
16+
- `gen_ai.provider.name`: Str(openai)
17+
- `gen_ai.operation.name`: Str(chat)
18+
- `gen_ai.request.model`: Str(gpt-3.5-turbo)
19+
- `gen_ai.response.finish_reasons`: Slice(["stop"])
20+
- `gen_ai.response.model`: Str(gpt-3.5-turbo-0125)
21+
- `gen_ai.response.id`: Str(chatcmpl-Bz8yrvPnydD9pObv625n2CGBPHS13)
22+
- `gen_ai.usage.input_tokens`: Int(24)
23+
- `gen_ai.usage.output_tokens`: Int(7)
24+
- `gen_ai.input.messages`: Str('[{"role": "Human", "parts": [{"content": "hello world", "type": "text"}]}]')
25+
- `gen_ai.output.messages`: Str('[{"role": "AI", "parts": [{"content": "hello back", "type": "text"}], "finish_reason": "stop"}]')
26+
27+
928
Installation
1029
------------
1130

util/opentelemetry-util-genai/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dynamic = ["version"]
88
description = "OpenTelemetry GenAI Utils"
99
readme = "README.rst"
1010
license = "Apache-2.0"
11-
requires-python = ">=3.8"
11+
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
1414
]
@@ -25,8 +25,8 @@ classifiers = [
2525
"Programming Language :: Python :: 3.13",
2626
]
2727
dependencies = [
28-
"opentelemetry-instrumentation ~= 0.51b0",
29-
"opentelemetry-semantic-conventions ~= 0.51b0",
28+
"opentelemetry-instrumentation ~= 0.57b0",
29+
"opentelemetry-semantic-conventions ~= 0.57b0",
3030
"opentelemetry-api>=1.31.0",
3131
]
3232

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Span generation utilities for GenAI telemetry.
17+
18+
This module maps GenAI (Generative AI) invocations to OpenTelemetry spans and
19+
applies GenAI semantic convention attributes.
20+
21+
Classes:
22+
- BaseTelemetryGenerator: Abstract base for GenAI telemetry emitters.
23+
- SpanGenerator: Concrete implementation that creates and finalizes spans
24+
for LLM operations (e.g., chat) and records input/output messages when
25+
experimental mode and content capture settings allow.
26+
27+
Usage:
28+
See `opentelemetry/util/genai/handler.py` for `TelemetryHandler`, which
29+
constructs `LLMInvocation` objects and delegates to `SpanGenerator.start`,
30+
`SpanGenerator.finish`, and `SpanGenerator.error` to produce spans that
31+
follow the GenAI semantic conventions.
32+
"""
33+
34+
from typing import Any
35+
36+
from opentelemetry import context as otel_context
37+
from opentelemetry import trace
38+
from opentelemetry.semconv._incubating.attributes import (
39+
gen_ai_attributes as GenAI,
40+
)
41+
from opentelemetry.semconv.schemas import Schemas
42+
from opentelemetry.trace import (
43+
SpanKind,
44+
Tracer,
45+
get_tracer,
46+
set_span_in_context,
47+
)
48+
from opentelemetry.util.genai.span_utils import (
49+
_apply_error_attributes,
50+
_apply_finish_attributes,
51+
)
52+
from opentelemetry.util.genai.types import Error, LLMInvocation
53+
from opentelemetry.util.genai.version import __version__
54+
55+
56+
class BaseTelemetryGenerator:
57+
"""
58+
Abstract base for emitters mapping GenAI types -> OpenTelemetry.
59+
"""
60+
61+
def start(self, invocation: LLMInvocation) -> None:
62+
raise NotImplementedError
63+
64+
def finish(self, invocation: LLMInvocation) -> None:
65+
raise NotImplementedError
66+
67+
def error(self, error: Error, invocation: LLMInvocation) -> None:
68+
raise NotImplementedError
69+
70+
71+
class SpanGenerator(BaseTelemetryGenerator):
72+
"""
73+
Generates only spans.
74+
"""
75+
76+
def __init__(
77+
self,
78+
**kwargs: Any,
79+
):
80+
tracer_provider = kwargs.get("tracer_provider")
81+
tracer = get_tracer(
82+
__name__,
83+
__version__,
84+
tracer_provider,
85+
schema_url=Schemas.V1_36_0.value,
86+
)
87+
self._tracer: Tracer = tracer or trace.get_tracer(__name__)
88+
89+
def start(self, invocation: LLMInvocation):
90+
# Create a span and attach it as current; keep the token to detach later
91+
span = self._tracer.start_span(
92+
name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {invocation.request_model}",
93+
kind=SpanKind.CLIENT,
94+
)
95+
invocation.span = span
96+
invocation.context_token = otel_context.attach(
97+
set_span_in_context(span)
98+
)
99+
100+
def finish(self, invocation: LLMInvocation):
101+
if invocation.context_token is None or invocation.span is None:
102+
return
103+
104+
_apply_finish_attributes(invocation.span, invocation)
105+
# Detach context and end span
106+
otel_context.detach(invocation.context_token)
107+
invocation.span.end()
108+
109+
def error(self, error: Error, invocation: LLMInvocation):
110+
if invocation.context_token is None or invocation.span is None:
111+
return
112+
113+
_apply_error_attributes(invocation.span, error)
114+
# Detach context and end span
115+
otel_context.detach(invocation.context_token)
116+
invocation.span.end()
117+
return
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Telemetry handler for GenAI invocations.
17+
18+
This module exposes the `TelemetryHandler` class, which manages the lifecycle of
19+
GenAI (Generative AI) invocations and emits telemetry data (spans and related attributes).
20+
It supports starting, stopping, and failing LLM invocations.
21+
22+
Classes:
23+
- TelemetryHandler: Manages GenAI invocation lifecycles and emits telemetry.
24+
25+
Functions:
26+
- get_telemetry_handler: Returns a singleton `TelemetryHandler` instance.
27+
28+
Usage:
29+
handler = get_telemetry_handler()
30+
31+
# Create an invocation object with your request data
32+
# The span and context_token attributes are set by the TelemetryHandler, and
33+
# managed by the TelemetryHandler during the lifecycle of the span.
34+
35+
# Use the context manager to manage the lifecycle of an LLM invocation.
36+
with handler.llm(invocation) as invocation:
37+
# Populate outputs and any additional attributes
38+
invocation.output_messages = [...]
39+
invocation.attributes.update({"more": "attrs"})
40+
41+
# Or, if you prefer to manage the lifecycle manually
42+
invocation = LLMInvocation(
43+
request_model="my-model",
44+
input_messages=[...],
45+
provider="my-provider",
46+
attributes={"custom": "attr"},
47+
)
48+
49+
# Start the invocation (opens a span)
50+
handler.start_llm(invocation)
51+
52+
# Populate outputs and any additional attributes, then stop (closes the span)
53+
invocation.output_messages = [...]
54+
invocation.attributes.update({"more": "attrs"})
55+
handler.stop_llm(invocation)
56+
57+
# Or, in case of error
58+
handler.fail_llm(invocation, Error(type="...", message="..."))
59+
"""
60+
61+
import time
62+
from contextlib import contextmanager
63+
from typing import Any, Iterator, Optional
64+
65+
from opentelemetry.util.genai.generators import SpanGenerator
66+
from opentelemetry.util.genai.types import Error, LLMInvocation
67+
68+
69+
class TelemetryHandler:
70+
"""
71+
High-level handler managing GenAI invocation lifecycles and emitting
72+
them as spans, metrics, and events.
73+
"""
74+
75+
def __init__(self, **kwargs: Any):
76+
self._generator = SpanGenerator(**kwargs)
77+
78+
def start_llm(
79+
self,
80+
invocation: LLMInvocation,
81+
) -> LLMInvocation:
82+
"""Start an LLM invocation and create a pending span entry."""
83+
self._generator.start(invocation)
84+
return invocation
85+
86+
def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation:
87+
"""Finalize an LLM invocation successfully and end its span."""
88+
invocation.end_time = time.time()
89+
self._generator.finish(invocation)
90+
return invocation
91+
92+
def fail_llm(
93+
self, invocation: LLMInvocation, error: Error
94+
) -> LLMInvocation:
95+
"""Fail an LLM invocation and end its span with error status."""
96+
invocation.end_time = time.time()
97+
self._generator.error(error, invocation)
98+
return invocation
99+
100+
@contextmanager
101+
def llm(self, invocation: LLMInvocation) -> Iterator[LLMInvocation]:
102+
"""Context manager for LLM invocations.
103+
104+
Only set data attributes on the invocation object, do not modify the span or context.
105+
106+
Starts the span on entry. On normal exit, finalizes the invocation and ends the span.
107+
If an exception occurs inside the context, marks the span as error, ends it, and
108+
re-raises the original exception.
109+
"""
110+
self.start_llm(invocation)
111+
try:
112+
yield invocation
113+
except Exception as exc:
114+
self.fail_llm(invocation, Error(message=str(exc), type=type(exc)))
115+
raise
116+
self.stop_llm(invocation)
117+
118+
119+
def get_telemetry_handler(**kwargs: Any) -> TelemetryHandler:
120+
"""
121+
Returns a singleton TelemetryHandler instance.
122+
"""
123+
handler: Optional[TelemetryHandler] = getattr(
124+
get_telemetry_handler, "_default_handler", None
125+
)
126+
if handler is None:
127+
handler = TelemetryHandler(**kwargs)
128+
setattr(get_telemetry_handler, "_default_handler", handler)
129+
return handler

0 commit comments

Comments
 (0)