Merge pull request 'Mereg testing in main: Release 1.0.3' (#8) from testing into main
Main Release & Publish / Build, Publish Crate to Gitea Registry & Create Release (push) Successful in 22s

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-08-26 18:12:04 +00:00
9 changed files with 215 additions and 133 deletions
+9
View File
@@ -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
+1
View File
@@ -3,6 +3,7 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
+5 -3
View File
@@ -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). - **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. - **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`). - **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. - **Veröffentlichung**: Über Gitea Actions CI/CD in die Gitea Cargo Package Registry.
@@ -31,8 +31,10 @@ Logger/
├── LICENSE # GPL-3.0 Lizenztext ├── LICENSE # GPL-3.0 Lizenztext
├── README.md # Projektdokumentation & Nutzungsbeispiele ├── README.md # Projektdokumentation & Nutzungsbeispiele
├── AGENTS.md # Entwickler- und Agenten-Richtlinien ├── AGENTS.md # Entwickler- und Agenten-Richtlinien
── src/ ── src/
└── lib.rs # Hauptimplementierung (LogLevel, log, set_log_level, format_message, Tests) └── lib.rs # Hauptimplementierung (LogLevel, log, set_log_level, format_message)
└── tests/
└── integration_tests.rs # Integrationstests für alle Modulfunktionen und Traits
``` ```
--- ---
Generated
+8 -1
View File
@@ -16,8 +16,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]] [[package]]
name = "logger-ctdra" name = "logger-ctdra"
version = "1.0.2" version = "1.0.3"
dependencies = [ dependencies = [
"program-ctdra",
"time", "time",
] ]
@@ -51,6 +52,12 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "logger-ctdra" name = "logger-ctdra"
version = "1.0.2" version = "1.0.3"
edition = "2024" edition = "2024"
authors = ['DragonSlayer_14'] authors = ['DragonSlayer_14']
readme = "README.md" readme = "README.md"
@@ -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" description = "Einfaches, threadsicheres Logging-Framework für Rust mit Schweregrad-Filterung, formatierten Ausgaben und automatischer Dateiprotokollierung"
[dependencies] [dependencies]
program-ctdra = { version = "1.0.0", registry = "gitea" }
time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] } time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] }
[profile.release] [profile.release]
+3 -3
View File
@@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
DockerUpdater logger
Copyright (C) 2026 Linuxapps Copyright (C) 2026 Rust-Crates
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
DockerUpdater Copyright (C) 2026 Linuxapps logger Copyright (C) 2026 Rust-Crates
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+4 -7
View File
@@ -14,7 +14,7 @@
- **Einheitliches Format**: `[Präfix][DD.MM.YYYY HH:MM:SS.mmm][LEVEL][Tag]: Nachricht` - **Einheitliches Format**: `[Präfix][DD.MM.YYYY HH:MM:SS.mmm][LEVEL][Tag]: Nachricht`
- **Flexible Konfiguration**: - **Flexible Konfiguration**:
- Log-Level per Code (`set_log_level`), Umgebungsvariable (`LOG_LEVEL` bzw. `RUST_LOG`) oder Fallback (`Info`). - 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()`). - 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 ```rust
use logger_ctdra::{set_log_dir, set_program_name}; use logger_ctdra::set_log_dir;
use std::path::PathBuf; use std::path::PathBuf;
fn init() { fn init() {
// Setzt den App-Namen für das Logverzeichnis (/tmp/<app-name>-<uuid>/) // Individuelles Logverzeichnis vorgeben:
set_program_name("my-service");
// Oder ein individuelles Logverzeichnis vorgeben:
set_log_dir(PathBuf::from("/var/log/my-service")); set_log_dir(PathBuf::from("/var/log/my-service"));
} }
``` ```
+10 -118
View File
@@ -99,9 +99,6 @@ static LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
/// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug. /// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug.
static LOG_FILE: OnceLock<Mutex<Option<File>>> = OnceLock::new(); static LOG_FILE: OnceLock<Mutex<Option<File>>> = OnceLock::new();
/// Optionaler benutzerdefinierter Programmname für die Verzeichnisstruktur im Temp-Ordner.
static PROGRAM_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
/// Optionaler benutzerdefinierter Pfad für Logdateien. /// Optionaler benutzerdefinierter Pfad für Logdateien.
static CUSTOM_LOG_DIR: OnceLock<RwLock<Option<PathBuf>>> = OnceLock::new(); static CUSTOM_LOG_DIR: OnceLock<RwLock<Option<PathBuf>>> = OnceLock::new();
@@ -151,30 +148,6 @@ pub fn get_log_level() -> LogLevel {
LogLevel::Info LogLevel::Info
} }
/// Setzt den Programmnamen für die Logdateibenennung explizit.
pub fn set_program_name(name: impl Into<String>) {
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. /// Setzt ein benutzerdefiniertes Verzeichnis für Logdateien.
pub fn set_log_dir(path: impl Into<PathBuf>) { pub fn set_log_dir(path: impl Into<PathBuf>) {
let lock = CUSTOM_LOG_DIR.get_or_init(|| RwLock::new(None)); let lock = CUSTOM_LOG_DIR.get_or_init(|| RwLock::new(None));
@@ -292,6 +265,15 @@ fn get_or_init_log_file() -> &'static Mutex<Option<File>> {
}) })
} }
/// 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. /// Öffnet (und erstellt bei Bedarf) die tagesbasierte Logdatei.
fn open_log_file() -> std::io::Result<File> { fn open_log_file() -> std::io::Result<File> {
let custom_dir = if let Some(lock) = CUSTOM_LOG_DIR.get() { let custom_dir = if let Some(lock) = CUSTOM_LOG_DIR.get() {
@@ -305,7 +287,7 @@ fn open_log_file() -> std::io::Result<File> {
}; };
let mut dir = custom_dir.unwrap_or_else(|| { 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(); let mut d = env::temp_dir();
d.push(format!("{}-{}", program, DEFAULT_LOG_UUID)); d.push(format!("{}-{}", program, DEFAULT_LOG_UUID));
d d
@@ -324,93 +306,3 @@ fn open_log_file() -> std::io::Result<File> {
OpenOptions::new().create(true).append(true).open(dir) 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());
}
}
+173
View File
@@ -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!(&timestamp[2..3], ".");
assert_eq!(&timestamp[5..6], ".");
assert_eq!(&timestamp[10..11], " ");
assert_eq!(&timestamp[13..14], ":");
assert_eq!(&timestamp[16..17], ":");
assert_eq!(&timestamp[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");
}