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), is defined for tool-call timing; see [metrics::record_function_invocation_duration] for why it isn’t wired to a call site yet. 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

Neither the tracing spans nor the otel-metrics histograms are exported anywhere by this crate — that stays the application’s job. A minimal bridge, using tracing-opentelemetry for spans and opentelemetry_sdk for metrics:

use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::metrics::SdkMeterProvider;
use tracing_subscriber::layer::SubscriberExt;

let tracer_provider = SdkTracerProvider::builder()
    // .with_batch_exporter(otlp_span_exporter) / .with_simple_exporter(...) …
    .build();
let meter_provider = SdkMeterProvider::builder()
    // .with_reader(periodic_reader_wrapping_your_metric_exporter) …
    .build();
opentelemetry::global::set_meter_provider(meter_provider); // powers `otel-metrics`

let tracer = tracer_provider.tracer("agent_framework");
let subscriber = tracing_subscriber::registry()
    .with(tracing_opentelemetry::layer().with_tracer(tracer));
tracing::subscriber::set_global_default(subscriber).unwrap();

Without any of this, 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).