Skip to content

Enable Observability

Use this when you need to trace LLM inference calls and tool executions with OpenTelemetry-compatible telemetry.

  • A working awaken agent runtime (see First Agent)
  • Feature observability enabled on the awaken crate (enabled by default)
  • For OTel export: feature otel enabled on awaken-ext-observability, plus a configured OTel collector
[dependencies]
awaken = { git = "https://github.com/AwakenWorks/awaken", features = ["observability"] }
tokio = { version = "1", features = ["full"] }
  1. Register with the in-memory sink (development).
use std::sync::Arc;
use awaken::engine::GenaiExecutor;
use awaken::ext_observability::{ObservabilityPlugin, InMemorySink};
use awaken::registry_spec::ModelSpec;
use awaken::registry_spec::AgentSpec;
use awaken::{AgentRuntimeBuilder, Plugin};
let sink = InMemorySink::new();
let obs_plugin = ObservabilityPlugin::new(sink.clone());
let mut agent_spec = AgentSpec::new("observed-agent")
.with_model_id("gpt-4o-mini")
.with_system_prompt("You are a helpful assistant.")
.with_hook_filter("observability");
agent_spec.plugin_ids.push("observability".into());
let runtime = AgentRuntimeBuilder::new()
.with_provider("openai", Arc::new(GenaiExecutor::new()))
.with_model(ModelSpec::new("gpt-4o-mini", "openai", "gpt-4o-mini"))
.with_agent_spec(agent_spec)
.with_plugin("observability", Arc::new(obs_plugin) as Arc<dyn Plugin>)
.build()
.expect("failed to build runtime");

plugin_ids loads the observability plugin. with_hook_filter("observability") keeps its phase hooks active when the agent loads multiple plugins.

After a run completes, inspect collected metrics:

let metrics = sink.metrics();
println!("inferences: {}", metrics.inferences.len());
println!("tool calls: {}", metrics.tools.len());
println!("total input tokens: {}", metrics.total_input_tokens());
println!("total output tokens: {}", metrics.total_output_tokens());
println!("tool failures: {}", metrics.tool_failures());
println!("session duration: {}ms", metrics.session_duration_ms);
for stat in metrics.stats_by_model() {
println!("{}: {} calls, {} in / {} out tokens",
stat.model, stat.inference_count, stat.input_tokens, stat.output_tokens);
}
for stat in metrics.stats_by_tool() {
println!("{}: {} calls, {} failures",
stat.name, stat.call_count, stat.failure_count);
}
  1. Register with the OTel sink (production).
use std::sync::Arc;
use awaken::engine::GenaiExecutor;
use awaken::ext_observability::{ObservabilityPlugin, OtelMetricsSink};
use awaken::registry_spec::ModelSpec;
use awaken::registry_spec::AgentSpec;
use awaken::{AgentRuntimeBuilder, Plugin};
use opentelemetry_sdk::trace::SdkTracerProvider;
let provider = SdkTracerProvider::builder()
// configure your exporter (OTLP, Jaeger, etc.)
.build();
let tracer = provider.tracer("awaken");
let obs_plugin = ObservabilityPlugin::new(OtelMetricsSink::new(tracer));
let mut agent_spec = AgentSpec::new("observed-agent")
.with_model_id("gpt-4o-mini")
.with_system_prompt("You are a helpful assistant.")
.with_hook_filter("observability");
agent_spec.plugin_ids.push("observability".into());
let runtime = AgentRuntimeBuilder::new()
.with_provider("openai", Arc::new(GenaiExecutor::new()))
.with_model(ModelSpec::new("gpt-4o-mini", "openai", "gpt-4o-mini"))
.with_agent_spec(agent_spec)
.with_plugin("observability", Arc::new(obs_plugin) as Arc<dyn Plugin>)
.build()
.expect("failed to build runtime");
  1. Implement a custom sink (optional).
use awaken::ext_observability::{MetricsSink, GenAISpan, ToolSpan, AgentMetrics};
struct MySink;
impl MetricsSink for MySink {
fn on_inference(&self, span: &GenAISpan) {
// forward to your metrics system
}
fn on_tool(&self, span: &ToolSpan) {
// forward to your metrics system
}
fn on_run_end(&self, metrics: &AgentMetrics) {
// emit summary metrics
}
}
  1. Captured telemetry.

    The plugin hooks into the following phases:

PhaseData Captured
RunStartSession start timestamp
BeforeInferenceInference start timestamp, model, provider
AfterInferenceToken usage, finish reasons, duration, cache tokens
BeforeToolExecuteTool call start timestamp
AfterToolExecuteTool duration, error status; optional tool arguments/results when ToolIoCapture is enabled
RunEndSession duration

OTel spans follow GenAI semantic conventions: the root agent span uses gen_ai.operation.name=invoke_agent; inference spans use attributes such as gen_ai.provider.name, gen_ai.request.model, gen_ai.conversation.id, gen_ai.usage.input_tokens, and gen_ai.usage.output_tokens; tool spans use gen_ai.operation.name=execute_tool plus gen_ai.tool.* attributes.

  1. Run an agent with the InMemorySink.
  2. After run() completes, call sink.metrics().
  3. Confirm inferences is non-empty and token counts are populated.
  4. For OTel, check your collector or Jaeger UI for spans named with the awaken tracer.
SymptomCauseFix
Metrics are all zeroPlugin not registeredRegister ObservabilityPlugin with the runtime builder
OtelMetricsSink not foundMissing otel featureEnable the otel feature on awaken-ext-observability
No spans in collectorExporter not configuredVerify SdkTracerProvider has an exporter and is not dropped
Token counts missingLLM provider does not report usageCheck that your LlmExecutor returns TokenUsage in LLMResponse
  • crates/awaken-ext-observability/tests/
PathPurpose
crates/awaken-ext-observability/src/lib.rsModule root and public re-exports
crates/awaken-ext-observability/src/plugin/plugin.rsObservabilityPlugin registration
crates/awaken-ext-observability/src/plugin/hooks.rsPhase hooks for each telemetry point
crates/awaken-ext-observability/src/metrics.rsAgentMetrics, GenAISpan, ToolSpan types
crates/awaken-ext-observability/src/sink.rsMetricsSink trait and InMemorySink
crates/awaken-ext-observability/src/otel.rsOtelMetricsSink with GenAI semantic conventions