Skip to content
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Add support for emitting inference events and enrich message types. ([#3994](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3994))
- Minor change to check LRU cache in Completion Hook before acquiring semaphore/thread ([#3907](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3907)).
- Add environment variable for genai upload hook queue size
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3943](#3943))
Expand Down
11 changes: 10 additions & 1 deletion util/opentelemetry-util-genai/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ while providing standardization for generating both types of otel, "spans and me
This package relies on environment variables to configure capturing of message content.
By default, message content will not be captured.
Set the environment variable `OTEL_SEMCONV_STABILITY_OPT_IN` to `gen_ai_latest_experimental` to enable experimental features.
And set the environment variable `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to `SPAN_ONLY` or `SPAN_AND_EVENT` to capture message content in spans.
And set the environment variable `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to one of:
- `NO_CONTENT`: Do not capture message content (default).
- `SPAN_ONLY`: Capture message content in spans only.
- `EVENT_ONLY`: Capture message content in events only.
- `SPAN_AND_EVENT`: Capture message content in both spans and events.

This package provides these span attributes:

Expand All @@ -23,6 +27,11 @@ This package provides these span attributes:
- `gen_ai.usage.output_tokens`: Int(7)
- `gen_ai.input.messages`: Str('[{"role": "Human", "parts": [{"content": "hello world", "type": "text"}]}]')
- `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)

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).


Installation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def _calculate_ref_path(
if is_system_instructions_hashable(system_instruction):
# Get a hash of the text.
system_instruction_hash = hashlib.sha256(
"\n".join(x.content for x in system_instruction).encode( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
"\n".join(x.content for x in system_instruction).encode( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType, reportCallIssue, reportArgumentType]
"utf-8"
),
usedforsecurity=False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
)

OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT = "OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT"
"""
.. envvar:: OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT

Controls whether to emit gen_ai.client.inference.operation.details events.
Must be one of ``true`` or ``false`` (case-insensitive).
Defaults to ``false``.
"""

OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK = (
"OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@
from typing import Iterator

from opentelemetry import context as otel_context
from opentelemetry.metrics import MeterProvider, get_meter
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAI,
from opentelemetry._logs import (
LoggerProvider,
get_logger,
)
from opentelemetry.metrics import MeterProvider, get_meter
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.trace import (
Span,
Expand All @@ -80,7 +81,8 @@
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
from opentelemetry.util.genai.span_utils import (
_apply_error_attributes,
_apply_finish_attributes,
_apply_llm_finish_attributes,
_maybe_emit_llm_event,
)
from opentelemetry.util.genai.types import Error, LLMInvocation
from opentelemetry.util.genai.version import __version__
Expand All @@ -96,6 +98,7 @@ def __init__(
self,
tracer_provider: TracerProvider | None = None,
meter_provider: MeterProvider | None = None,
logger_provider: LoggerProvider | None = None,
):
self._tracer = get_tracer(
__name__,
Expand All @@ -106,6 +109,12 @@ def __init__(
self._metrics_recorder: InvocationMetricsRecorder | None = None
meter = get_meter(__name__, meter_provider=meter_provider)
self._metrics_recorder = InvocationMetricsRecorder(meter)
self._logger = get_logger(
__name__,
__version__,
logger_provider,
schema_url=Schemas.V1_37_0.value,
)

def _record_llm_metrics(
self,
Expand All @@ -129,7 +138,7 @@ def start_llm(
"""Start an LLM invocation and create a pending span entry."""
# Create a span and attach it as current; keep the token to detach later
span = self._tracer.start_span(
name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {invocation.request_model}",
name=f"{invocation.operation_name} {invocation.request_model}",
kind=SpanKind.CLIENT,
)
# Record a monotonic start timestamp (seconds) for duration
Expand All @@ -148,8 +157,9 @@ def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pylint: disab
return invocation

span = invocation.span
_apply_finish_attributes(span, invocation)
_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()
Expand All @@ -164,10 +174,11 @@ def fail_llm( # pylint: disable=no-self-use
return invocation

span = invocation.span
_apply_finish_attributes(invocation.span, invocation)
_apply_error_attributes(span, error)
_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()
Expand Down Expand Up @@ -201,6 +212,7 @@ def llm(
def get_telemetry_handler(
tracer_provider: TracerProvider | None = None,
meter_provider: MeterProvider | None = None,
logger_provider: LoggerProvider | None = None,
) -> TelemetryHandler:
"""
Returns a singleton TelemetryHandler instance.
Expand All @@ -212,6 +224,7 @@ def get_telemetry_handler(
handler = TelemetryHandler(
tracer_provider=tracer_provider,
meter_provider=meter_provider,
logger_provider=logger_provider,
)
setattr(get_telemetry_handler, "_default_handler", handler)
return handler
Loading