diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..8455547 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,9 @@ +[registry] +default = "gitea" + +[registries.gitea] +index = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" # Sparse index +# index = "https://gitea.creative-dragonslayer.de/Rust-Crates/_cargo-index.git" # Git + +[net] +git-fetch-with-cli = true diff --git a/.idea/Logger.iml b/.idea/Logger.iml index cf84ae4..bbe0a70 100644 --- a/.idea/Logger.iml +++ b/.idea/Logger.iml @@ -3,6 +3,7 @@ + diff --git a/AGENTS.md b/AGENTS.md index 1ed5375..031577f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Diese Datei dient als Leitfaden und Kontextdokumentation für autonome Agenten u - **Keine Unicode-Emojis im Logging**: Das Logging verwendet einheitliche ASCII-Präfixe (`[!]` Error, `[?]` Warn, `[i]` Info, `[d]` Debug). - **Threadsicherheit**: Sämtliche Zustände (Log-Level, Dateizugriffe, Pfade) sind threadsicher gekapselt. -- **Minimaler Overhead**: Geringe Abhängigkeiten (ausschließlich `time` für formatierte Zeitstempel). +- **Minimaler Overhead**: Geringe Abhängigkeiten (`time` für formatierte Zeitstempel, `program-ctdra` zur Programmnamen-Ermittlung). - **Lizenz**: GNU General Public License v3.0 or later (`GPL-3.0-or-later`). - **Veröffentlichung**: Über Gitea Actions CI/CD in die Gitea Cargo Package Registry. @@ -31,8 +31,10 @@ Logger/ ├── LICENSE # GPL-3.0 Lizenztext ├── README.md # Projektdokumentation & Nutzungsbeispiele ├── AGENTS.md # Entwickler- und Agenten-Richtlinien -└── src/ - └── lib.rs # Hauptimplementierung (LogLevel, log, set_log_level, format_message, Tests) +├── src/ +│ └── lib.rs # Hauptimplementierung (LogLevel, log, set_log_level, format_message) +└── tests/ + └── integration_tests.rs # Integrationstests für alle Modulfunktionen und Traits ``` --- diff --git a/Cargo.lock b/Cargo.lock index 453aa54..f386cc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" name = "logger-ctdra" version = "1.0.2" dependencies = [ + "program-ctdra", "time", ] @@ -51,6 +52,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "program-ctdra" +version = "1.0.0" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "54f20af67f90bf6d10697dd77bb4beb41dafad7fe0fb46aacfc81ad45a8e48bd" + [[package]] name = "quote" version = "1.0.47" diff --git a/Cargo.toml b/Cargo.toml index efcca11..ce15ded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://gitea.creative-dragonslayer.de/Rust-Crates/logger" description = "Einfaches, threadsicheres Logging-Framework für Rust mit Schweregrad-Filterung, formatierten Ausgaben und automatischer Dateiprotokollierung" [dependencies] +program-ctdra = { version = "1.0.0", registry = "gitea" } time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] } [profile.release] diff --git a/README.md b/README.md index eab61b4..1c270f3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - **Einheitliches Format**: `[Präfix][DD.MM.YYYY HH:MM:SS.mmm][LEVEL][Tag]: Nachricht` - **Flexible Konfiguration**: - Log-Level per Code (`set_log_level`), Umgebungsvariable (`LOG_LEVEL` bzw. `RUST_LOG`) oder Fallback (`Info`). - - Einstellbarer Programmname (`set_program_name`) oder automatisches Ermitteln via `current_exe()`. + - Automatisches Ermitteln des Programmnamens via `program-ctdra` (mit zufälligem Fallback-Wert). - Anpassbares Log-Verzeichnis (`set_log_dir`) oder automatische Ablage im Temp-Verzeichnis (`std::env::temp_dir()`). --- @@ -81,17 +81,14 @@ fn run() { } ``` -### Konfiguration des Programmnamens und Logpfads +### Konfiguration des Logpfads ```rust -use logger_ctdra::{set_log_dir, set_program_name}; +use logger_ctdra::set_log_dir; use std::path::PathBuf; fn init() { - // Setzt den App-Namen für das Logverzeichnis (/tmp/-/) - set_program_name("my-service"); - - // Oder ein individuelles Logverzeichnis vorgeben: + // Individuelles Logverzeichnis vorgeben: set_log_dir(PathBuf::from("/var/log/my-service")); } ``` diff --git a/src/lib.rs b/src/lib.rs index 7f7f25e..be4a52d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,9 +99,6 @@ static LOG_LEVEL: AtomicU8 = AtomicU8::new(0); /// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug. static LOG_FILE: OnceLock>> = OnceLock::new(); -/// Optionaler benutzerdefinierter Programmname für die Verzeichnisstruktur im Temp-Ordner. -static PROGRAM_NAME: OnceLock>> = OnceLock::new(); - /// Optionaler benutzerdefinierter Pfad für Logdateien. static CUSTOM_LOG_DIR: OnceLock>> = OnceLock::new(); @@ -151,30 +148,6 @@ pub fn get_log_level() -> LogLevel { LogLevel::Info } -/// Setzt den Programmnamen für die Logdateibenennung explizit. -pub fn set_program_name(name: impl Into) { - let lock = PROGRAM_NAME.get_or_init(|| RwLock::new(None)); - if let Ok(mut guard) = lock.write() { - *guard = Some(name.into()); - } -} - -/// Ermittelt den Programmnamen (aus `set_program_name` oder `std::env::current_exe`). -pub fn get_program_name() -> String { - if let Some(lock) = PROGRAM_NAME.get() { - if let Ok(guard) = lock.read() { - if let Some(name) = guard.as_ref() { - return name.clone(); - } - } - } - env::current_exe() - .ok() - .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string())) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "app".to_string()) -} - /// Setzt ein benutzerdefiniertes Verzeichnis für Logdateien. pub fn set_log_dir(path: impl Into) { let lock = CUSTOM_LOG_DIR.get_or_init(|| RwLock::new(None)); @@ -292,6 +265,15 @@ fn get_or_init_log_file() -> &'static Mutex> { }) } +/// Generiert einen zufälligen Fallback-Programmnamen, falls der Name der ausführbaren Datei nicht ermittelt werden kann. +fn random_fallback_program_name() -> String { + use std::hash::{BuildHasher, Hasher, RandomState}; + let mut hasher = RandomState::new().build_hasher(); + hasher.write_i128(OffsetDateTime::now_utc().unix_timestamp_nanos()); + let val = hasher.finish(); + format!("app-{:016x}", val) +} + /// Öffnet (und erstellt bei Bedarf) die tagesbasierte Logdatei. fn open_log_file() -> std::io::Result { let custom_dir = if let Some(lock) = CUSTOM_LOG_DIR.get() { @@ -305,7 +287,7 @@ fn open_log_file() -> std::io::Result { }; let mut dir = custom_dir.unwrap_or_else(|| { - let program = get_program_name(); + let program = program_ctdra::program_name_or(random_fallback_program_name()); let mut d = env::temp_dir(); d.push(format!("{}-{}", program, DEFAULT_LOG_UUID)); d @@ -324,93 +306,3 @@ fn open_log_file() -> std::io::Result { OpenOptions::new().create(true).append(true).open(dir) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_message_prefixes() { - let msg_info = format_message("test", "Info message", &LogLevel::Info); - assert!(msg_info.starts_with("[i]")); - assert!(msg_info.contains("[INFO][test]: Info message")); - - let msg_err = format_message("test", "Error message", &LogLevel::Error); - assert!(msg_err.starts_with("[!]")); - assert!(msg_err.contains("[ERROR][test]: Error message")); - - let msg_warn = format_message("test", "Warn message", &LogLevel::Warn); - assert!(msg_warn.starts_with("[?]")); - assert!(msg_warn.contains("[WARN][test]: Warn message")); - - let msg_debug = format_message("test", "Debug message", &LogLevel::Debug); - assert!(msg_debug.starts_with("[d]")); - assert!(msg_debug.contains("[DEBUG][test]: Debug message")); - } - - #[test] - fn test_format_message_empty_tag() { - let msg = format_message("", "No tag message", &LogLevel::Info); - assert!(msg.starts_with("[i]")); - assert!(msg.contains("[INFO]: No tag message")); - } - - #[test] - fn test_log_level_display() { - assert_eq!(LogLevel::Error.to_string(), "ERROR"); - assert_eq!(LogLevel::Warn.to_string(), "WARN"); - assert_eq!(LogLevel::Info.to_string(), "INFO"); - assert_eq!(LogLevel::Debug.to_string(), "DEBUG"); - } - - #[test] - fn test_log_level_parsing() { - assert_eq!(LogLevel::try_from("error").unwrap(), LogLevel::Error); - assert_eq!(LogLevel::try_from("ERROR").unwrap(), LogLevel::Error); - assert_eq!(LogLevel::try_from("warn").unwrap(), LogLevel::Warn); - assert_eq!(LogLevel::try_from("warning").unwrap(), LogLevel::Warn); - assert_eq!(LogLevel::try_from("info").unwrap(), LogLevel::Info); - assert_eq!(LogLevel::try_from("debug").unwrap(), LogLevel::Debug); - assert_eq!(LogLevel::try_from("invalid").unwrap_err(), LogLevel::Info); - assert_eq!(LogLevel::from_str("debug").unwrap(), LogLevel::Debug); - } - - #[test] - fn test_log_level_ordering() { - assert!(LogLevel::Error < LogLevel::Warn); - assert!(LogLevel::Warn < LogLevel::Info); - assert!(LogLevel::Info < LogLevel::Debug); - } - - #[test] - fn test_set_and_get_log_level() { - set_log_level(LogLevel::Debug); - assert_eq!(get_log_level(), LogLevel::Debug); - - set_log_level(LogLevel::Error); - assert_eq!(get_log_level(), LogLevel::Error); - - set_log_level(LogLevel::Info); - assert_eq!(get_log_level(), LogLevel::Info); - } - - #[test] - fn test_convenience_functions() { - set_log_level(LogLevel::Debug); - error("test", "An error occurred"); - warn("test", "A warning occurred"); - info("test", "An info message"); - debug("test", "A debug message"); - } - - #[test] - fn test_program_name_and_custom_dir() { - set_program_name("my-custom-app"); - assert_eq!(get_program_name(), "my-custom-app"); - - let custom_dir = env::temp_dir().join("test-logger-custom-dir"); - set_log_dir(custom_dir.clone()); - let file = open_log_file(); - assert!(file.is_ok()); - } -} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..001bebb --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,173 @@ +use logger_ctdra::{ + debug, error, format_message, get_log_level, info, init, log, set_log_dir, set_log_level, + warn, LogLevel, +}; +use std::env; +use std::str::FromStr; + +#[test] +fn test_format_message_prefixes() { + let msg_info = format_message("test", "Info message", &LogLevel::Info); + assert!(msg_info.starts_with("[i]")); + assert!(msg_info.contains("[INFO][test]: Info message")); + + let msg_err = format_message("test", "Error message", &LogLevel::Error); + assert!(msg_err.starts_with("[!]")); + assert!(msg_err.contains("[ERROR][test]: Error message")); + + let msg_warn = format_message("test", "Warn message", &LogLevel::Warn); + assert!(msg_warn.starts_with("[?]")); + assert!(msg_warn.contains("[WARN][test]: Warn message")); + + let msg_debug = format_message("test", "Debug message", &LogLevel::Debug); + assert!(msg_debug.starts_with("[d]")); + assert!(msg_debug.contains("[DEBUG][test]: Debug message")); +} + +#[test] +fn test_format_message_empty_tag() { + let msg = format_message("", "No tag message", &LogLevel::Info); + assert!(msg.starts_with("[i]")); + assert!(msg.contains("[INFO]: No tag message")); + assert!(!msg.contains("[INFO][]")); +} + +#[test] +fn test_format_message_timestamp_format() { + let msg = format_message("tag", "payload", &LogLevel::Info); + // Format: [i][DD.MM.YYYY HH:MM:SS.mmm][INFO][tag]: payload + assert!(msg.starts_with("[i][")); + let closing_bracket = msg[3..].find(']').expect("Timestamp closing bracket"); + let timestamp = &msg[4..4 + closing_bracket - 1]; + // Timestamp format: DD.MM.YYYY HH:MM:SS.mmm (length: 23) + assert_eq!(timestamp.len(), 23); + assert_eq!(×tamp[2..3], "."); + assert_eq!(×tamp[5..6], "."); + assert_eq!(×tamp[10..11], " "); + assert_eq!(×tamp[13..14], ":"); + assert_eq!(×tamp[16..17], ":"); + assert_eq!(×tamp[19..20], "."); +} + +#[test] +fn test_log_level_display() { + assert_eq!(LogLevel::Error.to_string(), "ERROR"); + assert_eq!(LogLevel::Warn.to_string(), "WARN"); + assert_eq!(LogLevel::Info.to_string(), "INFO"); + assert_eq!(LogLevel::Debug.to_string(), "DEBUG"); +} + +#[test] +fn test_log_level_parsing_str() { + assert_eq!(LogLevel::try_from("error").unwrap(), LogLevel::Error); + assert_eq!(LogLevel::try_from("ERROR").unwrap(), LogLevel::Error); + assert_eq!(LogLevel::try_from(" error ").unwrap(), LogLevel::Error); + assert_eq!(LogLevel::try_from("warn").unwrap(), LogLevel::Warn); + assert_eq!(LogLevel::try_from("warning").unwrap(), LogLevel::Warn); + assert_eq!(LogLevel::try_from("WARN").unwrap(), LogLevel::Warn); + assert_eq!(LogLevel::try_from("WARNING").unwrap(), LogLevel::Warn); + assert_eq!(LogLevel::try_from("info").unwrap(), LogLevel::Info); + assert_eq!(LogLevel::try_from("INFO").unwrap(), LogLevel::Info); + assert_eq!(LogLevel::try_from("debug").unwrap(), LogLevel::Debug); + assert_eq!(LogLevel::try_from("DEBUG").unwrap(), LogLevel::Debug); + assert_eq!(LogLevel::try_from("invalid").unwrap_err(), LogLevel::Info); + assert_eq!(LogLevel::try_from("").unwrap_err(), LogLevel::Info); +} + +#[test] +fn test_log_level_parsing_string_and_from_str() { + assert_eq!( + LogLevel::try_from(String::from("error")).unwrap(), + LogLevel::Error + ); + assert_eq!( + LogLevel::try_from(String::from("WARN")).unwrap(), + LogLevel::Warn + ); + assert_eq!( + LogLevel::try_from(String::from("invalid")).unwrap_err(), + LogLevel::Info + ); + + assert_eq!(LogLevel::from_str("debug").unwrap(), LogLevel::Debug); + assert_eq!(LogLevel::from_str("INFO").unwrap(), LogLevel::Info); + assert_eq!(LogLevel::from_str("unknown").unwrap_err(), LogLevel::Info); +} + +#[test] +fn test_log_level_discriminants_and_traits() { + assert_eq!(LogLevel::Error as u8, 1); + assert_eq!(LogLevel::Warn as u8, 2); + assert_eq!(LogLevel::Info as u8, 3); + assert_eq!(LogLevel::Debug as u8, 4); + + let lvl = LogLevel::Info; + let lvl_cloned = lvl; + assert_eq!(lvl, lvl_cloned); + assert_eq!(format!("{:?}", LogLevel::Debug), "Debug"); +} + +#[test] +fn test_log_level_ordering() { + assert!(LogLevel::Error < LogLevel::Warn); + assert!(LogLevel::Warn < LogLevel::Info); + assert!(LogLevel::Info < LogLevel::Debug); + assert!(LogLevel::Error <= LogLevel::Error); + assert!(LogLevel::Debug >= LogLevel::Info); +} + +#[test] +fn test_set_and_get_log_level() { + set_log_level(LogLevel::Debug); + assert_eq!(get_log_level(), LogLevel::Debug); + + set_log_level(LogLevel::Error); + assert_eq!(get_log_level(), LogLevel::Error); + + set_log_level(LogLevel::Warn); + assert_eq!(get_log_level(), LogLevel::Warn); + + set_log_level(LogLevel::Info); + assert_eq!(get_log_level(), LogLevel::Info); +} + +#[test] +fn test_init_alias() { + init(LogLevel::Debug); + assert_eq!(get_log_level(), LogLevel::Debug); + init(LogLevel::Warn); + assert_eq!(get_log_level(), LogLevel::Warn); +} + +#[test] +fn test_convenience_functions() { + set_log_level(LogLevel::Debug); + error("test", "An error occurred"); + warn("test", "A warning occurred"); + info("test", "An info message"); + debug("test", "A debug message"); +} + +#[test] +fn test_log_filtering_behavior() { + set_log_level(LogLevel::Error); + // Debug and Info should be filtered out, Error should pass + log("filter_test", "This debug log should be ignored", LogLevel::Debug); + log("filter_test", "This info log should be ignored", LogLevel::Info); + log("filter_test", "This warn log should be ignored", LogLevel::Warn); + log("filter_test", "This error log should pass", LogLevel::Error); + + set_log_level(LogLevel::Debug); + log("filter_test", "All logs should pass in Debug level", LogLevel::Debug); +} + +#[test] +fn test_set_log_dir_string_and_pathbuf() { + let dir_str = env::temp_dir().join("test-integration-str-dir"); + set_log_dir(dir_str.to_string_lossy().to_string()); + + let dir_pathbuf = env::temp_dir().join("test-integration-pathbuf-dir"); + set_log_dir(dir_pathbuf); + + set_log_dir("/tmp/test-str-slice-dir"); +}