Skip to main content

Module observability

Module observability 

Source
Expand description

Lightweight OpenTelemetry GenAI-style span instrumentation, built on the tracing crate, plus optional GenAI metrics behind the otel-metrics feature.

This is a dependency-light port of the Python observability.py instrumentation. It emits tracing spans that follow the OpenTelemetry GenAI semantic conventions, so that an OTel bridge (e.g. tracing-opentelemetry) can export them without any additional glue:

  • span names: chat {model}, invoke_agent {agent}, execute_tool {tool} — the human-readable name is carried in the otel.name field (the static tracing metadata name is the bare operation, since tracing requires a literal span name).
  • chat-span attributes: gen_ai.operation.name, the provider tag as either gen_ai.provider.name or gen_ai.system (whichever the active GenAI semantic-convention version defines — see ObservabilityConfig::use_latest_experimental_gen_ai_semconv), gen_ai.request.model, gen_ai.response.model, gen_ai.response.id, gen_ai.response.finish_reasons, gen_ai.usage.{input,output}_tokens, the request parameters (gen_ai.request.{temperature,top_p,max_tokens, seed,frequency_penalty,presence_penalty,stop_sequences}, gen_ai.conversation.id), error.type plus the tracing-opentelemetry “special fields” otel.status_code / otel.status_message — and, only when content capture is explicitly enabled, gen_ai.input.messages / gen_ai.output.messages, gen_ai.system_instructions, and gen_ai.tool.definitions.
  • tool-span attributes: gen_ai.tool.name, gen_ai.tool.call.id, gen_ai.tool.description, gen_ai.tool.type, and (content-capture-gated) gen_ai.tool.call.arguments / gen_ai.tool.call.result.

The main entry point is ObservableChatClient, a ChatClient decorator. Tool execution inside FunctionInvokingChatClient and the Agent run paths are instrumented directly by those types using the span constructors here.

§Metrics (otel-metrics feature)

With the otel-metrics feature enabled, ObservableChatClient also records two histograms through the opentelemetry API crate only (mirroring observability.py:788-803, bucket boundaries at :65-96): [metrics::TOKEN_USAGE_METRIC] (gen_ai.client.token.usage, unit "tokens") and [metrics::OPERATION_DURATION_METRIC] (gen_ai.client.operation.duration, unit "s"). A third histogram, [metrics::FUNCTION_INVOCATION_DURATION_METRIC] (agent_framework.function.invocation.duration), times tool calls and is recorded by FunctionInvokingChatClient around each tool invocation. This crate never depends on an OTel SDK: without an application-installed MeterProvider (via [opentelemetry::global::set_meter_provider]) the instruments are no-ops, so the feature is safe to enable unconditionally.

§Wiring to a real OTel backend

By default neither the tracing spans nor the otel-metrics histograms are exported anywhere by this crate — that stays the application’s job, so that an OTel SDK and its version churn never reach consumers who don’t ask for them.

Two ways to close that loop:

  • otel-export feature — [export::OtelExport] builds an OTLP pipeline (exporter, tracer provider, meter provider, and the tracing↔OTel bridge) in one call, already wired to the conventions above. This is the only feature that pulls an OTel SDK.
  • Wire it yourself — compose tracing-opentelemetry for spans with an opentelemetry_sdk MeterProvider installed via [opentelemetry::global::set_meter_provider] for the histograms. The otel_export example in this repository’s examples/observability/ shows both routes end to end, and is compiled by CI so it cannot drift out of date.

Without either, spans are still emitted to whatever plain tracing subscriber you do have (e.g. for structured logging), and otel-metrics instruments silently drop their measurements — zero required setup either way.

§Environment configuration

ObservabilityConfig::from_env reads ENABLE_SENSITIVE_DATA (mirrors Python’s enable_sensitive_data, observability.py:347-394) and OTEL_SEMCONV_STABILITY_OPT_IN (which GenAI semantic-convention version to emit — see ObservabilityConfig::use_latest_experimental_gen_ai_semconv) for use with ObservableChatClient::from_env. Unlike Python, there is no ENABLE_OTEL equivalent to read: this crate’s spans are plain tracing spans, already effectively free without a subscriber attached, so there is no separate “enable observability” switch.

Modules§

attr
OpenTelemetry GenAI semantic-convention attribute keys.
op
OpenTelemetry GenAI operation names.

Structs§

ObservabilityConfig
Observability configuration read from the process environment, mirroring (a subset of) Python’s ObservabilitySettings (observability.py:347-394).
ObservableChatClient
A ChatClient decorator that emits a chat span per request following the OpenTelemetry GenAI semantic conventions.

Constants§

GEN_AI_LATEST_EXPERIMENTAL_OPT_IN
The token recognized in OTEL_SEMCONV_STABILITY_OPT_IN that selects the GenAI semantic conventions above the v1.36.0 baseline. Mirrors upstream’s GEN_AI_LATEST_EXPERIMENTAL_OPT_IN.

Functions§

agent_span
Build an invoke_agent {agent} span for an agent run.
chat_span
Build a chat {model} span for a chat-completion request.
error_type
The error.type value for a framework Error: its variant discriminant.
record_error
Record an error onto a span following the existing error.type pattern: error.type (the framework Error variant tag), plus the tracing-opentelemetry “special fields” otel.status_code ("ERROR") and otel.status_message (the exception’s Display text) so that a bridge sets real OTel span status. This mirrors Python’s capture_exception (record_exception + set_status, observability.py:1407-1411) as far as bare tracing fields allow — there is no span-events API here without also taking on an SDK dependency.
record_request
Record request-side attributes onto a chat span from ChatOptions, mirroring _get_span_attributes (observability.py:1345-1404). System instructions and the serialized tool list are additionally gated by capture_content (mirrors Python’s SENSITIVE_DATA_ENABLED gate).
record_response
Record the response-side attributes (finish reason, usage, id, model) onto span, mirroring _get_response_attributes (observability.py:1488-1512).
record_tool_arguments
Record tool-call arguments onto a tool span, gated by content capture (mirrors the SENSITIVE_DATA_ENABLED-gated gen_ai.tool.call.arguments capture in Python’s AIFunction.invoke, _tools.py:751-759).
record_tool_result
Record a tool-call result onto a tool span, gated by content capture (mirrors the gen_ai.tool.call.result capture in Python’s AIFunction.invoke, _tools.py:779-787).
tool_span
Build an execute_tool {tool} span (source-compatible two-argument form).
tool_span_ex
Build an execute_tool {tool} span with the full OTel GenAI tool attribute set: name, call id, description, a fixed "function" tool type (the only kind executed through this in-process loop — mirrors Python’s get_function_span_attributes, observability.py:1284-1302), and placeholders for the content-capture-gated call arguments/result (fill with record_tool_arguments / record_tool_result) and the error/status fields (fill with record_error).