Skip to main content

spring_lsp/
logging.rs

1//! 日志系统配置和管理
2//!
3//! 本模块提供 spring-lsp 的日志系统配置,支持:
4//! - 通过环境变量配置日志级别
5//! - 输出到文件和标准错误流
6//! - 结构化日志(包含时间戳、级别、模块等)
7//! - 调试模式支持
8//! - 确保日志不干扰 LSP 协议通信(LSP 使用 stdio)
9//!
10//! ## 环境变量
11//!
12//! - `SPRING_LSP_LOG_LEVEL`: 日志级别(trace, debug, info, warn, error),默认为 info
13//! - `SPRING_LSP_VERBOSE`: 启用详细日志模式(设置为 1 或 true)
14//! - `SPRING_LSP_LOG_FILE`: 日志文件路径(可选,如果不设置则只输出到 stderr)
15//!
16//! ## 使用示例
17//!
18//! ```rust,no_run
19//! use spring_lsp::logging::init_logging;
20//!
21//! // 初始化日志系统
22//! init_logging().expect("Failed to initialize logging");
23//!
24//! tracing::info!("Application started");
25//! ```
26
27use std::env;
28use std::fs::OpenOptions;
29use std::io;
30use std::path::PathBuf;
31use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
32
33/// 日志配置
34#[derive(Debug, Clone)]
35pub struct LogConfig {
36    /// 日志级别
37    pub level: String,
38    /// 是否启用详细模式
39    pub verbose: bool,
40    /// 日志文件路径(可选)
41    pub log_file: Option<PathBuf>,
42}
43
44impl Default for LogConfig {
45    fn default() -> Self {
46        Self {
47            level: "info".to_string(),
48            verbose: false,
49            log_file: None,
50        }
51    }
52}
53
54impl LogConfig {
55    /// 从环境变量加载配置
56    pub fn from_env() -> Self {
57        let level = env::var("SPRING_LSP_LOG_LEVEL")
58            .unwrap_or_else(|_| "info".to_string())
59            .to_lowercase();
60
61        let verbose = env::var("SPRING_LSP_VERBOSE")
62            .map(|v| v == "1" || v.to_lowercase() == "true")
63            .unwrap_or(false);
64
65        let log_file = env::var("SPRING_LSP_LOG_FILE").ok().map(PathBuf::from);
66
67        Self {
68            level,
69            verbose,
70            log_file,
71        }
72    }
73
74    /// 创建环境过滤器
75    fn create_env_filter(&self) -> EnvFilter {
76        // 如果设置了 RUST_LOG 环境变量,优先使用它
77        if let Ok(filter) = EnvFilter::try_from_default_env() {
78            return filter;
79        }
80
81        // 否则使用配置的日志级别
82        let level = &self.level;
83
84        // 在详细模式下,显示所有模块的日志
85        if self.verbose {
86            EnvFilter::new(format!("spring_lsp={},lsp_server={}", level, level))
87        } else {
88            // 正常模式下,只显示 spring_lsp 的日志
89            EnvFilter::new(format!("spring_lsp={}", level))
90        }
91    }
92
93    /// 验证日志级别
94    pub fn validate_level(&self) -> Result<(), String> {
95        match self.level.as_str() {
96            "trace" | "debug" | "info" | "warn" | "error" => Ok(()),
97            _ => Err(format!(
98                "Invalid log level: {}. Valid levels are: trace, debug, info, warn, error",
99                self.level
100            )),
101        }
102    }
103}
104
105/// 初始化日志系统
106///
107/// 根据环境变量配置日志系统,支持输出到 stderr 和文件。
108///
109/// # 错误
110///
111/// 如果日志系统初始化失败,返回错误信息。
112///
113/// # 注意
114///
115/// - LSP 协议使用 stdin/stdout 通信,因此日志必须输出到 stderr 或文件
116/// - 此函数应该在程序启动时调用一次
117/// - 重复调用会返回错误
118pub fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
119    let config = LogConfig::from_env();
120    init_logging_with_config(config)
121}
122
123/// 使用指定配置初始化日志系统
124///
125/// # 参数
126///
127/// - `config`: 日志配置
128///
129/// # 错误
130///
131/// 如果日志系统初始化失败,返回错误信息。
132pub fn init_logging_with_config(config: LogConfig) -> Result<(), Box<dyn std::error::Error>> {
133    // 验证日志级别
134    config.validate_level()?;
135
136    let env_filter = config.create_env_filter();
137
138    // 创建 stderr 输出层
139    let stderr_layer = fmt::layer()
140        .with_writer(io::stderr)
141        .with_ansi(atty::is(atty::Stream::Stderr)) // 只在终端时使用颜色
142        .with_target(config.verbose) // 详细模式显示目标模块
143        .with_thread_ids(config.verbose) // 详细模式显示线程 ID
144        .with_thread_names(config.verbose) // 详细模式显示线程名称
145        .with_line_number(config.verbose) // 详细模式显示行号
146        .with_file(config.verbose) // 详细模式显示文件名
147        .with_filter(env_filter.clone());
148
149    // 如果配置了日志文件,创建文件输出层
150    if let Some(log_file) = &config.log_file {
151        // 确保日志文件的父目录存在
152        if let Some(parent) = log_file.parent() {
153            std::fs::create_dir_all(parent)?;
154        }
155
156        // 打开或创建日志文件(追加模式)
157        let file = OpenOptions::new()
158            .create(true)
159            .append(true)
160            .open(log_file)?;
161
162        // 创建文件输出层(使用 JSON 格式以便于解析)
163        let file_layer = fmt::layer()
164            .with_writer(file)
165            .json() // 使用 JSON 格式
166            .with_current_span(true) // 包含当前 span 信息
167            .with_span_list(true) // 包含 span 列表
168            .with_filter(env_filter);
169
170        // 组合两个层
171        tracing_subscriber::registry()
172            .with(stderr_layer)
173            .with(file_layer)
174            .try_init()?;
175    } else {
176        // 只使用 stderr 输出
177        tracing_subscriber::registry()
178            .with(stderr_layer)
179            .try_init()?;
180    }
181
182    // 记录日志系统初始化信息
183    tracing::info!(
184        level = %config.level,
185        verbose = config.verbose,
186        log_file = ?config.log_file,
187        "Logging system initialized"
188    );
189
190    Ok(())
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::env;
197
198    #[test]
199    fn test_log_config_default() {
200        let config = LogConfig::default();
201        assert_eq!(config.level, "info");
202        assert!(!config.verbose);
203        assert!(config.log_file.is_none());
204    }
205
206    #[test]
207    fn test_log_config_from_env() {
208        // 保存原始环境变量
209        let original_level = env::var("SPRING_LSP_LOG_LEVEL").ok();
210        let original_verbose = env::var("SPRING_LSP_VERBOSE").ok();
211        let original_file = env::var("SPRING_LSP_LOG_FILE").ok();
212
213        // 设置测试环境变量
214        env::set_var("SPRING_LSP_LOG_LEVEL", "debug");
215        env::set_var("SPRING_LSP_VERBOSE", "true");
216        env::set_var("SPRING_LSP_LOG_FILE", "/tmp/spring-lsp.log");
217
218        let config = LogConfig::from_env();
219        assert_eq!(config.level, "debug");
220        assert!(config.verbose);
221        assert_eq!(config.log_file, Some(PathBuf::from("/tmp/spring-lsp.log")));
222
223        // 恢复原始环境变量
224        match original_level {
225            Some(v) => env::set_var("SPRING_LSP_LOG_LEVEL", v),
226            None => env::remove_var("SPRING_LSP_LOG_LEVEL"),
227        }
228        match original_verbose {
229            Some(v) => env::set_var("SPRING_LSP_VERBOSE", v),
230            None => env::remove_var("SPRING_LSP_VERBOSE"),
231        }
232        match original_file {
233            Some(v) => env::set_var("SPRING_LSP_LOG_FILE", v),
234            None => env::remove_var("SPRING_LSP_LOG_FILE"),
235        }
236    }
237
238    #[test]
239    fn test_log_config_verbose_variants() {
240        // 保存原始环境变量
241        let original = env::var("SPRING_LSP_VERBOSE").ok();
242
243        // 测试 "1"
244        env::set_var("SPRING_LSP_VERBOSE", "1");
245        let config = LogConfig::from_env();
246        assert!(config.verbose);
247
248        // 测试 "true"
249        env::set_var("SPRING_LSP_VERBOSE", "true");
250        let config = LogConfig::from_env();
251        assert!(config.verbose);
252
253        // 测试 "TRUE"
254        env::set_var("SPRING_LSP_VERBOSE", "TRUE");
255        let config = LogConfig::from_env();
256        assert!(config.verbose);
257
258        // 测试 "false"
259        env::set_var("SPRING_LSP_VERBOSE", "false");
260        let config = LogConfig::from_env();
261        assert!(!config.verbose);
262
263        // 测试未设置
264        env::remove_var("SPRING_LSP_VERBOSE");
265        let config = LogConfig::from_env();
266        assert!(!config.verbose);
267
268        // 恢复原始环境变量
269        match original {
270            Some(v) => env::set_var("SPRING_LSP_VERBOSE", v),
271            None => env::remove_var("SPRING_LSP_VERBOSE"),
272        }
273    }
274
275    #[test]
276    fn test_validate_level() {
277        let valid_levels = vec!["trace", "debug", "info", "warn", "error"];
278        for level in valid_levels {
279            let config = LogConfig {
280                level: level.to_string(),
281                ..Default::default()
282            };
283            assert!(config.validate_level().is_ok());
284        }
285
286        let invalid_config = LogConfig {
287            level: "invalid".to_string(),
288            ..Default::default()
289        };
290        assert!(invalid_config.validate_level().is_err());
291    }
292
293    #[test]
294    fn test_create_env_filter() {
295        let config = LogConfig {
296            level: "debug".to_string(),
297            verbose: false,
298            log_file: None,
299        };
300        let _filter = config.create_env_filter();
301        // EnvFilter 已创建,无法直接测试其内容
302        // 只验证创建成功即可
303
304        let verbose_config = LogConfig {
305            level: "info".to_string(),
306            verbose: true,
307            log_file: None,
308        };
309        let _filter = verbose_config.create_env_filter();
310        // EnvFilter 已创建,无法直接测试其内容
311        // 只验证创建成功即可
312    }
313
314    #[test]
315    fn test_log_config_case_insensitive() {
316        // 保存原始环境变量
317        let original = env::var("SPRING_LSP_LOG_LEVEL").ok();
318
319        // 测试大写
320        env::set_var("SPRING_LSP_LOG_LEVEL", "DEBUG");
321        let config = LogConfig::from_env();
322        assert_eq!(config.level, "debug");
323
324        // 测试混合大小写
325        env::set_var("SPRING_LSP_LOG_LEVEL", "WaRn");
326        let config = LogConfig::from_env();
327        assert_eq!(config.level, "warn");
328
329        // 恢复原始环境变量
330        match original {
331            Some(v) => env::set_var("SPRING_LSP_LOG_LEVEL", v),
332            None => env::remove_var("SPRING_LSP_LOG_LEVEL"),
333        }
334    }
335}