Skip to content

Commit baf3b95

Browse files
committed
review feedback
1 parent dc2cd6d commit baf3b95

13 files changed

Lines changed: 229 additions & 170 deletions

File tree

instrumentation-genai/AGENTS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ except Exception as exc:
3535

3636
## 3. Exception Handling
3737

38-
- Do not add `raise` statements in instrumentation/telemetry code — validation belongs in
38+
- Do not add `raise {Error}` statements in instrumentation/telemetry code — validation belongs in
3939
tests and callers, not in the instrumentation layer.
4040
- When catching exceptions from the underlying library to record telemetry, always re-raise
4141
the original exception unmodified.
4242
- Do not wrap, replace, or suppress exceptions — telemetry must be transparent to callers.
43-
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
from __future__ import annotations
16+
17+
import timeit
18+
from abc import ABC, abstractmethod
19+
from contextvars import Token
20+
from dataclasses import dataclass
21+
from typing import TYPE_CHECKING, Any, Type
22+
23+
from typing_extensions import TypeAlias
24+
25+
from opentelemetry._logs import Logger
26+
from opentelemetry.context import Context, attach, detach
27+
from opentelemetry.semconv.attributes import error_attributes
28+
from opentelemetry.trace import INVALID_SPAN as _INVALID_SPAN
29+
from opentelemetry.trace import Span, SpanKind, Tracer, set_span_in_context
30+
from opentelemetry.trace.status import Status, StatusCode
31+
32+
if TYPE_CHECKING:
33+
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
34+
35+
ContextToken: TypeAlias = Token[Context]
36+
37+
38+
@dataclass
39+
class Error:
40+
message: str
41+
type: Type[BaseException]
42+
43+
44+
class GenAIInvocation(ABC):
45+
"""
46+
Base class for all GenAI invocation types. Manages the lifecycle of a single
47+
GenAI operation (LLM call, embedding, tool execution, workflow, etc.).
48+
49+
Use the factory methods on TelemetryHandler (start_inference, start_embedding,
50+
start_workflow, start_tool) rather than constructing invocations directly.
51+
"""
52+
53+
def __init__(
54+
self,
55+
# Individual components instead of TelemetryHandler to avoid a circular
56+
# import between handler.py and the invocation modules.
57+
tracer: Tracer,
58+
metrics_recorder: InvocationMetricsRecorder,
59+
logger: Logger,
60+
operation_name: str,
61+
span_name: str,
62+
span_kind: SpanKind = SpanKind.CLIENT,
63+
attributes: dict[str, Any] | None = None,
64+
metric_attributes: dict[str, Any] | None = None,
65+
) -> None:
66+
self._tracer = tracer
67+
self._metrics_recorder = metrics_recorder
68+
self._logger = logger
69+
self._operation_name: str = operation_name
70+
self.attributes: dict[str, Any] = (
71+
{} if attributes is None else attributes
72+
)
73+
"""Additional attributes to set on spans and/or events. Not set on metrics."""
74+
self.metric_attributes: dict[str, Any] = (
75+
{} if metric_attributes is None else metric_attributes
76+
)
77+
"""Additional attributes to set on metrics. Must be low cardinality. Not set on spans or events."""
78+
self.span: Span = _INVALID_SPAN
79+
self._span_context: Context
80+
self._span_name: str = span_name
81+
self._span_kind: SpanKind = span_kind
82+
self._context_token: ContextToken | None = None
83+
self._monotonic_start_s: float | None = None
84+
85+
def _start(self) -> None:
86+
"""Start the invocation span and attach it to the current context."""
87+
self.span = self._tracer.start_span(
88+
name=self._span_name,
89+
kind=self._span_kind,
90+
)
91+
self._span_context = set_span_in_context(self.span)
92+
self._monotonic_start_s = timeit.default_timer()
93+
self._context_token = attach(self._span_context)
94+
95+
def _get_metric_attributes(self) -> dict[str, Any]:
96+
"""Return low-cardinality attributes for metric recording."""
97+
return self.metric_attributes
98+
99+
def _get_metric_token_counts(self) -> dict[str, int]: # pylint: disable=no-self-use
100+
"""Return {token_type: count} for token histogram recording."""
101+
return {}
102+
103+
def _apply_error_attributes(self, error: Error) -> None:
104+
"""Apply error status and error.type attribute to the span, events, and metrics."""
105+
error_type = error.type.__qualname__
106+
self.span.set_status(Status(StatusCode.ERROR, error.message))
107+
self.attributes[error_attributes.ERROR_TYPE] = error_type
108+
self.metric_attributes[error_attributes.ERROR_TYPE] = error_type
109+
110+
@abstractmethod
111+
def _apply_finish(self, error: Error | None = None) -> None:
112+
"""Apply finish telemetry (attributes, metrics, events)."""
113+
114+
def _finish(self, error: Error | None = None) -> None:
115+
"""Apply finish telemetry and end the span."""
116+
if self._context_token is None:
117+
return
118+
try:
119+
self._apply_finish(error)
120+
finally:
121+
try:
122+
detach(self._context_token)
123+
except Exception: # pylint: disable=broad-except
124+
pass
125+
self.span.end()
126+
127+
def stop(self) -> None:
128+
"""Finalize the invocation successfully and end its span."""
129+
self._finish()
130+
131+
def fail(self, error: Error | BaseException) -> None:
132+
"""Fail the invocation and end its span with error status."""
133+
if isinstance(error, BaseException):
134+
error = Error(type=type(error), message=str(error))
135+
self._finish(error)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
)
2323
from opentelemetry.semconv.attributes import server_attributes
2424
from opentelemetry.trace import SpanKind, Tracer
25+
from opentelemetry.util.genai._invocation import Error, GenAIInvocation
2526
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
26-
from opentelemetry.util.genai.types import Error, GenAIInvocation
2727
from opentelemetry.util.types import AttributeValue
2828

