use std::env;
use std::fs::OpenOptions;
use std::io;
use std::path::PathBuf;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
#[derive(Debug, Clone)]
pub struct LogConfig {
pub level: String,
pub verbose: bool,
pub log_file: Option<PathBuf>,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
verbose: false,
log_file: None,
}
}
}
impl LogConfig {
pub fn from_env() -> Self {
let level = env::var("SUMMER_LSP_LOG_LEVEL")
.unwrap_or_else(|_| "info".to_string())
.to_lowercase();
let verbose = env::var("SUMMER_LSP_VERBOSE")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
let log_file = env::var("SUMMER_LSP_LOG_FILE").ok().map(PathBuf::from);
Self {
level,
verbose,
log_file,
}
}
fn create_env_filter(&self) -> EnvFilter {
if let Ok(filter) = EnvFilter::try_from_default_env() {
return filter;
}
let level = &self.level;
if self.verbose {
EnvFilter::new(format!("summer_lsp={},lsp_server={}", level, level))
} else {
EnvFilter::new(format!("summer_lsp={}", level))
}
}
pub fn validate_level(&self) -> Result<(), String> {
match self.level.as_str() {
"trace" | "debug" | "info" | "warn" | "error" => Ok(()),
_ => Err(format!(
"Invalid log level: {}. Valid levels are: trace, debug, info, warn, error",
self.level
)),
}
}
}
pub fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
let config = LogConfig::from_env();
init_logging_with_config(config)
}
pub fn init_logging_with_config(config: LogConfig) -> Result<(), Box<dyn std::error::Error>> {
config.validate_level()?;
let env_filter = config.create_env_filter();
let stderr_layer = fmt::layer()
.with_writer(io::stderr)
.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());
if let Some(log_file) = &config.log_file {
if let Some(parent) = log_file.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(log_file)?;
let file_layer = fmt::layer()
.with_writer(file)
.json() .with_current_span(true) .with_span_list(true) .with_filter(env_filter);
tracing_subscriber::registry()
.with(stderr_layer)
.with(file_layer)
.try_init()?;
} else {
tracing_subscriber::registry()
.with(stderr_layer)
.try_init()?;
}
tracing::info!(
level = %config.level,
verbose = config.verbose,
log_file = ?config.log_file,
"Logging system initialized"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_log_config_default() {
let config = LogConfig::default();
assert_eq!(config.level, "info");
assert!(!config.verbose);
assert!(config.log_file.is_none());
}
#[test]
fn test_log_config_from_env() {
let original_level = env::var("SUMMER_LSP_LOG_LEVEL").ok();
let original_verbose = env::var("SUMMER_LSP_VERBOSE").ok();
let original_file = env::var("SUMMER_LSP_LOG_FILE").ok();
env::set_var("SUMMER_LSP_LOG_LEVEL", "debug");
env::set_var("SUMMER_LSP_VERBOSE", "true");
env::set_var("SUMMER_LSP_LOG_FILE", "/tmp/summer-lsp.log");
let config = LogConfig::from_env();
assert_eq!(config.level, "debug");
assert!(config.verbose);
assert_eq!(config.log_file, Some(PathBuf::from("/tmp/summer-lsp.log")));
match original_level {
Some(v) => env::set_var("SUMMER_LSP_LOG_LEVEL", v),
None => env::remove_var("SUMMER_LSP_LOG_LEVEL"),
}
match original_verbose {
Some(v) => env::set_var("SUMMER_LSP_VERBOSE", v),
None => env::remove_var("SUMMER_LSP_VERBOSE"),
}
match original_file {
Some(v) => env::set_var("SUMMER_LSP_LOG_FILE", v),
None => env::remove_var("SUMMER_LSP_LOG_FILE"),
}
}
#[test]
fn test_log_config_verbose_variants() {
let original = env::var("SUMMER_LSP_VERBOSE").ok();
env::set_var("SUMMER_LSP_VERBOSE", "1");
let config = LogConfig::from_env();
assert!(config.verbose);
env::set_var("SUMMER_LSP_VERBOSE", "true");
let config = LogConfig::from_env();
assert!(config.verbose);
env::set_var("SUMMER_LSP_VERBOSE", "TRUE");
let config = LogConfig::from_env();
assert!(config.verbose);
env::set_var("SUMMER_LSP_VERBOSE", "false");
let config = LogConfig::from_env();
assert!(!config.verbose);
env::remove_var("SUMMER_LSP_VERBOSE");
let config = LogConfig::from_env();
assert!(!config.verbose);
match original {
Some(v) => env::set_var("SUMMER_LSP_VERBOSE", v),
None => env::remove_var("SUMMER_LSP_VERBOSE"),
}
}
#[test]
fn test_validate_level() {
let valid_levels = vec!["trace", "debug", "info", "warn", "error"];
for level in valid_levels {
let config = LogConfig {
level: level.to_string(),
..Default::default()
};
assert!(config.validate_level().is_ok());
}
let invalid_config = LogConfig {
level: "invalid".to_string(),
..Default::default()
};
assert!(invalid_config.validate_level().is_err());
}
#[test]
fn test_create_env_filter() {
let config = LogConfig {
level: "debug".to_string(),
verbose: false,
log_file: None,
};
let _filter = config.create_env_filter();
let verbose_config = LogConfig {
level: "info".to_string(),
verbose: true,
log_file: None,
};
let _filter = verbose_config.create_env_filter();
}
#[test]
fn test_log_config_case_insensitive() {
let original = env::var("SUMMER_LSP_LOG_LEVEL").ok();
env::set_var("SUMMER_LSP_LOG_LEVEL", "DEBUG");
let config = LogConfig::from_env();
assert_eq!(config.level, "debug");
env::set_var("SUMMER_LSP_LOG_LEVEL", "WaRn");
let config = LogConfig::from_env();
assert_eq!(config.level, "warn");
match original {
Some(v) => env::set_var("SUMMER_LSP_LOG_LEVEL", v),
None => env::remove_var("SUMMER_LSP_LOG_LEVEL"),
}
}
}