Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
58d2824
feat(anthropic): add Messages.create sync instrumentation
vasantteja Dec 16, 2025
371c580
polish: updating the changelog.
vasantteja Dec 16, 2025
84003d9
polish: fxing the checks.
vasantteja Dec 16, 2025
1aa239d
polish: fixing the linting, test failures and commit checks.
vasantteja Dec 16, 2025
a37e12b
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 16, 2025
9601d0c
wip: fixing generate and lint errors.
vasantteja Dec 16, 2025
fd24edc
fixing changelog.
vasantteja Dec 16, 2025
5e57910
polish: removing unnecessary pyright ignore comments.
vasantteja Dec 16, 2025
c21a700
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 16, 2025
0c30279
polish: downgrading the sdk version to 0.16.0.
vasantteja Dec 17, 2025
94035fb
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 18, 2025
99f274e
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 20, 2025
404c691
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 23, 2025
998882c
Refactor Anthropic instrumentation to use TelemetryHandler for tracing
vasantteja Dec 25, 2025
d4aaa08
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 25, 2025
e3a4ebe
Refactor test setup and instrumentor functionality
vasantteja Dec 25, 2025
589ba43
wip: updating requirements for avoiding test failures.
vasantteja Dec 25, 2025
b6d448a
wip: fixing tox issues.
vasantteja Dec 25, 2025
2a96e0c
Refactor request model retrieval in messages_create function
vasantteja Dec 25, 2025
643a282
Enhance LLM request attribute extraction in get_llm_request_attribute…
vasantteja Dec 25, 2025
24c1bff
Add integration tests for Anthropic API and update pytest configuration
vasantteja Dec 28, 2025
3052f69
Refactor integration tests for Anthropic API
vasantteja Dec 28, 2025
ef58f1f
Enable pylint for integration tests in test_integration.py
vasantteja Dec 28, 2025
f6594c3
Refactor LLM request attribute handling and improve documentation
vasantteja Dec 28, 2025
ad25439
Add TODO for logger_provider in TelemetryHandler
vasantteja Dec 29, 2025
6a42212
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Dec 30, 2025
adc7e39
Merge branch 'main' into feat/anthropic-messages-create-sync
vasantteja Jan 5, 2026
a6f669d
Remove integration test markers and related code from the Anthropic i…
vasantteja Jan 6, 2026
e5ea522
Remove unnecessary blank lines in the VCR fixture in the Anthropic in…
vasantteja Jan 6, 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
2 changes: 1 addition & 1 deletion instrumentation-genai/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

