1use regex::Regex;
13use std::io::Write;
14use std::sync::Mutex;
15use std::sync::OnceLock;
16use std::sync::atomic::{AtomicBool, Ordering};
17use tracing_appender::non_blocking::WorkerGuard;
18use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
19
20static NO_COLOR: AtomicBool = AtomicBool::new(false);
22static QUIET: AtomicBool = AtomicBool::new(false);
23static VERBOSE: AtomicBool = AtomicBool::new(false);
24static DEBUG: AtomicBool = AtomicBool::new(false);
25
26static WORKER_GUARD: Mutex<Option<WorkerGuard>> = Mutex::new(None);
27
28pub fn flush() {
30 if let Ok(mut guard) = WORKER_GUARD.lock() {
31 let _ = guard.take(); }
33}
34
35pub fn init(no_color: bool, quiet: bool, verbose: bool, debug: bool) {
37 NO_COLOR.store(no_color, Ordering::Relaxed);
38 QUIET.store(quiet, Ordering::Relaxed);
39 VERBOSE.store(verbose, Ordering::Relaxed);
40 DEBUG.store(debug, Ordering::Relaxed);
41
42 if no_color {
44 colored::control::set_override(false);
45 }
46
47 let console_filter = std::env::var("RAPS_LOG")
49 .or_else(|_| std::env::var("RUST_LOG"))
50 .map(EnvFilter::new)
51 .unwrap_or_else(|_| {
52 if debug {
53 EnvFilter::new("debug")
54 } else if verbose {
55 EnvFilter::new("info")
56 } else if quiet {
57 EnvFilter::new("error")
58 } else {
59 EnvFilter::new("warn")
60 }
61 });
62
63 let stderr_layer = tracing_subscriber::fmt::layer()
64 .with_writer(RedactingMakeWriter::new(std::io::stderr))
65 .with_ansi(!no_color)
66 .with_target(debug)
67 .without_time()
68 .with_filter(console_filter);
69
70 let log_dir = directories::ProjectDirs::from("com", "autodesk", "raps")
71 .map(|dirs| dirs.data_local_dir().join("logs"))
72 .unwrap_or_else(|| {
73 std::env::current_dir()
74 .unwrap_or_else(|_| std::path::PathBuf::from("."))
75 .join(".raps-logs")
76 });
77
78 let _ = crate::security::create_dir_restricted(&log_dir);
79 cleanup_old_logs(&log_dir, 7);
80
81 let file_appender = tracing_appender::rolling::daily(&log_dir, "raps.log");
82 let (non_blocking_appender, guard) = tracing_appender::non_blocking(file_appender);
83 if let Ok(mut lock) = WORKER_GUARD.lock() {
84 *lock = Some(guard);
85 }
86
87 let file_filter = std::env::var("RAPS_FILE_LOG")
89 .map(EnvFilter::new)
90 .unwrap_or_else(|_| EnvFilter::new("raps=debug,info"));
91
92 let use_json = std::env::var("RAPS_FILE_FORMAT")
94 .map(|v| v.eq_ignore_ascii_case("json"))
95 .unwrap_or(false);
96
97 let redacting_appender = RedactingMakeWriter::new(non_blocking_appender);
98
99 let file_layer: Box<dyn Layer<_> + Send + Sync> = if use_json {
100 Box::new(
101 tracing_subscriber::fmt::layer()
102 .json()
103 .with_writer(redacting_appender)
104 .with_current_span(true)
105 .with_filter(file_filter),
106 )
107 } else {
108 Box::new(
109 tracing_subscriber::fmt::layer()
110 .with_writer(redacting_appender)
111 .with_ansi(false)
112 .with_filter(file_filter),
113 )
114 };
115
116 let _ = tracing_subscriber::registry()
117 .with(stderr_layer)
118 .with(file_layer)
119 .try_init();
120}
121
122#[allow(dead_code)] pub fn no_color() -> bool {
125 NO_COLOR.load(Ordering::Relaxed)
126}
127
128pub fn quiet() -> bool {
130 QUIET.load(Ordering::Relaxed)
131}
132
133pub fn verbose() -> bool {
135 VERBOSE.load(Ordering::Relaxed)
136}
137
138pub fn debug() -> bool {
140 DEBUG.load(Ordering::Relaxed)
141}
142
143pub fn redact_secrets(text: &str) -> String {
145 fn secret_pattern() -> &'static Regex {
146 static PAT: OnceLock<Regex> = OnceLock::new();
147 PAT.get_or_init(|| {
148 Regex::new(r"(?i)(client[_-]?secret|secret[_-]?key|api[_-]?key)\s*[:=]\s*[^\s]+")
149 .expect("secret_pattern regex is valid")
150 })
151 }
152
153 fn token_pattern() -> &'static Regex {
154 static PAT: OnceLock<Regex> = OnceLock::new();
155 PAT.get_or_init(|| {
156 Regex::new(
157 r#"(?i)(token|access[_-]?token|refresh[_-]?token|bearer)\s*"?\s*[:=]\s*"?\s*([A-Za-z0-9_\-\.]{20,})"#,
158 )
159 .expect("token_pattern regex is valid")
160 })
161 }
162
163 fn auth_header_pattern() -> &'static Regex {
164 static PAT: OnceLock<Regex> = OnceLock::new();
165 PAT.get_or_init(|| {
166 Regex::new(r"(?i)(Authorization:\s*(?:Bearer|Basic))\s+[^\s,;]+")
167 .expect("auth_header_pattern regex is valid")
168 })
169 }
170
171 fn cookie_pattern() -> &'static Regex {
172 static PAT: OnceLock<Regex> = OnceLock::new();
173 PAT.get_or_init(|| {
174 Regex::new(r"(?i)((?:Set-)?Cookie:)\s*[^\r\n]+").expect("cookie_pattern regex is valid")
175 })
176 }
177
178 fn x_api_key_pattern() -> &'static Regex {
179 static PAT: OnceLock<Regex> = OnceLock::new();
180 PAT.get_or_init(|| {
181 Regex::new(r"(?i)(X-API-Key:)\s*[^\s,;]+").expect("x_api_key_pattern regex is valid")
182 })
183 }
184
185 fn url_token_pattern() -> &'static Regex {
186 static PAT: OnceLock<Regex> = OnceLock::new();
187 PAT.get_or_init(|| {
188 Regex::new(r"(?i)([?&](?:access_token|apikey|api_key|token)=)[^&\s]+")
189 .expect("url_token_pattern regex is valid")
190 })
191 }
192
193 let redacted = secret_pattern().replace_all(text, "$1: [REDACTED]");
194 let redacted = token_pattern().replace_all(&redacted, "$1: [REDACTED]");
195 let redacted = auth_header_pattern().replace_all(&redacted, "$1 [REDACTED]");
196 let redacted = cookie_pattern().replace_all(&redacted, "$1 [REDACTED]");
197 let redacted = x_api_key_pattern().replace_all(&redacted, "$1 [REDACTED]");
198 url_token_pattern()
199 .replace_all(&redacted, "${1}[REDACTED]")
200 .into_owned()
201}
202
203struct RedactingWriter<W: Write> {
205 inner: W,
206}
207
208impl<W: Write> Write for RedactingWriter<W> {
209 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
210 let text = String::from_utf8_lossy(buf);
211 let redacted = redact_secrets(&text);
212 self.inner.write_all(redacted.as_bytes())?;
213 Ok(buf.len())
214 }
215
216 fn flush(&mut self) -> std::io::Result<()> {
217 self.inner.flush()
218 }
219}
220
221struct RedactingMakeWriter<W> {
223 inner: W,
224}
225
226impl<W> RedactingMakeWriter<W> {
227 fn new(inner: W) -> Self {
228 Self { inner }
229 }
230}
231
232impl<'a, W> tracing_subscriber::fmt::MakeWriter<'a> for RedactingMakeWriter<W>
233where
234 W: tracing_subscriber::fmt::MakeWriter<'a>,
235{
236 type Writer = RedactingWriter<W::Writer>;
237
238 fn make_writer(&'a self) -> Self::Writer {
239 RedactingWriter {
240 inner: self.inner.make_writer(),
241 }
242 }
243}
244
245const MAX_LOG_BYTES: u64 = 50 * 1024 * 1024;
247
248fn cleanup_old_logs(log_dir: &std::path::Path, max_files: usize) {
250 let Ok(entries) = std::fs::read_dir(log_dir) else {
251 return;
252 };
253 let mut files: Vec<_> = entries
254 .flatten()
255 .filter(|e| e.file_name().to_string_lossy().starts_with("raps.log"))
256 .collect();
257 files.sort_by_key(|e| std::cmp::Reverse(e.metadata().and_then(|m| m.modified()).ok()));
259 let mut total_size = 0u64;
260 for (i, file) in files.iter().enumerate() {
261 let size = file.metadata().map(|m| m.len()).unwrap_or(0);
262 total_size += size;
263 if i >= max_files || total_size > MAX_LOG_BYTES {
264 let _ = std::fs::remove_file(file.path());
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
280 fn test_redact_client_secret() {
281 let text = "client_secret: abc123xyz";
282 let redacted = redact_secrets(text);
283 assert!(redacted.contains("[REDACTED]"));
284 assert!(!redacted.contains("abc123xyz"));
285 }
286
287 #[test]
288 fn test_redact_client_secret_underscore() {
289 let text = "client_secret=my_super_secret_value";
290 let redacted = redact_secrets(text);
291 assert!(redacted.contains("[REDACTED]"));
292 assert!(!redacted.contains("my_super_secret_value"));
293 }
294
295 #[test]
296 fn test_redact_api_key() {
297 let text = "api_key: supersecretapikey123";
298 let redacted = redact_secrets(text);
299 assert!(redacted.contains("[REDACTED]"));
300 assert!(!redacted.contains("supersecretapikey123"));
301 }
302
303 #[test]
304 fn test_redact_api_key_dash() {
305 let text = "api-key=myapikey456";
306 let redacted = redact_secrets(text);
307 assert!(redacted.contains("[REDACTED]"));
308 assert!(!redacted.contains("myapikey456"));
309 }
310
311 #[test]
312 fn test_redact_secret_key() {
313 let text = "secret_key: topsecret";
314 let redacted = redact_secrets(text);
315 assert!(redacted.contains("[REDACTED]"));
316 assert!(!redacted.contains("topsecret"));
317 }
318
319 #[test]
320 fn test_redact_access_token() {
321 let text = "access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
322 let redacted = redact_secrets(text);
323 assert!(redacted.contains("[REDACTED]"));
324 assert!(!redacted.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
325 }
326
327 #[test]
328 fn test_redact_refresh_token() {
329 let text = "refresh_token=abcdefghijklmnopqrstuvwxyz";
330 let redacted = redact_secrets(text);
331 assert!(redacted.contains("[REDACTED]"));
332 assert!(!redacted.contains("abcdefghijklmnopqrstuvwxyz"));
333 }
334
335 #[test]
336 fn test_redact_bearer_token() {
337 let text = "bearer: ABCDEFGHIJKLMNOPQRSTUVWXYZ123456";
338 let redacted = redact_secrets(text);
339 assert!(redacted.contains("[REDACTED]"));
340 assert!(!redacted.contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"));
341 }
342
343 #[test]
344 fn test_redact_case_insensitive() {
345 let text1 = "CLIENT_SECRET: secret1";
346 let text2 = "Client_Secret: secret2";
347 let text3 = "client_SECRET: secret3";
348
349 assert!(redact_secrets(text1).contains("[REDACTED]"));
350 assert!(redact_secrets(text2).contains("[REDACTED]"));
351 assert!(redact_secrets(text3).contains("[REDACTED]"));
352 }
353
354 #[test]
355 fn test_redact_preserves_non_secret_text() {
356 let text = "This is a normal message without secrets";
357 let redacted = redact_secrets(text);
358 assert_eq!(text, redacted);
359 }
360
361 #[test]
362 fn test_redact_multiple_secrets() {
363 let text = "client_secret: secret1 api_key: key123";
364 let redacted = redact_secrets(text);
365 assert!(!redacted.contains("secret1"));
366 assert!(!redacted.contains("key123"));
367 assert!(redacted.matches("[REDACTED]").count() >= 2);
368 }
369
370 #[test]
371 fn test_redact_mixed_content() {
372 let text = "Logging in with client_secret: mysecret for user john";
373 let redacted = redact_secrets(text);
374 assert!(redacted.contains("Logging in"));
375 assert!(redacted.contains("for user john"));
376 assert!(!redacted.contains("mysecret"));
377 }
378
379 #[test]
380 fn test_redact_short_token_not_redacted() {
381 let text = "token: short";
383 let redacted = redact_secrets(text);
384 assert!(redacted.contains("short"));
385 }
386
387 #[test]
388 fn test_redact_empty_string() {
389 let text = "";
390 let redacted = redact_secrets(text);
391 assert_eq!(redacted, "");
392 }
393
394 #[test]
395 fn test_redact_json_access_token() {
396 let text = r#""access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.abc123""#;
397 let redacted = redact_secrets(text);
398 assert!(!redacted.contains("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"));
399 }
400
401 #[test]
404 fn test_redact_bearer_header() {
405 let text = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig";
406 let redacted = redact_secrets(text);
407 assert!(redacted.contains("[REDACTED]"));
408 assert!(!redacted.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
409 }
410
411 #[test]
412 fn test_redact_basic_auth_header() {
413 let text = "Authorization: Basic dXNlcjpwYXNzd29yZA==";
414 let redacted = redact_secrets(text);
415 assert!(redacted.contains("[REDACTED]"));
416 assert!(!redacted.contains("dXNlcjpwYXNzd29yZA=="));
417 }
418
419 #[test]
420 fn test_redact_cookie_header() {
421 let text = "Cookie: session_id=abc123; auth_token=xyz789";
422 let redacted = redact_secrets(text);
423 assert!(redacted.contains("[REDACTED]"));
424 assert!(!redacted.contains("abc123"));
425 }
426
427 #[test]
428 fn test_redact_set_cookie_header() {
429 let text = "Set-Cookie: session=secret_value; Path=/; HttpOnly";
430 let redacted = redact_secrets(text);
431 assert!(redacted.contains("[REDACTED]"));
432 assert!(!redacted.contains("secret_value"));
433 }
434
435 #[test]
436 fn test_redact_x_api_key_header() {
437 let text = "X-API-Key: sk-1234567890abcdef";
438 let redacted = redact_secrets(text);
439 assert!(redacted.contains("[REDACTED]"));
440 assert!(!redacted.contains("sk-1234567890abcdef"));
441 }
442
443 #[test]
444 fn test_redact_url_access_token_param() {
445 let text = "https://api.example.com/data?access_token=secret123&format=json";
446 let redacted = redact_secrets(text);
447 assert!(redacted.contains("[REDACTED]"));
448 assert!(!redacted.contains("secret123"));
449 assert!(redacted.contains("format=json"));
450 }
451
452 #[test]
453 fn test_redact_url_apikey_param() {
454 let text = "https://api.example.com/data?apikey=mykey123&limit=10";
455 let redacted = redact_secrets(text);
456 assert!(redacted.contains("[REDACTED]"));
457 assert!(!redacted.contains("mykey123"));
458 }
459
460 #[test]
461 fn test_redact_non_sensitive_unchanged() {
462 let text = "GET /api/v1/projects HTTP/1.1\nHost: example.com\nAccept: application/json";
463 let redacted = redact_secrets(text);
464 assert_eq!(text, redacted);
465 }
466
467 #[test]
468 fn test_redact_combined_patterns() {
469 let text =
470 "Authorization: Bearer eyJtoken123456789012345 Cookie: sess=val X-API-Key: key123";
471 let redacted = redact_secrets(text);
472 assert!(!redacted.contains("eyJtoken"));
473 assert!(!redacted.contains("sess=val"));
474 assert!(!redacted.contains("key123"));
475 }
476
477 #[test]
478 fn test_redacting_writer() {
479 let mut buf = Vec::new();
480 {
481 let mut writer = super::RedactingWriter { inner: &mut buf };
482 write!(writer, "Authorization: Bearer secret_token_value_here").unwrap();
483 }
484 let output = String::from_utf8(buf).unwrap();
485 assert!(output.contains("[REDACTED]"));
486 assert!(!output.contains("secret_token_value_here"));
487 }
488}