Skip to main content

lc_core/observability/
mod.rs

1// lc-core/src/observability/mod.rs
2//! Unified, pluggable observability export.
3//!
4//! `TokenTrackingLLM` (token usage) and `AgentExecutor` (per-run metrics) both
5//! write through the same [`MetricsSink`] interface with an [`ObsEvent`] payload.
6//! The framework only provides the capability — no concrete sink is bundled here
7//! (see the `lc-observability` crate for `JsonLinesSink`/`MongoSink`). Failures
8//! are logged as `warn` and never interrupt the main flow.
9
10mod agent_metrics;
11mod error;
12
13pub use agent_metrics::AgentMetrics;
14pub use error::ObsError;
15
16use crate::language_models::TokenUsage;
17use serde::Serialize;
18
19/// One observability record (the unified export payload).
20#[derive(Debug, Clone, Serialize)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22pub enum ObsEvent {
23    /// Token usage of a single LLM call (exported as it happens).
24    TokenUsage(TokenUsage),
25    /// Aggregated metrics of one agent run (exported once at the end).
26    AgentMetrics(AgentMetrics),
27}
28
29/// Pluggable observability sink. The framework only provides the interface and
30/// binds no concrete plugin.
31#[async_trait::async_trait]
32pub trait MetricsSink: Send + Sync {
33    /// Pushes one record. Implementations must contain their own failures (or
34    /// let the framework `warn` on `Err`) — errors never propagate to the caller.
35    async fn export(&self, event: &ObsEvent) -> Result<(), ObsError>;
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use std::time::Duration;
42
43    fn sample_usage() -> TokenUsage {
44        TokenUsage {
45            prompt_tokens: 10,
46            completion_tokens: 5,
47            total_tokens: 15,
48        }
49    }
50
51    #[test]
52    fn obs_event_token_usage_serializes_with_kind_tag() {
53        let json = serde_json::to_string(&ObsEvent::TokenUsage(sample_usage())).unwrap();
54        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
55        assert_eq!(v["kind"], "token_usage");
56        assert_eq!(v["prompt_tokens"], 10);
57        assert_eq!(v["completion_tokens"], 5);
58        assert_eq!(v["total_tokens"], 15);
59    }
60
61    #[test]
62    fn obs_event_agent_metrics_serializes_with_kind_tag() {
63        let m = AgentMetrics {
64            trace_id: Some("trace-x".to_string()),
65            llm_calls: 2,
66            cache_hits: 0,
67            tool_calls: 1,
68            total_tokens: Some(30),
69            duration: Duration::from_millis(100),
70        };
71        let json = serde_json::to_string(&ObsEvent::AgentMetrics(m)).unwrap();
72        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
73        assert_eq!(v["kind"], "agent_metrics");
74        assert_eq!(v["llm_calls"], 2);
75        assert_eq!(v["tool_calls"], 1);
76        assert_eq!(v["total_tokens"], 30);
77    }
78}