feat: add OpenTelemetry tracing and metrics observer

* feat: add OpenTelemetry tracing and metrics observer

Add OtelObserver that exports traces and metrics via OTLP HTTP/protobuf
to any OpenTelemetry-compatible collector (Jaeger, Grafana Tempo, etc.).

- ObserverEvents map to OTel spans (AgentEnd, ToolCall, Error) and
  metric counters (AgentStart, ChannelMessage, HeartbeatTick)
- ObserverMetrics map to OTel histograms and gauges
- Spans include proper timing via SpanBuilder.with_start_time
- Config: backend="otel", otel_endpoint, otel_service_name
- Accepts "otel", "opentelemetry", "otlp" as backend aliases
- Graceful fallback to NoopObserver on init failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve unused variable warning and update Cargo.lock

Prefix unused `resolved_key` with underscore to suppress clippy
warning introduced by upstream changes. Regenerate Cargo.lock
after rebase on main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review comments on OTel observer

- Fix metric types: use Gauge for ActiveSessions/QueueDepth (absolute
  readings, not deltas), Counter<u64> for TokensUsed (monotonic)
- Remove duplicate token recording from AgentEnd event handler
  (TokensUsed metric via record_metric is the canonical path)
- Store meter_provider in struct so flush() exports both traces
  and metrics (was silently dropping metrics on shutdown)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
This commit is contained in:
Edvard Schøyen 2026-02-15 14:46:49 -05:00 committed by GitHub
parent 89b1ec6fa2
commit 0f6648ceb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 632 additions and 3 deletions

View file

@ -328,12 +328,22 @@ impl Default for MemoryConfig {
pub struct ObservabilityConfig {
/// "none" | "log" | "prometheus" | "otel"
pub backend: String,
/// OTLP endpoint (e.g. "http://localhost:4318"). Only used when backend = "otel".
#[serde(default)]
pub otel_endpoint: Option<String>,
/// Service name reported to the OTel collector. Defaults to "zeroclaw".
#[serde(default)]
pub otel_service_name: Option<String>,
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self {
backend: "none".into(),
otel_endpoint: None,
otel_service_name: None,
}
}
}
@ -1087,6 +1097,7 @@ mod tests {
default_temperature: 0.5,
observability: ObservabilityConfig {
backend: "log".into(),
..ObservabilityConfig::default()
},
autonomy: AutonomyConfig {
level: AutonomyLevel::Full,