Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,5 @@ target

# opentelemetry-admin jobs
opentelemetry-admin-jobs.txt

.claude/settings.local.json
41 changes: 41 additions & 0 deletions instrumentation-genai/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# GenAI Instrumentation — Agent and Contributor Guidelines

Instrumentation packages here wrap specific libraries (OpenAI, Anthropic, etc.) and bridge
them to the shared telemetry layer in `util/opentelemetry-util-genai`.

## 1. Instrumentation Layer Boundary

Do not call OpenTelemetry APIs (`tracer`, `meter`, `span`, event APIs) directly.
Always go through `TelemetryHandler` and the invocation objects it returns.

This layer is responsible only for:

- Patching the library
- Parsing library-specific input/output into invocation fields

Everything else (span creation, metric recording, event emission, context propagation)
belongs in `util/opentelemetry-util-genai`.

## 2. Invocation Pattern

Use `start_*()` and control span lifetime manually:

```python
invocation = handler.start_inference(provider, request_model, server_address=..., server_port=...)
invocation.temperature = ...
try:
response = client.call(...)
invocation.response_model_name = response.model
invocation.finish_reasons = response.finish_reasons
invocation.stop()
except Exception as exc:
invocation.fail(exc)
raise
```

## 3. Exception Handling

- Do not add `raise {Error}` statements in instrumentation/telemetry code — validation belongs in
tests and callers, not in the instrumentation layer.
- When catching exceptions from the underlying library to record telemetry, always re-raise
the original exception unmodified.
1 change: 1 addition & 0 deletions instrumentation-genai/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.util.genai.handler import TelemetryHandler
from opentelemetry.util.genai.types import Error, LLMInvocation
from opentelemetry.util.genai.types import (
Error,
LLMInvocation, # TODO: migrate to InferenceInvocation
)
from opentelemetry.util.genai.utils import (
should_capture_content_on_spans_in_experimental_mode,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from opentelemetry.util.genai.handler import TelemetryHandler
from opentelemetry.util.genai.types import (
Error,
LLMInvocation,
LLMInvocation, # TODO: migrate to InferenceInvocation
)

from .messages_extractors import set_invocation_response_attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from opentelemetry.util.genai.types import (
Error,
InputMessage,
LLMInvocation,
LLMInvocation, # TODO: migrate to InferenceInvocation
MessagePart,
OutputMessage,
Text,
Expand Down Expand Up @@ -158,7 +158,7 @@ def on_chat_model_start(
self._invocation_manager.add_invocation_state(
run_id=run_id,
parent_run_id=parent_run_id,
invocation=llm_invocation,
invocation=llm_invocation, # pyright: ignore[reportArgumentType]
)

def on_llm_end(
Expand All @@ -171,7 +171,8 @@ def on_llm_end(
) -> None:
llm_invocation = self._invocation_manager.get_invocation(run_id=run_id)
if llm_invocation is None or not isinstance(
llm_invocation, LLMInvocation
llm_invocation,
LLMInvocation,
):
# If the invocation does not exist, we cannot set attributes or end it
return
Expand Down Expand Up @@ -262,7 +263,8 @@ def on_llm_error(
) -> None:
llm_invocation = self._invocation_manager.get_invocation(run_id=run_id)
if llm_invocation is None or not isinstance(
llm_invocation, LLMInvocation
llm_invocation,
LLMInvocation,
):
# If the invocation does not exist, we cannot set attributes or end it
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"opentelemetry-api >= 1.37",
"opentelemetry-api >= 1.39",
"opentelemetry-instrumentation >= 0.58b0",
"opentelemetry-semantic-conventions >= 0.58b0",
"opentelemetry-util-genai"
"opentelemetry-semantic-conventions >= 0.60b0",
"opentelemetry-util-genai >= 0.4b0.dev"
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ grpcio>=1.60.0; implementation_name != "pypy"
grpcio<1.60.0; implementation_name == "pypy"

-e opentelemetry-instrumentation
-e util/opentelemetry-util-genai
-e instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ pytest-asyncio==0.21.0
wrapt==1.16.0
opentelemetry-exporter-otlp-proto-grpc~=1.30
opentelemetry-exporter-otlp-proto-http~=1.30
opentelemetry-api==1.37 # when updating, also update in pyproject.toml
opentelemetry-sdk==1.37 # when updating, also update in pyproject.toml
opentelemetry-semantic-conventions==0.58b0 # when updating, also update in pyproject.toml
opentelemetry-api==1.39 # when updating, also update in pyproject.toml
opentelemetry-sdk==1.39 # when updating, also update in pyproject.toml
opentelemetry-semantic-conventions==0.60b0 # when updating, also update in pyproject.toml
grpcio>=1.60.0; implementation_name != "pypy"
grpcio<1.60.0; implementation_name == "pypy"

-e util/opentelemetry-util-genai
-e instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"opentelemetry-api ~= 1.39",
"opentelemetry-instrumentation ~= 0.60b0",
"opentelemetry-semantic-conventions ~= 0.60b0",
"opentelemetry-util-genai",
"opentelemetry-util-genai >= 0.4b0.dev",
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from opentelemetry.util.genai.types import (
ContentCapturingMode,
Error,
LLMInvocation,
LLMInvocation, # pylint: disable=no-name-in-module # TODO: migrate to InferenceInvocation
OutputMessage,
Text,
ToolCallRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from typing import TYPE_CHECKING, Callable, Generator, Generic, TypeVar

from opentelemetry.util.genai.handler import TelemetryHandler
from opentelemetry.util.genai.types import Error, LLMInvocation
from opentelemetry.util.genai.types import (
Error,
LLMInvocation, # pylint: disable=no-name-in-module # TODO: migrate to InferenceInvocation
)

# OpenAI Responses internals are version-gated (added in openai>=1.66.0), so
# pylint may not resolve them in all lint environments even though we guard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util.genai.types import (
InputMessage,
LLMInvocation,
LLMInvocation, # pylint: disable=no-name-in-module # TODO: migrate to InferenceInvocation
OutputMessage,
Text,
ToolCallRequest,
Expand Down
80 changes: 80 additions & 0 deletions util/opentelemetry-util-genai/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# GenAI Utils — Agent and Contributor Guidelines

This package provides shared telemetry utilities for OpenTelemetry GenAI instrumentation.

## 1. Semantic Convention Compliance

All attributes, operation names, and span names must match the
[OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/)
exactly.

Use the appropriate semconv attribute modules — do not hardcode attribute name strings:

- `gen_ai.*` attributes: `opentelemetry.semconv._incubating.attributes.gen_ai_attributes`
- `server.*` attributes: `opentelemetry.semconv.attributes.server_attributes`
- `error.*` attributes: `opentelemetry.semconv.attributes.error_attributes`
- Other namespaces: use the corresponding module from `opentelemetry.semconv`

## 2. Invocation Lifecycle Pattern

Every new operation type must follow this pattern:

```python
invocation = handler.start_inference(provider, request_model, server_address=..., server_port=...)
invocation.temperature = ...
try:
response = client.call(...)
invocation.response_model_name = response.model
invocation.finish_reasons = response.finish_reasons
invocation.stop()
except Exception as exc:
invocation.fail(exc)
raise
```

Factory methods on `TelemetryHandler` (`handler.py`):

- `start_inference(provider, request_model, *, server_address, server_port)` → `InferenceInvocation`
- `start_embedding(provider, request_model, *, server_address, server_port)` → `EmbeddingInvocation`
- `start_tool(name, *, arguments, tool_call_id, tool_type, tool_description)` → `ToolInvocation`
- `start_workflow(name)` → `WorkflowInvocation`

Context manager equivalents (`handler.inference()`, `handler.embedding()`, `handler.tool()`,
`handler.workflow()`) are available when the span lifetime maps cleanly to a `with` block.

### Anti-patterns

**Never construct invocation types directly** (`InferenceInvocation(...)`, `ToolInvocation(...)`,
etc.) in instrumentation or production code — direct construction skips span creation and context
propagation, so all telemetry calls become no-ops. Always use `handler.start_*()`.

## 3. Exception Handling

- Do not add `raise {Error}` statements to `handler.py` or `types.py` — validation belongs in
tests and callers, not telemetry internals.
- When catching exceptions from the underlying library to record telemetry, always re-raise
the original exception unmodified.

## 4. Documentation

- Docstrings for invocation types and span/event helpers must include a link to the
corresponding operation in the semconv spec.
- When adding or changing attributes, update the docstring to describe what is set and under
what conditions (e.g., "set only when `server_address` is provided").

## 5. Tests

- Every new operation type or attribute change must have tests verifying the exact attribute
names and values produced, checked against the semconv spec.
- Cover all paths: success (`invocation.stop()`), failure (`invocation.fail(exc)`), and any
conditional attribute logic (e.g., attributes set only when optional fields are populated).
- Tests live in `tests/` — follow existing patterns there.
- Don't call internal API in tests when the public API is available.

## 6. Python API Conventions

- Mark private modules with an underscore.
- Objects inside of a private module should be prefixed with underscopre if they
are not used outside the that module.
- Before removing or renaming an object exposed publicly, deprecate it first with
`@deprecated("... Use X instead.")` pointing to the replacement;
6 changes: 4 additions & 2 deletions util/opentelemetry-util-genai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased


- Add support for workflow in genAI utils handler.
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366](#4366))
- Enrich ToolCall type, breaking change: usage of ToolCall class renamed to ToolCallRequest
- Enrich ToolCall type, breaking change: usage of ToolCall class renamed to ToolCallRequest
([#4218](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4218))
- Add EmbeddingInvocation span lifecycle support
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219](#4219))
Expand All @@ -20,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4310](#4310))
- Check if upload works at startup in initializer of the `UploadCompletionHook`, instead
of repeatedly failing on every upload ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4390](#4390)).
- 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.
([#4391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4391))


## Version 0.3b0 (2026-02-20)

Expand Down
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/CLAUDE.md
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ include = ["/src", "/tests"]

[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]

Loading
Loading