1use std::sync::Mutex;
11use std::sync::atomic::{AtomicU32, Ordering};
12
13use crate::event::{Counters, Errors, TurnWall};
14
15#[derive(Debug, Default)]
17pub 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 providers: Mutex<Vec<&'static str>>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Counter {
46 Turns,
48 ToolCalls,
50 FleetDispatch,
52 WorkflowRun,
54 SubagentSpawn,
56 McpServerConnected,
58 MemorySearch,
60 ApprovalModalShown,
62 ApprovalAutoAllowed,
64 CommandPaletteOpen,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum ErrorCounter {
71 AuthPreflightFailed,
73 ProviderHttp4xx,
75 ProviderHttp5xx,
77 ToolDeniedByPolicy,
79 ToolTimeout,
81 NetworkError,
83}
84
85impl SessionCounters {
86 pub fn bump(&self, counter: Counter) {
88 self.slot(counter).fetch_add(1, Ordering::Relaxed);
89 }
90
91 pub fn bump_error(&self, counter: ErrorCounter) {
93 self.error_slot(counter).fetch_add(1, Ordering::Relaxed);
94 }
95
96 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 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 #[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 #[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 #[must_use]
152 pub fn turn_wall(&self) -> TurnWall {
153 self.turn_wall.lock().map(|wall| *wall).unwrap_or_default()
154 }
155
156 #[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#[must_use]
196pub 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}