Skip to main content

codex_convert_proxy/
logger.rs

1//! Logging module based on tracing ecosystem.
2//!
3//! This module provides structured logging with:
4//! - Multi-output (stdout + file)
5//! - Sensitive data masking
6//! - Request lifecycle tracking
7
8use std::cmp::min;
9use std::path::Path;
10use tracing::info;
11use tracing_subscriber::{
12    fmt::{self, format::FmtSpan},
13    layer::SubscriberExt,
14    util::SubscriberInitExt,
15    EnvFilter, Layer, Registry,
16};
17
18/// Global log configuration.
19static LOG_CONFIG: std::sync::OnceLock<LogConfig> = std::sync::OnceLock::new();
20
21struct LogConfig {
22    #[allow(dead_code)]
23    log_body: bool,
24    #[allow(dead_code)]
25    log_headers: bool,
26}
27
28/// Initialize the logging system.
29///
30/// Configures tracing with multiple outputs:
31/// - stdout: terminal output (timeline level)
32/// - file: detailed log file (all levels)
33pub fn init_logging(log_dir: &Path, log_body: bool, log_headers: bool) -> anyhow::Result<()> {
34    std::fs::create_dir_all(log_dir)?;
35
36    // Create file appender
37    let file_appender = tracing_appender::rolling::daily(log_dir, "proxy.log");
38    let file_layer = fmt::layer()
39        .with_writer(file_appender)
40        .with_target(false)
41        .with_thread_ids(false)
42        .with_file(false)
43        .with_line_number(false)
44        .with_ansi(false)
45        .with_span_events(FmtSpan::CLOSE)
46        .with_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")));
47
48    // Terminal output
49    let stdout_layer = fmt::layer()
50        .with_writer(std::io::stdout)
51        .with_target(false)
52        .with_thread_ids(false)
53        .with_ansi(true)
54        .with_filter(create_timeline_filter());
55
56    // Initialize subscriber
57    Registry::default()
58        .with(file_layer)
59        .with(stdout_layer)
60        .try_init()
61        .map_err(|e| anyhow::anyhow!("Failed to initialize logging: {}", e))?;
62
63    // Store global config
64    LOG_CONFIG.get_or_init(|| LogConfig {
65        log_body,
66        log_headers,
67    });
68
69    info!("Logging initialized: {}", log_dir.display());
70    Ok(())
71}
72
73/// Create timeline filter (info level for timeline events).
74fn create_timeline_filter() -> EnvFilter {
75    EnvFilter::builder()
76        .parse("info")
77        .unwrap_or_else(|_| EnvFilter::new("info"))
78}
79
80/// Check if header is sensitive (should be masked in logs).
81pub fn is_sensitive_header(name: &str) -> bool {
82    let lower = name.to_lowercase();
83    lower == "x-api-key"
84        || lower == "authorization"
85        || lower == "api-key"
86        || lower == "x-api-token"
87        || lower == "cookie"
88        || lower == "set-cookie"
89}
90
91/// Mask sensitive values for display.
92pub fn mask_sensitive(value: &str) -> String {
93    if value.len() <= 10 {
94        return "***".to_string();
95    }
96    if value.starts_with("Bearer ") {
97        // 完全隐藏 Bearer token,只保留前缀标识
98        return "Bearer ***".to_string();
99    }
100    // 对其他敏感值也完全隐藏
101    format!("{}***", &value[..min(6, value.len())])
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_mask_sensitive() {
110        assert_eq!(mask_sensitive("short"), "***");
111        assert_eq!(mask_sensitive("Bearer sk-xxx"), "Bearer ***");
112        assert_eq!(
113            mask_sensitive("Bearer sk-project-12345"),
114            "Bearer ***"
115        );
116        assert_eq!(
117            mask_sensitive("sk-ant-api03-xxxxxxxxxxxx"),
118            "sk-ant***"
119        );
120    }
121
122    #[test]
123    fn test_is_sensitive_header() {
124        assert!(is_sensitive_header("x-api-key"));
125        assert!(is_sensitive_header("X-API-KEY"));
126        assert!(is_sensitive_header("authorization"));
127        assert!(is_sensitive_header("Authorization"));
128        assert!(!is_sensitive_header("content-type"));
129        assert!(!is_sensitive_header("x-request-id"));
130    }
131}