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}