2929

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
handler = get_telemetry_handler()
2929
3030
# Factory method: construct and start in one call, then stop or fail.
31-
invocation = handler.start_inference("my-provider", "my-model")
31+
invocation = handler.start_inference("my-provider", request_model="my-model")
3232
invocation.input_messages = [...]
3333
invocation.temperature = 0.7
3434
try:
@@ -40,7 +40,7 @@
4040
raise
4141
4242
# Or use the context manager form — exception handling is automatic.
43-
with handler.inference("my-provider", "my-model") as invocation:
43+
with handler.inference("my-provider", request_model="my-model") as invocation:
4444
invocation.input_messages = [...]
4545
# ... call the underlying library ...
4646
invocation.output_messages = [...]
@@ -63,14 +63,14 @@
6363
TracerProvider,
6464
get_tracer,
6565
)
66+
from opentelemetry.util.genai._invocation import Error
6667
from opentelemetry.util.genai.embedding_invocation import EmbeddingInvocation
6768
from opentelemetry.util.genai.inference_invocation import (
6869
InferenceInvocation,
6970
LLMInvocation, # pyright: ignore[reportDeprecated]
7071
)
7172
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
7273
from opentelemetry.util.genai.tool_invocation import ToolInvocation
73-
from opentelemetry.util.genai.types import Error
7474
from opentelemetry.util.genai.version import __version__
7575
from opentelemetry.util.genai.workflow_invocation import WorkflowInvocation
7676

@@ -110,8 +110,8 @@ def __init__(
110110
def start_inference(
111111
self,
112112
provider: str,
113-
request_model: str | None = None,
114113
*,
114+
request_model: str | None = None,
115115
server_address: str | None = None,
116116
server_port: int | None = None,
117117
) -> InferenceInvocation:
@@ -150,8 +150,8 @@ def start_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pyright: ign
150150
def start_embedding(
151151
self,
152152
provider: str,
153-
request_model: str | None = None,
154153
*,
154+
request_model: str | None = None,
155155
server_address: str | None = None,
156156
server_port: int | None = None,
157157
) -> EmbeddingInvocation:
@@ -197,6 +197,7 @@ def start_tool(
197197

198198
def start_workflow(
199199
self,
200+
*,
200201
name: str | None = None,
201202
) -> WorkflowInvocation:
202203
"""Create and start a workflow invocation.
@@ -240,8 +241,8 @@ def fail_llm( # pylint: disable=no-self-use
240241
def inference(
241242
self,
242243
provider: str,
243-
request_model: str | None = None,
244244
*,
245+
request_model: str | None = None,
245246
server_address: str | None = None,
246247
server_port: int | None = None,
247248
) -> Iterator[InferenceInvocation]:
@@ -270,8 +271,8 @@ def inference(
270271
def embedding(
271272
self,
272273
provider: str,
273-
request_model: str | None = None,
274274
*,
275+
request_model: str | None = None,
275276
server_address: str | None = None,
276277
server_port: int | None = None,
277278
) -> Iterator[EmbeddingInvocation]:

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@
3030
Tracer,
3131
set_span_in_context,
3232
)
33+
from opentelemetry.util.genai._invocation import Error, GenAIInvocation
3334
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
3435
from opentelemetry.util.genai.types import (
35-
Error,
36-
GenAIInvocation,
3736
InputMessage,
3837
MessagePart,
3938
OutputMessage,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
"""Public re-export of all GenAI invocation types.
16+
17+
Users can import everything from this single module:
18+
19+
from opentelemetry.util.genai.invocation import (
20+
Error,
21+
GenAIInvocation,
22+
InferenceInvocation,
23+
EmbeddingInvocation,
24+
ToolInvocation,
25+
WorkflowInvocation,
26+
)
27+
"""
28+
29+
from opentelemetry.util.genai._invocation import (
30+
ContextToken,
31+
Error,
32+
GenAIInvocation,
33+
)
34+
from opentelemetry.util.genai.embedding_invocation import EmbeddingInvocation
35+
from opentelemetry.util.genai.inference_invocation import InferenceInvocation
36+
from opentelemetry.util.genai.tool_invocation import ToolInvocation
37+
from opentelemetry.util.genai.workflow_invocation import WorkflowInvocation
38+
39+
__all__ = [
40+
"ContextToken",
41+
"Error",
42+
"GenAIInvocation",
43+
"InferenceInvocation",
44+
"EmbeddingInvocation",
45+
"ToolInvocation",
46+
"WorkflowInvocation",
47+
]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
create_token_histogram,
1515
)
1616

17-
from .types import GenAIInvocation
17+
from ._invocation import GenAIInvocation
1818

1919

2020
class InvocationMetricsRecorder:

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616

1717
from typing import Any
1818

19-
from typing_extensions import deprecated
20-
2119
from opentelemetry._logs import Logger
2220
from opentelemetry.semconv._incubating.attributes import (
2321
gen_ai_attributes as GenAI,
2422
)
2523
from opentelemetry.trace import Tracer
24+
from opentelemetry.util.genai._invocation import Error, GenAIInvocation
2625
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
27-
from opentelemetry.util.genai.types import Error, GenAIInvocation
2826

2927

3028
class ToolInvocation(GenAIInvocation):
@@ -96,8 +94,3 @@ def _apply_finish(self, error: Error | None = None) -> None:
9694
}
9795
attributes.update(self.attributes)
9896
self.span.set_attributes(attributes)
99-
100-
101-
@deprecated("ToolCall is deprecated. Use ToolInvocation instead.")
102-
class ToolCall(ToolInvocation):
103-
pass

0 commit comments

Comments
 (0)