use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::Instant;
pub const SLOW_MS: u128 = 1000;
pub const ROTATE_BYTES: u64 = 4 * 1024 * 1024;
static SLOWEST: Mutex<Option<(String, u128)>> = Mutex::new(None);
pub fn note_step(step: &str, ms: u128) {
if let Ok(mut slowest) = SLOWEST.lock() {
if slowest.as_ref().is_none_or(|(_, longest)| ms > *longest) {
*slowest = Some((step.to_string(), ms));
}
}
}
pub fn timed<T>(step: &str, f: impl FnOnce() -> T) -> T {
let started = Instant::now();
let value = f();
note_step(step, started.elapsed().as_millis());
value
}
pub fn slowest_step() -> Option<(String, u128)> {
SLOWEST.lock().ok().and_then(|slowest| slowest.clone())
}
pub fn path() -> PathBuf {
crate::agent::global_instructions_dir()
.join("logs")
.join("slow.jsonl")
}
fn secret_flag(flag: &str) -> bool {
let name = flag.trim_start_matches('-').to_ascii_lowercase();
matches!(
name.as_str(),
"token"
| "key"
| "api-key"
| "apikey"
| "secret"
| "password"
| "passwd"
| "credential"
| "credentials"
| "auth"
| "authorization"
| "bearer"
| "cookie"
| "session-token"
)
}
fn secret_name(name: &str) -> bool {
let name = name.to_ascii_lowercase();
[
"token",
"secret",
"password",
"passwd",
"credential",
"api_key",
"api-key",
"apikey",
"authorization",
"cookie",
]
.iter()
.any(|word| name.contains(word))
}
fn secret_value(value: &str) -> bool {
let token_like = |prefix: &str, min: usize| value.starts_with(prefix) && value.len() >= min;
token_like("sk-", 19)
|| token_like("ghp_", 24)
|| token_like("gho_", 24)
|| token_like("ghs_", 24)
|| token_like("ghu_", 24)
|| token_like("ghr_", 24)
|| token_like("xoxb-", 15)
|| token_like("xoxp-", 15)
|| (value.starts_with("eyJ") && value.matches('.').count() == 2 && value.len() > 40)
|| (value.len() >= 40
&& value.bytes().all(|b| {
b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'_' | b'-' | b'=')
}))
}
pub fn redact_args(args: &[String]) -> Vec<String> {
let mut out = Vec::with_capacity(args.len());
let mut hide_next = false;
for arg in args {
if hide_next {
out.push("[redacted]".to_string());
hide_next = false;
continue;
}
if let Some((flag, _)) = arg.split_once('=') {
if flag.starts_with('-') && secret_flag(flag) {
out.push(format!("{flag}=[redacted]"));
continue;
}
if !flag.starts_with('-')
&& !flag.is_empty()
&& flag.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
&& secret_name(flag)
{
out.push(format!("{flag}=[redacted]"));
continue;
}
}
if arg.starts_with('-') && secret_flag(arg) {
out.push(arg.clone());
hide_next = true;
continue;
}
if secret_value(arg) {
out.push("[redacted]".to_string());
continue;
}
if arg.chars().count() > 200 {
out.push(format!("{}…", arg.chars().take(200).collect::<String>()));
} else {
out.push(arg.clone());
}
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn record(
kind: &str,
name: &str,
args: &[String],
ms: u128,
outcome: &str,
slowest: Option<(String, u128)>,
caller: impl FnOnce() -> Option<String>,
) -> bool {
if ms <= SLOW_MS {
return false;
}
let line = serde_json::json!({
"v": 1,
"at": supercode_interchange::sidecar::ms_to_rfc3339(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_millis() as i64)
.unwrap_or_default(),
),
"kind": kind,
"name": name,
"args": redact_args(args),
"ms": ms as u64,
"outcome": outcome,
"machine": crate::mailbox::local_machine_name(),
"pid": std::process::id(),
"caller": caller().map(|address| serde_json::json!({ "address": address })),
"slowest": slowest.map(|(step, ms)| serde_json::json!({ "step": step, "ms": ms as u64 })),
});
let path = path();
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if std::fs::metadata(&path).is_ok_and(|meta| meta.len() > ROTATE_BYTES) {
let _ = std::fs::rename(&path, path.with_extension("jsonl.1"));
}
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.and_then(|mut file| writeln!(file, "{line}"))
.is_ok()
}