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 theotel.namefield (the statictracingmetadata name is the bare operation, sincetracingrequires a literal span name). - chat-span attributes:
gen_ai.operation.name, the provider tag as eithergen_ai.provider.nameorgen_ai.system(whichever the active GenAI semantic-convention version defines — seeObservabilityConfig::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.typeplus thetracing-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, andgen_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§
Structs§
- Observability
Config - Observability configuration read from the process environment, mirroring
(a subset of) Python’s
ObservabilitySettings(observability.py:347-394). - Observable
Chat Client - A
ChatClientdecorator that emits achatspan per request following the OpenTelemetry GenAI semantic conventions.
Constants§
- GEN_
AI_ LATEST_ EXPERIMENTAL_ OPT_ IN - The token recognized in
OTEL_SEMCONV_STABILITY_OPT_INthat selects the GenAI semantic conventions above the v1.36.0 baseline. Mirrors upstream’sGEN_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.typevalue for a frameworkError: its variant discriminant. - record_
error - Record an error onto a span following the existing
error.typepattern:error.type(the frameworkErrorvariant tag), plus thetracing-opentelemetry“special fields”otel.status_code("ERROR") andotel.status_message(the exception’sDisplaytext) so that a bridge sets real OTel span status. This mirrors Python’scapture_exception(record_exception+set_status,observability.py:1407-1411) as far as baretracingfields allow — there is no span-events API here without also taking on an SDK dependency. - record_
request - Record request-side attributes onto a
chatspan fromChatOptions, mirroring_get_span_attributes(observability.py:1345-1404). System instructions and the serialized tool list are additionally gated bycapture_content(mirrors Python’sSENSITIVE_DATA_ENABLEDgate). - 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-gatedgen_ai.tool.call.argumentscapture in Python’sAIFunction.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.resultcapture in Python’sAIFunction.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’sget_function_span_attributes,observability.py:1284-1302), and placeholders for the content-capture-gated call arguments/result (fill withrecord_tool_arguments/record_tool_result) and the error/status fields (fill withrecord_error).