1use crate::{config, paths};
2use serde_json::Value;
3use std::collections::HashSet;
4use std::fs::{self, OpenOptions};
5use std::io::{self, Write};
6use std::path::Path;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10pub const MAX_LOG_BYTES: u64 = 20 * 1024 * 1024;
11
12static STDERR_SUPPRESSION_DEPTH: AtomicUsize = AtomicUsize::new(0);
13
14pub const REDACT_KEYS: [&str; 14] = [
15 "authorization",
16 "access",
17 "access_token",
18 "refresh",
19 "refresh_token",
20 "id_token",
21 "code",
22 "code_verifier",
23 "chatgpt-account-id",
24 "cookie",
25 "set-cookie",
26 "x-api-key",
27 "apikey",
28 "api_key",
29];
30
31pub fn log_file() -> std::path::PathBuf {
32 paths::log_file()
33}
34
35#[must_use]
36pub struct StderrSuppressionGuard;
37
38impl Drop for StderrSuppressionGuard {
39 fn drop(&mut self) {
40 STDERR_SUPPRESSION_DEPTH.fetch_sub(1, Ordering::Relaxed);
41 }
42}
43
44pub fn suppress_stderr() -> StderrSuppressionGuard {
45 STDERR_SUPPRESSION_DEPTH.fetch_add(1, Ordering::Relaxed);
46 StderrSuppressionGuard
47}
48
49fn stderr_suppressed() -> bool {
50 STDERR_SUPPRESSION_DEPTH.load(Ordering::Relaxed) > 0
51}
52
53fn should_mirror_to_stderr(level: &str) -> bool {
54 !stderr_suppressed() && (matches!(level, "warn" | "error") || config::log_stderr())
55}
56
57#[derive(Clone)]
58pub struct Logger {
59 service: String,
60 base: serde_json::Map<String, Value>,
61}
62
63impl Logger {
64 pub fn child(&self, bindings: serde_json::Map<String, Value>) -> Logger {
65 let mut merged = self.base.clone();
66 merged.extend(bindings);
67 Logger {
68 service: self.service.clone(),
69 base: merged,
70 }
71 }
72
73 pub fn debug(&self, msg: &str, fields: Option<serde_json::Map<String, Value>>) {
74 self.emit("debug", msg, fields)
75 }
76
77 pub fn info(&self, msg: &str, fields: Option<serde_json::Map<String, Value>>) {
78 self.emit("info", msg, fields)
79 }
80
81 pub fn warn(&self, msg: &str, fields: Option<serde_json::Map<String, Value>>) {
82 self.emit("warn", msg, fields)
83 }
84
85 pub fn error(&self, msg: &str, fields: Option<serde_json::Map<String, Value>>) {
86 self.emit("error", msg, fields)
87 }
88
89 fn emit(&self, level: &str, msg: &str, fields: Option<serde_json::Map<String, Value>>) {
90 let mut body = serde_json::Map::new();
91 body.insert("t".into(), Value::String(now_iso8601()));
92 body.insert("level".into(), Value::String(level.to_string()));
93 body.insert("service".into(), Value::String(self.service.clone()));
94 body.insert("msg".into(), Value::String(msg.to_string()));
95
96 let mut merged = self.base.clone();
97 if let Some(fields) = fields {
98 merged.extend(fields);
99 }
100 if !merged.is_empty() {
101 body.insert("fields".into(), redact_value(Value::Object(merged)));
102 }
103
104 let line = Value::Object(body).to_string();
105
106 let mirror_to_stderr = should_mirror_to_stderr(level);
107 if mirror_to_stderr {
108 let _ = writeln!(io::stderr(), "{line}");
109 }
110
111 if write_log_line(&line).is_err() && mirror_to_stderr {
112 }
114 }
115}
116
117pub fn create_logger(service: &str) -> Logger {
118 Logger {
119 service: service.to_string(),
120 base: serde_json::Map::new(),
121 }
122}
123
124fn write_log_line(line: &str) -> io::Result<()> {
125 let file = log_file();
126 if let Some(dir) = file.parent() {
127 create_dir(dir, 0o700)?;
128 }
129
130 if fs::metadata(&file).is_ok_and(|meta| meta.len() > MAX_LOG_BYTES) {
131 rotate_file(&file)?;
132 }
133
134 let mut out = OpenOptions::new().create(true).append(true).open(&file)?;
135 out.write_all(line.as_bytes())?;
136 out.write_all(b"\n")?;
137 Ok(())
138}
139
140fn rotate_file(path: &Path) -> io::Result<()> {
141 let ts = SystemTime::now()
142 .duration_since(UNIX_EPOCH)
143 .unwrap_or_default()
144 .as_millis();
145 let rotated = path.with_extension(format!("{ts}"));
146 fs::rename(path, rotated)?;
147 Ok(())
148}
149
150fn create_dir(path: &Path, mode: u32) -> io::Result<()> {
151 fs::create_dir_all(path)?;
152 set_mode(path, mode);
153 Ok(())
154}
155
156fn set_mode(path: &Path, mode: u32) {
157 #[cfg(unix)]
158 {
159 use std::os::unix::fs::PermissionsExt;
160 if let Ok(meta) = fs::metadata(path) {
161 let mut perm = meta.permissions();
162 perm.set_mode(mode);
163 let _ = fs::set_permissions(path, perm);
164 }
165 }
166}
167
168fn now_iso8601() -> String {
169 let now = time::OffsetDateTime::now_utc();
170 let format = time::format_description::parse_borrowed::<3>(
171 "[year]-[month]-[day]T[hour]:[minute]:[second]Z",
172 )
173 .unwrap();
174 now.format(&format).unwrap_or_else(|_| String::new())
175}
176
177pub fn redact_value(value: Value) -> Value {
178 redact_with_depth(value, 0)
179}
180
181fn redact_with_depth(value: Value, depth: u8) -> Value {
182 if depth > 6 {
183 return Value::String("[depth-limit]".into());
184 }
185
186 match value {
187 Value::String(s) => {
188 if config::log_verbose() {
189 Value::String(s)
190 } else if s.len() > 4000 {
191 Value::String(format!("{}…[{} more]", &s[..4000], s.len() - 4000))
192 } else {
193 Value::String(s)
194 }
195 }
196 Value::Array(values) => Value::Array(
197 values
198 .into_iter()
199 .map(|v| redact_with_depth(v, depth + 1))
200 .collect(),
201 ),
202 Value::Object(fields) => {
203 let mut out = serde_json::Map::new();
204 for (key, value) in fields {
205 if REDACT_KEYS.contains(&key.to_lowercase().as_str()) {
206 out.insert(key, redact_key_redaction(value));
207 } else {
208 out.insert(key, redact_with_depth(value, depth + 1));
209 }
210 }
211 Value::Object(out)
212 }
213 value => value,
214 }
215}
216
217fn redact_key_redaction(value: Value) -> Value {
218 match value {
219 Value::String(s) => Value::String(format!("[redacted len={}]", s.len())),
220 _ => Value::String("[redacted]".to_string()),
221 }
222}
223
224pub fn redacted_keys() -> HashSet<&'static str> {
225 REDACT_KEYS.iter().copied().collect()
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use std::sync::Mutex;
232
233 static STDERR_TEST_LOCK: Mutex<()> = Mutex::new(());
234
235 #[test]
236 fn stderr_suppression_disables_level_mirroring() {
237 let _lock = STDERR_TEST_LOCK.lock().unwrap();
238 assert!(should_mirror_to_stderr("warn"));
239
240 {
241 let _guard = suppress_stderr();
242 assert!(!should_mirror_to_stderr("warn"));
243 assert!(!should_mirror_to_stderr("error"));
244 }
245
246 assert!(should_mirror_to_stderr("warn"));
247 }
248
249 #[test]
250 fn stderr_suppression_supports_nested_guards() {
251 let _lock = STDERR_TEST_LOCK.lock().unwrap();
252 let outer = suppress_stderr();
253 let inner = suppress_stderr();
254 assert!(!should_mirror_to_stderr("warn"));
255
256 drop(inner);
257 assert!(!should_mirror_to_stderr("warn"));
258
259 drop(outer);
260 assert!(should_mirror_to_stderr("warn"));
261 }
262}