Skip to main content

oxios_kernel/
observability.rs

1//! Observability — oxi-sdk 0.26.2 tracing, cost tracking, and audit.
2//!
3//! Provides global instances of oxi-sdk's `Tracer`, `CostTracker`, and `AuditLog`
4//! for use across the kernel. These complement the existing `metrics` module
5//! (Prometheus counters/gauges) with distributed tracing, per-agent cost
6//! accounting, and structured audit logging.
7//!
8//! # Architecture
9//!
10//! ```text
11//! Global instances (OnceLock):
12//!   tracer()     → Tracer      (distributed spans: AgentSpan, ToolSpan, etc.)
13//!   cost_tracker() → CostTracker (per-agent token/cost tracking)
14//!   audit_log()  → AuditLog    (structured security audit entries)
15//! ```
16//!
17//! # Usage
18//!
19//! ```no_run
20//! use oxios_kernel::observability;
21//!
22//! // Start a span for an agent execution
23//! let _span = observability::tracer().start("agent-execution", observability::SpanKind::Agent);
24//!
25//! // Log audit entry
26//! observability::audit_log()
27//!     .log(observability::AuditEntry::tool_execution(
28//!         "agent-1".into(),
29//!         "exec".into(),
30//!         "ls -la".into(),
31//!         true,
32//!         42,
33//!     ));
34//! ```
35
36use oxi_sdk::ModelRegistry;
37// Re-exports grouped by concern. All names are part of the kernel's public
38// surface (re-exported via `lib.rs`) — do not remove or rename without
39// auditing downstream consumers.
40//
41// `audit_trail::*` types (AuditTrail, AuditAction, HashDigest, ...) are
42// intentionally NOT re-exported here: they live in the dormant `audit_trail`
43// module of oxi-sdk and will be activated in Phase F (RFC-014).
44pub use oxi_sdk::{
45    // ── Audit (in-memory) ──────────────────────────────────────────────
46    // Simple structured audit log. Replaced by `audit_trail` (blake3 chain)
47    // in Phase F.
48    AuditEntry,
49    AuditFilter,
50    AuditLog,
51    // ── Cost ───────────────────────────────────────────────────────────
52    // Per-agent token usage and cost accounting.
53    CostBreakdown,
54    CostSnapshot,
55    CostTracker,
56    CostTrackerConfig,
57    GlobalCostSnapshot,
58    // ── Tracing ────────────────────────────────────────────────────────
59    // Distributed spans for agent/tool/kernel operations.
60    Span,
61    SpanContext,
62    SpanGuard,
63    SpanId,
64    SpanKind,
65    SpanStatus,
66    TokenUsage,
67    TraceId,
68    Tracer,
69};
70use std::sync::Arc;
71
72/// Global Tracer instance.
73/// Stored as `Arc<Tracer>` because `Tracer::start` takes `self: &Arc<Self>` (oxi-sdk 0.56.0+).
74static TRACER: std::sync::OnceLock<Arc<Tracer>> = std::sync::OnceLock::new();
75
76/// Global CostTracker instance.
77static COST_TRACKER: std::sync::OnceLock<CostTracker> = std::sync::OnceLock::new();
78
79/// Global AuditLog instance.
80static AUDIT_LOG: std::sync::OnceLock<AuditLog> = std::sync::OnceLock::new();
81
82/// Get the global Tracer.
83///
84/// The tracer is lazily initialized on first access.
85/// Used for distributed tracing of agent executions, tool calls, and kernel operations.
86pub fn tracer() -> &'static Arc<Tracer> {
87    TRACER.get_or_init(|| Arc::new(Tracer::new()))
88}
89
90/// Get the global CostTracker.
91///
92/// The cost tracker uses a minimal ModelRegistry for token cost estimation.
93/// Record per-agent token usage after each LLM call.
94pub fn cost_tracker() -> &'static CostTracker {
95    COST_TRACKER.get_or_init(|| {
96        let registry = Arc::new(ModelRegistry::from_static());
97        CostTracker::new(registry, CostTrackerConfig::default())
98    })
99}
100
101/// Get the global AuditLog.
102///
103/// The audit log stores structured security events (tool calls, access decisions,
104/// lifecycle events). Entries can be queried by agent, action type, or time range.
105pub fn audit_log() -> &'static AuditLog {
106    AUDIT_LOG.get_or_init(|| AuditLog::new(1024))
107}
108
109/// Initialize all observability instances.
110///
111/// Call during kernel startup to ensure all instances are warm.
112/// Non-blocking — just triggers lazy initialization.
113pub fn init() {
114    let _ = tracer();
115    let _ = cost_tracker();
116    let _ = audit_log();
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_tracer_smoke() {
125        let t = tracer();
126        let _guard = t.start("test-span", SpanKind::Agent);
127        // Span is active while guard is in scope
128        drop(_guard);
129    }
130
131    #[test]
132    fn test_cost_tracker_smoke() {
133        let ct = cost_tracker();
134        let model = oxi_sdk::Model::new(
135            "test/model",
136            "Test",
137            oxi_sdk::Api::OpenAiCompletions,
138            "test",
139            "https://test.com",
140        );
141        ct.record(
142            "test-agent",
143            &model,
144            TokenUsage {
145                input: 100,
146                output: 50,
147                cache_read: 0,
148                cache_write: 0,
149            },
150        );
151        let snap = ct.snapshot("test-agent");
152        assert!(snap.is_some());
153    }
154
155    #[test]
156    fn test_audit_log_smoke() {
157        let al = audit_log();
158        al.log(AuditEntry::lifecycle("test-agent".into(), "started".into()));
159        let entries = al.query(AuditFilter {
160            agent_id: Some("test-agent".to_string()),
161            entry_type: None,
162            after_ms: None,
163        });
164        assert!(!entries.is_empty());
165    }
166
167    #[test]
168    fn test_init_idempotent() {
169        init();
170        init(); // Should not panic
171    }
172}