supercode_harness/
slow_log.rs1use std::io::Write;
10use std::path::PathBuf;
11use std::sync::Mutex;
12use std::time::Instant;
13
14pub const SLOW_MS: u128 = 1000;
16
17pub const ROTATE_BYTES: u64 = 4 * 1024 * 1024;
19
20pub const KEEP_ROTATED: usize = 3;
22
23static SLOWEST: Mutex<Option<(String, u128)>> = Mutex::new(None);
24static STEPS: Mutex<Vec<(String, u128, u32)>> = Mutex::new(Vec::new());
26
27pub fn note_step(step: &str, ms: u128) {
30 if let Ok(mut slowest) = SLOWEST.lock() {
31 if slowest.as_ref().is_none_or(|(_, longest)| ms > *longest) {
32 *slowest = Some((step.to_string(), ms));
33 }
34 }
35 if let Ok(mut steps) = STEPS.lock() {
36 match steps.iter_mut().find(|(name, _, _)| name == step) {
37 Some((_, total, count)) => {
38 *total += ms;
39 *count += 1;
40 }
41 None => steps.push((step.to_string(), ms, 1)),
42 }
43 }
44}
45
46fn step_totals() -> serde_json::Value {
48 let mut steps = STEPS.lock().map(|steps| steps.clone()).unwrap_or_default();
49 steps.sort_by(|a, b| b.1.cmp(&a.1));
50 serde_json::Value::Array(
51 steps
52 .into_iter()
53 .take(12)
54 .map(|(step, ms, count)| serde_json::json!({ "step": step, "ms": ms as u64, "count": count }))
55 .collect(),
56 )
57}
58
59pub fn timed<T>(step: &str, f: impl FnOnce() -> T) -> T {
61 let started = Instant::now();
62 let value = f();
63 note_step(step, started.elapsed().as_millis());
64 value
65}
66
67pub fn slowest_step() -> Option<(String, u128)> {
69 SLOWEST.lock().ok().and_then(|slowest| slowest.clone())
70}
71
72pub fn path() -> PathBuf {
74 crate::agent::global_instructions_dir()
75 .join("logs")
76 .join("slow.jsonl")
77}
78
79fn secret_flag(flag: &str) -> bool {
80 let name = flag.trim_start_matches('-').to_ascii_lowercase();
81 matches!(
82 name.as_str(),
83 "token"
84 | "key"
85 | "api-key"
86 | "apikey"
87 | "secret"
88 | "password"
89 | "passwd"
90 | "credential"
91 | "credentials"
92 | "auth"
93 | "authorization"
94 | "bearer"
95 | "cookie"
96 | "session-token"
97 )
98}
99
100fn secret_name(name: &str) -> bool {
101 let name = name.to_ascii_lowercase();
102 [
103 "token",
104 "secret",
105 "password",
106 "passwd",
107 "credential",
108 "api_key",
109 "api-key",
110 "apikey",
111 "authorization",
112 "cookie",
113 ]
114 .iter()
115 .any(|word| name.contains(word))
116}
117
118fn secret_value(value: &str) -> bool {
119 let token_like = |prefix: &str, min: usize| value.starts_with(prefix) && value.len() >= min;
120 token_like("sk-", 19)
121 || token_like("ghp_", 24)
122 || token_like("gho_", 24)
123 || token_like("ghs_", 24)
124 || token_like("ghu_", 24)
125 || token_like("ghr_", 24)
126 || token_like("xoxb-", 15)
127 || token_like("xoxp-", 15)
128 || (value.starts_with("eyJ") && value.matches('.').count() == 2 && value.len() > 40)
129 || (value.len() >= 40
130 && value.bytes().all(|b| {
131 b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'_' | b'-' | b'=')
132 }))
133}
134
135pub fn redact_args(args: &[String]) -> Vec<String> {
137 let mut out = Vec::with_capacity(args.len());
138 let mut hide_next = false;
139 for arg in args {
140 if hide_next {
141 out.push("[redacted]".to_string());
142 hide_next = false;
143 continue;
144 }
145 if let Some((flag, _)) = arg.split_once('=') {
146 if flag.starts_with('-') && secret_flag(flag) {
147 out.push(format!("{flag}=[redacted]"));
148 continue;
149 }
150 if !flag.starts_with('-')
151 && !flag.is_empty()
152 && flag.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
153 && secret_name(flag)
154 {
155 out.push(format!("{flag}=[redacted]"));
156 continue;
157 }
158 }
159 if arg.starts_with('-') && secret_flag(arg) {
160 out.push(arg.clone());
161 hide_next = true;
162 continue;
163 }
164 if secret_value(arg) {
165 out.push("[redacted]".to_string());
166 continue;
167 }
168 if arg.chars().count() > 200 {
169 out.push(format!("{}…", arg.chars().take(200).collect::<String>()));
170 } else {
171 out.push(arg.clone());
172 }
173 }
174 out
175}
176
177fn rotate(path: &std::path::Path) {
182 if !std::fs::metadata(path).is_ok_and(|meta| meta.len() > ROTATE_BYTES) {
183 return;
184 }
185 let Some(dir) = path.parent() else { return };
186 let now = std::time::SystemTime::now()
187 .duration_since(std::time::UNIX_EPOCH)
188 .map(|elapsed| elapsed.as_millis())
189 .unwrap_or_default();
190 if std::fs::rename(
191 path,
192 dir.join(format!("slow.{now:013}-{}.jsonl", std::process::id())),
193 )
194 .is_err()
195 {
196 return;
197 }
198 let mut rotated: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
199 .map(|entries| {
200 entries
201 .flatten()
202 .map(|entry| entry.path())
203 .filter(|path| {
204 path.file_name()
205 .and_then(|name| name.to_str())
206 .is_some_and(|name| {
207 name.starts_with("slow.")
208 && name.ends_with(".jsonl")
209 && name != "slow.jsonl"
210 })
211 })
212 .collect()
213 })
214 .unwrap_or_default();
215 rotated.sort();
216 let excess = rotated.len().saturating_sub(KEEP_ROTATED);
217 for old in &rotated[..excess] {
218 let _ = std::fs::remove_file(old);
219 }
220}
221
222#[allow(clippy::too_many_arguments)]
225pub fn record(
226 kind: &str,
227 name: &str,
228 args: &[String],
229 ms: u128,
230 outcome: &str,
231 slowest: Option<(String, u128)>,
232 caller: impl FnOnce() -> Option<String>,
233) -> bool {
234 if ms <= SLOW_MS {
235 return false;
236 }
237 let line = serde_json::json!({
238 "v": 1,
239 "at": supercode_interchange::sidecar::ms_to_rfc3339(
240 std::time::SystemTime::now()
241 .duration_since(std::time::UNIX_EPOCH)
242 .map(|elapsed| elapsed.as_millis() as i64)
243 .unwrap_or_default(),
244 ),
245 "kind": kind,
246 "name": name,
247 "args": redact_args(args),
248 "ms": ms as u64,
249 "outcome": outcome,
250 "machine": crate::mailbox::local_machine_name(),
251 "pid": std::process::id(),
252 "caller": caller().map(|address| serde_json::json!({ "address": address })),
253 "slowest": slowest.map(|(step, ms)| serde_json::json!({ "step": step, "ms": ms as u64 })),
254 "steps": if kind == "command" { step_totals() } else { serde_json::Value::Null },
255 });
256 let path = path();
257 if let Some(dir) = path.parent() {
258 let _ = std::fs::create_dir_all(dir);
259 }
260 rotate(&path);
261 std::fs::OpenOptions::new()
262 .create(true)
263 .append(true)
264 .open(&path)
265 .and_then(|mut file| writeln!(file, "{line}"))
266 .is_ok()
267}