返回 CodeWhale
counters.rs
根目录 / crates / telemetry / src / counters.rs
1 //! Process-wide session accumulators.
2 //!
3 //! Atomics rather than a lock, because every bump happens on a hot path and
4 //! must cost nothing measurable. Every bump also happens at a **call site**,
5 //! never inside a conditionally-entered handler: a counter sited above an early
6 //! return is a landmine, and the natural future optimization — hoisting the
7 //! guard to the caller — silently zeroes it for exactly the users who do not
8 //! use that feature.
9
10 use std::sync::Mutex;
11 use std::sync::atomic::{AtomicU32, Ordering};
12
13 use crate::event::{Counters, Errors, TurnWall};
14
15 /// One session's accumulated counts.
16 #[derive(Debug, Default)]
17 pub struct SessionCounters {
18 turns: AtomicU32,
19 tool_calls: AtomicU32,
20 fleet_dispatch: AtomicU32,
21 workflow_run: AtomicU32,
22 subagent_spawn: AtomicU32,
23 mcp_server_connected: AtomicU32,
24 memory_search: AtomicU32,
25 approval_modal_shown: AtomicU32,
26 approval_auto_allowed: AtomicU32,
27 command_palette_open: AtomicU32,
28
29 auth_preflight_failed: AtomicU32,
30 provider_http_4xx: AtomicU32,
31 provider_http_5xx: AtomicU32,
32 tool_denied_by_policy: AtomicU32,
33 tool_timeout: AtomicU32,
34 network_error: AtomicU32,
35
36 turn_wall: Mutex<TurnWall>,
37 /// Sorted, deduplicated `ProviderKind::as_str()` values. `&'static str`
38 /// only: a `String` here would be the seam through which a customer's
39 /// `[providers.<name>]` table key reaches the wire.
40 providers: Mutex<Vec<&'static str>>,
41 }
42
43 /// Which counter to bump. A closed enum so a call site cannot invent a key.
44 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
45 pub enum Counter {
46 /// A model turn completed.
47 Turns,
48 /// A tool call was executed.
49 ToolCalls,
50 /// A fleet dispatch started.
51 FleetDispatch,
52 /// A `workflow_run` action was parsed and executed.
53 WorkflowRun,
54 /// A sub-agent was spawned.
55 SubagentSpawn,
56 /// An MCP server reached `connected`.
57 McpServerConnected,
58 /// A native-memory search ran.
59 MemorySearch,
60 /// An approval modal was shown.
61 ApprovalModalShown,
62 /// An approval was granted by an auto-allow rule.
63 ApprovalAutoAllowed,
64 /// The command palette was opened.
65 CommandPaletteOpen,
66 }
67
68 /// Which error counter to bump.
69 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
70 pub enum ErrorCounter {
71 /// Credential preflight rejected the route.
72 AuthPreflightFailed,
73 /// Provider responded 4xx.
74 ProviderHttp4xx,
75 /// Provider responded 5xx.
76 ProviderHttp5xx,
77 /// A tool call was denied by policy.
78 ToolDeniedByPolicy,
79 /// A tool call timed out.
80 ToolTimeout,
81 /// A request failed below HTTP.
82 NetworkError,
83 }
84
85 impl SessionCounters {
86 /// Bump a feature counter by one.
87 pub fn bump(&self, counter: Counter) {
88 self.slot(counter).fetch_add(1, Ordering::Relaxed);
89 }
90
91 /// Bump an error counter by one.
92 pub fn bump_error(&self, counter: ErrorCounter) {
93 self.error_slot(counter).fetch_add(1, Ordering::Relaxed);
94 }
95
96 /// Record one turn's wall-clock time in the histogram.
97 pub fn observe_turn_secs(&self, secs: u64) {
98 if let Ok(mut wall) = self.turn_wall.lock() {
99 wall.observe_secs(secs);
100 }
101 }
102
103 /// Record that a provider was routed to.
104 ///
105 /// Takes a `ProviderKind` **by value**, never a `&str`: the four
106 /// persistence and label accessors that look like the natural seam all
107 /// return the customer's own `[providers.<name>]` table key when the route
108 /// is custom, and `/status` already prints it. `ProviderKind::Custom`
109 /// yields the literal `"custom"` and nothing else.
110 pub fn record_provider(&self, provider: codewhale_config::ProviderKind) {
111 let name = provider.as_str();
112 if let Ok(mut providers) = self.providers.lock()
113 && !providers.contains(&name)
114 {
115 providers.push(name);
116 providers.sort_unstable();
117 }
118 }
119
120 /// Snapshot the feature counters.
121 #[must_use]
122 pub fn counters(&self) -> Counters {
123 Counters {
124 turns: self.turns.load(Ordering::Relaxed),
125 tool_calls: self.tool_calls.load(Ordering::Relaxed),
126 fleet_dispatch: self.fleet_dispatch.load(Ordering::Relaxed),
127 workflow_run: self.workflow_run.load(Ordering::Relaxed),
128 subagent_spawn: self.subagent_spawn.load(Ordering::Relaxed),
129 mcp_server_connected: self.mcp_server_connected.load(Ordering::Relaxed),
130 memory_search: self.memory_search.load(Ordering::Relaxed),
131 approval_modal_shown: self.approval_modal_shown.load(Ordering::Relaxed),
132 approval_auto_allowed: self.approval_auto_allowed.load(Ordering::Relaxed),
133 command_palette_open: self.command_palette_open.load(Ordering::Relaxed),
134 }
135 }
136
137 /// Snapshot the error counters.
138 #[must_use]
139 pub fn errors(&self) -> Errors {
140 Errors {
141 auth_preflight_failed: self.auth_preflight_failed.load(Ordering::Relaxed),
142 provider_http_4xx: self.provider_http_4xx.load(Ordering::Relaxed),
143 provider_http_5xx: self.provider_http_5xx.load(Ordering::Relaxed),
144 tool_denied_by_policy: self.tool_denied_by_policy.load(Ordering::Relaxed),
145 tool_timeout: self.tool_timeout.load(Ordering::Relaxed),
146 network_error: self.network_error.load(Ordering::Relaxed),
147 }
148 }
149
150 /// Snapshot the turn wall-clock histogram.
151 #[must_use]
152 pub fn turn_wall(&self) -> TurnWall {
153 self.turn_wall.lock().map(|wall| *wall).unwrap_or_default()
154 }
155
156 /// Snapshot the sorted, deduplicated provider set.
157 #[must_use]
158 pub fn providers(&self) -> Vec<String> {
159 self.providers
160 .lock()
161 .map(|providers| providers.iter().map(|name| (*name).to_string()).collect())
162 .unwrap_or_default()
163 }
164
165 fn slot(&self, counter: Counter) -> &AtomicU32 {
166 match counter {
167 Counter::Turns => &self.turns,
168 Counter::ToolCalls => &self.tool_calls,
169 Counter::FleetDispatch => &self.fleet_dispatch,
170 Counter::WorkflowRun => &self.workflow_run,
171 Counter::SubagentSpawn => &self.subagent_spawn,
172 Counter::McpServerConnected => &self.mcp_server_connected,
173 Counter::MemorySearch => &self.memory_search,
174 Counter::ApprovalModalShown => &self.approval_modal_shown,
175 Counter::ApprovalAutoAllowed => &self.approval_auto_allowed,
176 Counter::CommandPaletteOpen => &self.command_palette_open,
177 }
178 }
179
180 fn error_slot(&self, counter: ErrorCounter) -> &AtomicU32 {
181 match counter {
182 ErrorCounter::AuthPreflightFailed => &self.auth_preflight_failed,
183 ErrorCounter::ProviderHttp4xx => &self.provider_http_4xx,
184 ErrorCounter::ProviderHttp5xx => &self.provider_http_5xx,
185 ErrorCounter::ToolDeniedByPolicy => &self.tool_denied_by_policy,
186 ErrorCounter::ToolTimeout => &self.tool_timeout,
187 ErrorCounter::NetworkError => &self.network_error,
188 }
189 }
190 }
191
192 /// Classify a provider HTTP status into the 4xx or 5xx counter, if either
193 /// applies. Captured from `status.as_u16() / 100` **before** the error is
194 /// built — every `LlmError` variant carries the raw provider body verbatim.
195 #[must_use]
196 pub fn http_status_counter(status: u16) -> Option<ErrorCounter> {
197 match status / 100 {
198 4 => Some(ErrorCounter::ProviderHttp4xx),
199 5 => Some(ErrorCounter::ProviderHttp5xx),
200 _ => None,
201 }
202 }
203
203 lines RUST