use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{Result, anyhow};
use tracing::Level;
#[derive(Debug, Clone, PartialEq)]
pub enum OutputFormat {
Minimal,
Standard,
Full,
Custom(String),
}
impl Default for OutputFormat {
fn default() -> Self {
OutputFormat::Standard
}
}
impl FromStr for OutputFormat {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"minimal" => Ok(OutputFormat::Minimal),
"standard" => Ok(OutputFormat::Standard),
"full" => Ok(OutputFormat::Full),
custom => Ok(OutputFormat::Custom(custom.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct EnvConfig {
pub global_excludes: Vec<String>,
pub tabs: Vec<(String, String, String)>,
pub output_dir: Option<PathBuf>,
pub output_format: OutputFormat,
pub streams: HashMap<String, PathBuf>,
pub stream_configs: HashMap<String, (String, Vec<String>)>,
pub tui_config: Option<TuiConfig>,
}
#[derive(Debug, Clone)]
pub struct TuiConfig {
pub enabled: bool,
pub modules: Vec<String>,
pub level: String,
pub output: Option<PathBuf>,
}
impl Default for EnvConfig {
fn default() -> Self {
Self {
global_excludes: Vec::new(),
tabs: Vec::new(),
output_dir: None,
output_format: OutputFormat::default(),
streams: HashMap::new(),
stream_configs: HashMap::new(),
tui_config: None,
}
}
}
impl EnvConfig {
pub fn from_env() -> Result<Self> {
let mut config = Self::default();
if let Ok(excludes) = std::env::var("KERF_GLOBAL_EXCLUDE") {
config.global_excludes = parse_comma_separated(&excludes);
}
if let Ok(tabs) = std::env::var("KERF_TABS") {
config.tabs = parse_tab_definitions(&tabs)?;
}
if let Ok(dir) = std::env::var("KERF_OUTPUT_DIR") {
config.output_dir = Some(PathBuf::from(dir));
}
if let Ok(format) = std::env::var("KERF_OUTPUT_FORMAT") {
config.output_format = OutputFormat::from_str(&format)?;
}
if let Ok(streams) = std::env::var("KERF_STREAMS") {
config.streams = parse_stream_routing(&streams)?;
}
for (stream_name, _) in &config.streams {
let env_var = format!("KERF_STREAM_{}", stream_name);
if let Ok(stream_config) = std::env::var(&env_var) {
let (level, patterns) = parse_stream_config(&stream_config)?;
config.stream_configs.insert(stream_name.clone(), (level, patterns));
}
}
config.tui_config = parse_tui_config()?;
Ok(config)
}
pub fn has_env_config() -> bool {
std::env::var("KERF_GLOBAL_EXCLUDE").is_ok()
|| std::env::var("KERF_TABS").is_ok()
|| std::env::var("KERF_OUTPUT_DIR").is_ok()
|| std::env::var("KERF_OUTPUT_FORMAT").is_ok()
|| std::env::var("KERF_STREAMS").is_ok()
|| std::env::var("TUI_KERF_ENABLE").is_ok()
}
}
fn parse_comma_separated(input: &str) -> Vec<String> {
input
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn parse_tab_definitions(input: &str) -> Result<Vec<(String, String, String)>> {
let mut tabs = Vec::new();
for tab_def in input.split(',') {
let parts: Vec<&str> = tab_def.trim().split(':').collect();
if parts.len() != 3 {
return Err(anyhow!(
"Invalid tab definition '{}'. Expected format: 'name:level:pattern'",
tab_def
));
}
let name = parts[0].trim().to_string();
let level = parts[1].trim().to_string();
let pattern = parts[2].trim().to_string();
validate_level(&level)?;
tabs.push((name, level, pattern));
}
Ok(tabs)
}
fn parse_stream_routing(input: &str) -> Result<HashMap<String, PathBuf>> {
let mut streams = HashMap::new();
for stream_def in input.split(',') {
let parts: Vec<&str> = stream_def.trim().split(':').collect();
if parts.len() != 2 {
return Err(anyhow!(
"Invalid stream definition '{}'. Expected format: 'name:path'",
stream_def
));
}
let name = parts[0].trim().to_string();
let path = PathBuf::from(parts[1].trim());
streams.insert(name, path);
}
Ok(streams)
}
fn parse_stream_config(input: &str) -> Result<(String, Vec<String>)> {
let parts: Vec<&str> = input.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(anyhow!(
"Invalid stream config '{}'. Expected format: 'level:patterns'",
input
));
}
let level = parts[0].trim().to_string();
validate_level(&level)?;
let patterns = parse_comma_separated(parts[1]);
Ok((level, patterns))
}
fn parse_tui_config() -> Result<Option<TuiConfig>> {
if std::env::var("TUI_KERF_ENABLE").is_err() {
return Ok(None);
}
let mut config = TuiConfig {
enabled: true,
modules: Vec::new(),
level: "debug".to_string(),
output: None,
};
if let Ok(modules) = std::env::var("TUI_KERF_MODULES") {
config.modules = parse_comma_separated(&modules);
} else {
config.modules = vec!["cactui::*".to_string(), "codeng_tui::*".to_string()];
}
if let Ok(level) = std::env::var("TUI_KERF_LEVEL") {
validate_level(&level)?;
config.level = level;
}
if let Ok(output) = std::env::var("TUI_KERF_OUTPUT") {
config.output = Some(PathBuf::from(output));
}
Ok(Some(config))
}
fn validate_level(level: &str) -> Result<()> {
match level.to_lowercase().as_str() {
"trace" | "debug" | "info" | "warn" | "error" => Ok(()),
_ => Err(anyhow!("Invalid log level '{}'. Must be one of: trace, debug, info, warn, error", level)),
}
}
pub fn parse_level(level: &str) -> Result<Level> {
match level.to_lowercase().as_str() {
"trace" => Ok(Level::TRACE),
"debug" => Ok(Level::DEBUG),
"info" => Ok(Level::INFO),
"warn" => Ok(Level::WARN),
"error" => Ok(Level::ERROR),
_ => Err(anyhow!("Invalid log level '{}'", level)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_parse_comma_separated() {
assert_eq!(
parse_comma_separated("a,b,c"),
vec!["a", "b", "c"]
);
assert_eq!(
parse_comma_separated(" a , b , c "),
vec!["a", "b", "c"]
);
assert_eq!(
parse_comma_separated(""),
Vec::<String>::new()
);
}
#[test]
fn test_parse_tab_definitions() {
let result = parse_tab_definitions("errors:error:*,api:info:api_*").unwrap();
assert_eq!(result, vec![
("errors".to_string(), "error".to_string(), "*".to_string()),
("api".to_string(), "info".to_string(), "api_*".to_string()),
]);
}
#[test]
fn test_parse_tab_definitions_invalid() {
assert!(parse_tab_definitions("invalid").is_err());
assert!(parse_tab_definitions("name:invalid_level:pattern").is_err());
}
#[test]
fn test_parse_stream_routing() {
let result = parse_stream_routing("console:/tmp/console.log,errors:/tmp/errors.log").unwrap();
let mut expected = HashMap::new();
expected.insert("console".to_string(), PathBuf::from("/tmp/console.log"));
expected.insert("errors".to_string(), PathBuf::from("/tmp/errors.log"));
assert_eq!(result, expected);
}
#[test]
fn test_parse_stream_config() {
let (level, patterns) = parse_stream_config("info:*,-hyper::*").unwrap();
assert_eq!(level, "info");
assert_eq!(patterns, vec!["*", "-hyper::*"]);
}
#[test]
fn test_output_format_from_str() {
assert_eq!(OutputFormat::from_str("minimal").unwrap(), OutputFormat::Minimal);
assert_eq!(OutputFormat::from_str("standard").unwrap(), OutputFormat::Standard);
assert_eq!(OutputFormat::from_str("full").unwrap(), OutputFormat::Full);
assert_eq!(OutputFormat::from_str("custom").unwrap(), OutputFormat::Custom("custom".to_string()));
}
#[test]
fn test_validate_level() {
assert!(validate_level("trace").is_ok());
assert!(validate_level("debug").is_ok());
assert!(validate_level("info").is_ok());
assert!(validate_level("warn").is_ok());
assert!(validate_level("error").is_ok());
assert!(validate_level("invalid").is_err());
}
#[test]
fn test_parse_level() {
assert_eq!(parse_level("trace").unwrap(), Level::TRACE);
assert_eq!(parse_level("debug").unwrap(), Level::DEBUG);
assert_eq!(parse_level("info").unwrap(), Level::INFO);
assert_eq!(parse_level("warn").unwrap(), Level::WARN);
assert_eq!(parse_level("error").unwrap(), Level::ERROR);
assert!(parse_level("invalid").is_err());
}
#[test]
fn test_env_config_from_env() {
unsafe {
env::remove_var("KERF_GLOBAL_EXCLUDE");
env::remove_var("KERF_TABS");
env::remove_var("KERF_OUTPUT_FORMAT");
env::remove_var("TUI_KERF_ENABLE");
env::remove_var("TUI_KERF_MODULES");
env::remove_var("TUI_KERF_LEVEL");
}
unsafe {
env::set_var("KERF_GLOBAL_EXCLUDE", "hyper::*,tokio::*");
env::set_var("KERF_TABS", "errors:error:*,api:info:api_*");
env::set_var("KERF_OUTPUT_FORMAT", "minimal");
env::set_var("TUI_KERF_ENABLE", "1");
env::set_var("TUI_KERF_MODULES", "cactui::*");
env::set_var("TUI_KERF_LEVEL", "debug");
}
let config = EnvConfig::from_env().unwrap();
assert_eq!(config.global_excludes, vec!["hyper::*", "tokio::*"]);
assert_eq!(config.tabs.len(), 2);
assert_eq!(config.output_format, OutputFormat::Minimal);
assert!(config.tui_config.is_some());
let tui_config = config.tui_config.unwrap();
assert!(tui_config.enabled);
assert_eq!(tui_config.modules, vec!["cactui::*"]);
assert_eq!(tui_config.level, "debug");
unsafe {
env::remove_var("KERF_GLOBAL_EXCLUDE");
env::remove_var("KERF_TABS");
env::remove_var("KERF_OUTPUT_FORMAT");
env::remove_var("TUI_KERF_ENABLE");
env::remove_var("TUI_KERF_MODULES");
env::remove_var("TUI_KERF_LEVEL");
}
}
#[test]
fn test_has_env_config() {
unsafe {
env::remove_var("KERF_GLOBAL_EXCLUDE");
env::remove_var("KERF_TABS");
env::remove_var("KERF_OUTPUT_DIR");
env::remove_var("KERF_OUTPUT_FORMAT");
env::remove_var("KERF_STREAMS");
env::remove_var("TUI_KERF_ENABLE");
}
assert!(!EnvConfig::has_env_config());
unsafe {
env::set_var("KERF_GLOBAL_EXCLUDE", "test");
}
assert!(EnvConfig::has_env_config());
unsafe {
env::remove_var("KERF_GLOBAL_EXCLUDE");
}
assert!(!EnvConfig::has_env_config());
}
}