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}