1use 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#[derive(Debug, Clone)]
35pub struct LogConfig {
36 pub level: String,
38 pub verbose: bool,
40 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 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 fn create_env_filter(&self) -> EnvFilter {
76 if let Ok(filter) = EnvFilter::try_from_default_env() {
78 return filter;
79 }
80
81 let level = &self.level;
83
84 if self.verbose {
86 EnvFilter::new(format!("spring_lsp={},lsp_server={}", level, level))
87 } else {
88 EnvFilter::new(format!("spring_lsp={}", level))
90 }
91 }
92
93 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
105pub fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
119 let config = LogConfig::from_env();
120 init_logging_with_config(config)
121}
122
123pub fn init_logging_with_config(config: LogConfig) -> Result<(), Box<dyn std::error::Error>> {
133 config.validate_level()?;
135
136 let env_filter = config.create_env_filter();
137
138 let stderr_layer = fmt::layer()
140 .with_writer(io::stderr)
141 .with_ansi(atty::is(atty::Stream::Stderr)) .with_target(config.verbose) .with_thread_ids(config.verbose) .with_thread_names(config.verbose) .with_line_number(config.verbose) .with_file(config.verbose) .with_filter(env_filter.clone());
148
149 if let Some(log_file) = &config.log_file {
151 if let Some(parent) = log_file.parent() {
153 std::fs::create_dir_all(parent)?;
154 }
155
156 let file = OpenOptions::new()
158 .create(true)
159 .append(true)
160 .open(log_file)?;
161
162 let file_layer = fmt::layer()
164 .with_writer(file)
165 .json() .with_current_span(true) .with_span_list(true) .with_filter(env_filter);
169
170 tracing_subscriber::registry()
172 .with(stderr_layer)
173 .with(file_layer)
174 .try_init()?;
175 } else {
176 tracing_subscriber::registry()
178 .with(stderr_layer)
179 .try_init()?;
180 }
181
182 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 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 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 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 let original = env::var("SPRING_LSP_VERBOSE").ok();
242
243 env::set_var("SPRING_LSP_VERBOSE", "1");
245 let config = LogConfig::from_env();
246 assert!(config.verbose);
247
248 env::set_var("SPRING_LSP_VERBOSE", "true");
250 let config = LogConfig::from_env();
251 assert!(config.verbose);
252
253 env::set_var("SPRING_LSP_VERBOSE", "TRUE");
255 let config = LogConfig::from_env();
256 assert!(config.verbose);
257
258 env::set_var("SPRING_LSP_VERBOSE", "false");
260 let config = LogConfig::from_env();
261 assert!(!config.verbose);
262
263 env::remove_var("SPRING_LSP_VERBOSE");
265 let config = LogConfig::from_env();
266 assert!(!config.verbose);
267
268 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 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 }
313
314 #[test]
315 fn test_log_config_case_insensitive() {
316 let original = env::var("SPRING_LSP_LOG_LEVEL").ok();
318
319 env::set_var("SPRING_LSP_LOG_LEVEL", "DEBUG");
321 let config = LogConfig::from_env();
322 assert_eq!(config.level, "debug");
323
324 env::set_var("SPRING_LSP_LOG_LEVEL", "WaRn");
326 let config = LogConfig::from_env();
327 assert_eq!(config.level, "warn");
328
329 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}