Skip to main content

llm_kernel/telemetry/
events.rs

1//! Telemetry event types — strictly enum-gated to prevent PII leaks.
2
3use serde::{Deserialize, Serialize};
4
5/// Known tool names for telemetry.
6#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
7pub enum ToolName {
8    /// Keyword or semantic search over stored data.
9    Search,
10    /// Graph-aware smart recall.
11    Recall,
12    /// Text embedding generation.
13    Embed,
14    /// Knowledge graph query.
15    GraphQuery,
16    /// Single-turn LLM completion.
17    LlmComplete,
18    /// Streaming LLM completion.
19    LlmStream,
20    /// Configuration file load.
21    ConfigLoad,
22    /// Secret/credential read from vault.
23    SecretRead,
24}
25
26/// Known feature names for telemetry.
27#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
28pub enum FeatureName {
29    /// Smart recall with composite scoring.
30    SmartRecall,
31    /// BFS/DFS graph traversal.
32    GraphTraversal,
33    /// Hybrid BM25 + vector search with RRF fusion.
34    HybridSearch,
35    /// Unicode-aware token count estimation.
36    TokenEstimation,
37    /// Regex-based error classification.
38    SafetyClassification,
39    /// Output sanitization (Bidi, plane-14, null removal).
40    OutputSanitization,
41}
42
43/// Provider categories for telemetry.
44#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
45pub enum ProviderCategory {
46    /// Direct API access to a first-party cloud provider (OpenAI, Anthropic, etc.).
47    CloudFirstParty,
48    /// Third-party cloud provider or aggregator (OpenRouter, etc.).
49    CloudThirdParty,
50    /// Locally running model (Ollama, llama.cpp, etc.).
51    Local,
52    /// Proxy or routing layer in front of another provider.
53    Proxy,
54}
55
56/// A telemetry event. All fields use controlled vocabularies — no free strings.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum TelemetryEvent {
59    /// A tool was invoked.
60    ToolInvoked {
61        /// Tool identifier (from a fixed set).
62        tool: ToolName,
63    },
64
65    /// A tool invocation completed.
66    ToolCompleted {
67        /// Tool that completed.
68        tool: ToolName,
69        /// Milliseconds elapsed.
70        duration_ms: u64,
71        /// Whether the invocation succeeded.
72        success: bool,
73    },
74
75    /// A session started.
76    SessionStarted {
77        /// Session identifier (UUID, no PII).
78        session_id: String,
79    },
80
81    /// A session ended.
82    SessionEnded {
83        /// Session identifier (UUID, no PII).
84        session_id: String,
85        /// Total turns in the session.
86        turns: u32,
87        /// Total tokens consumed.
88        tokens_used: u64,
89    },
90
91    /// An error occurred.
92    Error {
93        /// Categorized failure class (no raw error messages).
94        class: FailureClass,
95    },
96
97    /// A feature was used.
98    FeatureUsed {
99        /// Feature that was used.
100        feature: FeatureName,
101    },
102
103    /// Provider routing decision.
104    ProviderRouted {
105        /// Provider category (not the exact provider name).
106        category: ProviderCategory,
107    },
108}
109
110/// Classification of failures for telemetry.
111///
112/// Uses broad categories to avoid leaking sensitive error details.
113#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
114pub enum FailureClass {
115    /// Network or connectivity issue.
116    Network,
117    /// Authentication or authorization failure.
118    Auth,
119    /// Rate limited by provider.
120    RateLimit,
121    /// Invalid input or configuration.
122    Validation,
123    /// Database or storage failure.
124    Storage,
125    /// Timeout.
126    Timeout,
127    /// Internal logic error.
128    Internal,
129    /// Unknown or uncategorized.
130    Unknown,
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn event_serializes_no_pii() {
139        let event = TelemetryEvent::ToolInvoked {
140            tool: ToolName::Search,
141        };
142        let json = serde_json::to_string(&event).unwrap();
143        assert!(json.contains("Search"));
144        assert!(!json.contains("password"));
145        assert!(!json.contains("email"));
146    }
147
148    #[test]
149    fn failure_class_roundtrip() {
150        let class = FailureClass::RateLimit;
151        let json = serde_json::to_string(&class).unwrap();
152        let back: FailureClass = serde_json::from_str(&json).unwrap();
153        assert_eq!(back, FailureClass::RateLimit);
154    }
155
156    #[test]
157    fn session_event() {
158        let event = TelemetryEvent::SessionStarted {
159            session_id: "abc-123".into(),
160        };
161        let json = serde_json::to_string(&event).unwrap();
162        assert!(json.contains("abc-123"));
163    }
164}