| Instrumentation | Supported Packages | Metrics support | Semconv status |
| --------------- | ------------------ | --------------- | -------------- |
| [opentelemetry-instrumentation-anthropic](./opentelemetry-instrumentation-anthropic) | anthropic >= 0.3.0 | No | development
| [opentelemetry-instrumentation-anthropic](./opentelemetry-instrumentation-anthropic) | anthropic >= 0.16.0 | No | development
| [opentelemetry-instrumentation-google-genai](./opentelemetry-instrumentation-google-genai) | google-genai >= 1.0.0 | No | development
| [opentelemetry-instrumentation-langchain](./opentelemetry-instrumentation-langchain) | langchain >= 0.3.21 | No | development
| [opentelemetry-instrumentation-openai-agents-v2](./opentelemetry-instrumentation-openai-agents-v2) | openai-agents >= 0.3.3 | No | development
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Initial implementation of Anthropic instrumentation
([#ISSUE_NUMBER](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3978))
([#3978](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3978))
- Implement sync `Messages.create` instrumentation with GenAI semantic convention attributes
([#4034](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4034))
- Captures request attributes: `gen_ai.request.model`, `gen_ai.request.max_tokens`, `gen_ai.request.temperature`, `gen_ai.request.top_p`, `gen_ai.request.top_k`, `gen_ai.request.stop_sequences`
- Captures response attributes: `gen_ai.response.id`, `gen_ai.response.model`, `gen_ai.response.finish_reasons`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`
- Error handling with `error.type` attribute
- Minimum supported anthropic version is 0.16.0 (SDK uses modern `anthropic.resources.messages` module structure introduced in this version)

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
anthropic~=0.3.0
anthropic>=0.16.0
opentelemetry-sdk~=1.36.0
opentelemetry-instrumentation-anthropic #TODO: update to 2.1b0 when released
opentelemetry-exporter-otlp-proto-grpc~=1.36.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
anthropic~=0.3.0
anthropic>=0.16.0
opentelemetry-sdk~=1.28.2
opentelemetry-distro~=0.49b2
opentelemetry-instrumentation-anthropic #TODO: update to 2.1b0 when released
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies = [

[project.optional-dependencies]
instruments = [
"anthropic >= 0.3.0",
"anthropic >= 0.16.0",
]

[project.entry-points.opentelemetry_instrumentor]
Expand All @@ -56,3 +56,6 @@ include = [
[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]

[tool.pytest.ini_options]
testpaths = ["tests"]

Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,16 @@

from typing import Any, Collection

from wrapt import (
wrap_function_wrapper, # pyright: ignore[reportUnknownVariableType]
)

from opentelemetry.instrumentation.anthropic.package import _instruments
from opentelemetry.instrumentation.anthropic.patch import messages_create
from opentelemetry.instrumentation.anthropic.utils import is_content_enabled
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.util.genai.handler import TelemetryHandler


class AnthropicInstrumentor(BaseInstrumentor):
Expand Down Expand Up @@ -80,50 +87,31 @@ def _instrument(self, **kwargs: Any) -> None:
- meter_provider: MeterProvider instance
- logger_provider: LoggerProvider instance
"""
# pylint: disable=import-outside-toplevel
from opentelemetry._logs import get_logger # noqa: PLC0415
from opentelemetry.metrics import get_meter # noqa: PLC0415
from opentelemetry.trace import get_tracer # noqa: PLC0415

# Get providers from kwargs
tracer_provider = kwargs.get("tracer_provider")
logger_provider = kwargs.get("logger_provider")
meter_provider = kwargs.get("meter_provider")

# Initialize tracer
tracer = get_tracer(
__name__,
"",
tracer_provider,
schema_url=Schemas.V1_28_0.value,
)

# Initialize logger for events
logger = get_logger(
__name__,
"",
schema_url=Schemas.V1_28_0.value,
logger_provider=logger_provider,
# TODO: Add logger_provider to TelemetryHandler to capture content events.
handler = TelemetryHandler(
tracer_provider=tracer_provider,
meter_provider=meter_provider,
)

# Initialize meter for metrics
meter = get_meter(
__name__,
"",
meter_provider,
schema_url=Schemas.V1_28_0.value,
# Patch Messages.create
wrap_function_wrapper(
module="anthropic.resources.messages",
name="Messages.create",
wrapper=messages_create(handler, is_content_enabled()),
)

# Store for later use in _uninstrument
self._tracer = tracer
self._logger = logger
self._meter = meter

# Patching will be added in Ticket 3

def _uninstrument(self, **kwargs: Any) -> None:
"""Disable Anthropic instrumentation.

This removes all patches applied during instrumentation.
"""
# Unpatching will be added in Ticket 3
import anthropic # pylint: disable=import-outside-toplevel # noqa: PLC0415

unwrap(
anthropic.resources.messages.Messages, # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType,reportUnknownArgumentType]
"create",
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

_instruments = ("anthropic >= 0.3.0",)
_instruments = ("anthropic >= 0.16.0",)
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,63 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Patching functions for Anthropic instrumentation."""

from typing import Any, Callable

from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.util.genai.handler import TelemetryHandler
from opentelemetry.util.genai.types import LLMInvocation

from .utils import (
get_llm_request_attributes,
)


def messages_create(
handler: TelemetryHandler,
_capture_content: bool,
) -> Callable[..., Any]:
"""Wrap the `create` method of the `Messages` class to trace it."""

def traced_method(
wrapped: Callable[..., Any],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
attributes = get_llm_request_attributes(kwargs, instance)
request_model = str(
attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
or kwargs.get("model")
or "unknown"
)

invocation = LLMInvocation(
request_model=request_model,
provider="anthropic",
attributes=attributes,
)

with handler.llm(invocation) as invocation:
result = wrapped(*args, **kwargs)

if getattr(result, "model", None):
invocation.response_model_name = result.model

if getattr(result, "id", None):
invocation.response_id = result.id

if getattr(result, "stop_reason", None):
invocation.finish_reasons = [result.stop_reason]

if getattr(result, "usage", None):
invocation.input_tokens = result.usage.input_tokens
invocation.output_tokens = result.usage.output_tokens

return result

return traced_method
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Utility functions for Anthropic instrumentation."""

from __future__ import annotations

from os import environ
from typing import Any, Optional
from urllib.parse import urlparse

from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.semconv._incubating.attributes import (
server_attributes as ServerAttributes,
)
from opentelemetry.util.types import AttributeValue

OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
)


def is_content_enabled() -> bool:
"""Check if content capture is enabled via environment variable."""
capture_content = environ.get(
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
)
return capture_content.lower() == "true"


def set_server_address_and_port(
client_instance: Any, attributes: dict[str, Any]
) -> None:
"""Extract server address and port from the Anthropic client instance."""
base_client = getattr(client_instance, "_client", None)
base_url = getattr(base_client, "base_url", None)
if not base_url:
return

port: Optional[int] = None
if hasattr(base_url, "host"):
# httpx.URL object
attributes[ServerAttributes.SERVER_ADDRESS] = base_url.host
port = getattr(base_url, "port", None)
elif isinstance(base_url, str):
url = urlparse(base_url)
attributes[ServerAttributes.SERVER_ADDRESS] = url.hostname
port = url.port

if port and port != 443 and port > 0:
attributes[ServerAttributes.SERVER_PORT] = port


def get_llm_request_attributes(
kwargs: dict[str, Any], client_instance: Any
) -> dict[str, AttributeValue]:
"""Extract LLM request attributes from kwargs.

Returns a dictionary of OpenTelemetry semantic convention attributes for LLM requests.
The attributes follow the GenAI semantic conventions (gen_ai.*) and server semantic
conventions (server.*) as defined in the OpenTelemetry specification.

GenAI attributes included:
- gen_ai.operation.name: The operation name (e.g., "chat")
- gen_ai.system: The GenAI system identifier (e.g., "anthropic")
- gen_ai.request.model: The model identifier
- gen_ai.request.max_tokens: Maximum tokens in the request
- gen_ai.request.temperature: Sampling temperature
- gen_ai.request.top_p: Top-p sampling parameter
- gen_ai.request.top_k: Top-k sampling parameter
- gen_ai.request.stop_sequences: Stop sequences for the request

Server attributes included (if available):
- server.address: The server hostname
- server.port: The server port (if not default 443)

Only non-None values are included in the returned dictionary.
"""
attributes = {
GenAIAttributes.GEN_AI_OPERATION_NAME: GenAIAttributes.GenAiOperationNameValues.CHAT.value,
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.ANTHROPIC.value, # pyright: ignore[reportDeprecated]
GenAIAttributes.GEN_AI_REQUEST_MODEL: kwargs.get("model"),
GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature"),
GenAIAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("top_p"),
GenAIAttributes.GEN_AI_REQUEST_TOP_K: kwargs.get("top_k"),
GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: kwargs.get(
"stop_sequences"
),
}

set_server_address_and_port(client_instance, attributes)

# Filter out None values
return {k: v for k, v in attributes.items() if v is not None}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
interactions:
- request:
body: |-
{
"max_tokens": 100,
"messages": [
{
"role": "user",
"content": "Hello"
}
],
"model": "invalid-model-name"
}
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-length:
- '110'
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python
x-api-key:
- test_anthropic_api_key
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: |-
{
"type": "error",
"error": {
"type": "not_found_error",
"message": "model: invalid-model-name"
}
}
headers:
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Mon, 15 Dec 2024 10:00:04 GMT
Server:
- cloudflare
content-length:
- '105'
status:
code: 404
message: Not Found
version: 1

Loading