diff --git a/src/log.rs b/src/log.rs index 80f5145..50d335f 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,3 +1,25 @@ +//! Einfaches, threadsicheres Logging-Modul. +//! +//! Merkmale: +//! - Ausgabe in Terminal (stdout/stderr) und zusätzlich in eine Logdatei im temporären Verzeichnis. +//! - Nachrichtenvorlage: `[DD.MM.YYYY HH:MM:SS.mmm][LEVEL][TAG]: ` mit Emoji-Präfix (🛑/⚠️/ℹ️/🚧). +//! - Der aktuell verwendete Schweregradfilter ist statisch (`LOG_LEVEL`) und wird zur Laufzeit nicht geändert. +//! - Terminal-Erkennung und Dateihandle werden lazily initialisiert und zwischengespeichert (`OnceLock`). +//! +//! Hinweise: +//! - Bei einem leeren `tag` wird ein leerer Tag-Abschnitt erzeugt. +//! - Die Logdatei wird unterhalb eines prozessspezifischen Ordners im System-Temp-Verzeichnis abgelegt und nach Datum +//! benannt (z. B. `log-2025-08-18.log`). Es wird im Append-Modus geschrieben. +//! +//! Beispiel (ohne Ausführung in Doctests): +//! ```rust,no_run +//! use crate::log::{log, LogLevel}; +//! +//! log("startup", "Dienst wird initialisiert …", LogLevel::Info); +//! log("db", "Verbindung unterbrochen!", LogLevel::Warn); +//! log("core", "Unerwarteter Fehler", LogLevel::Error); +//! ``` + use std::env; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File, OpenOptions}; @@ -6,11 +28,24 @@ use std::sync::{Mutex, OnceLock}; use time::{macros::format_description, OffsetDateTime}; +/// Schweregrade für Logeinträge in aufsteigender Detailtiefe. +/// +/// Die Reihenfolge bestimmt den Filter: Nur Einträge mit `log_level <= LOG_LEVEL` werden ausgegeben. +/// +/// Anzeige (Display): +/// - `Error` -> `ERROR` +/// - `Warn` -> `WARN` +/// - `Info` -> `INFO` +/// - `Debug` -> `DEBUG` #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] pub enum LogLevel { + /// Kritische Fehler, gehen zusätzlich auf `stderr`. Error = 1, + /// Wichtige Warnungen über potenzielle Probleme. Warn = 2, + /// Allgemeine Betriebsinformationen. Info = 3, + /// Ausführliche Diagnoseausgaben für die Entwicklung. Debug = 4, } @@ -25,11 +60,36 @@ impl Display for LogLevel { } } +/// Zwischenspeicher für die einmalig ermittelte Terminal-Fähigkeit von `stdout`. static IS_TERMINAL: OnceLock = OnceLock::new(); -static LOG_LEVEL: LogLevel = LogLevel::Debug; // TODO: LogLevel aus Config laden +/// Globaler Schweregradfilter für Ausgabe. +/// TODO: In Zukunft aus Konfiguration laden. +static LOG_LEVEL: LogLevel = LogLevel::Debug; + +/// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug. static LOG_FILE: OnceLock>> = OnceLock::new(); +/// Protokolliert eine Nachricht abhängig vom angegebenen Schweregrad. +/// +/// Verhalten: +/// - Wenn `log_level` größer als der globale Filter ist, wird nichts ausgegeben. +/// - Bei Terminalausgabe gehen `Error`-Meldungen nach `stderr`, alle anderen nach `stdout`. +/// - Zusätzlich wird in eine tägliche Logdatei im Temp-Verzeichnis geschrieben (wenn erfolgreich geöffnet). +/// +/// Parameter: +/// - `tag`: Kurzer Kontext (z. B. Modulname). +/// - `message`: Die eigentliche Nachricht (eine Zeile). +/// - `log_level`: Schweregrad der Nachricht. +/// +/// Thread-Sicherheit: +/// - Dateischreibzugriffe sind über `Mutex` serialisiert. +/// +/// Beispiel: +/// ```rust,no_run +/// # use crate::log::{log, LogLevel}; +/// log("http", "Server gestartet auf Port 8080", LogLevel::Info); +/// ``` pub fn log(tag: &str, message: &str, log_level: LogLevel) { if log_level <= LOG_LEVEL { let message: String = format_message(tag, message, &log_level); @@ -52,10 +112,28 @@ pub fn log(tag: &str, message: &str, log_level: LogLevel) { } } +/// Ermittelt einmalig, ob `stdout` ein Terminal ist, und cached das Ergebnis. +/// +/// Rückgabe: +/// - `true`, wenn `stdout` ein TTY/Terminal ist. +/// - `false` andernfalls. fn is_terminal() -> bool { *IS_TERMINAL.get_or_init(|| stdout().is_terminal()) } +/// Formatiert eine Lognachricht mit Zeitstempel, Level, Tag und Emoji-Präfix. +/// +/// Format: +/// - Datum/Zeit lokal (Fallback: UTC) im Format `DD.MM.YYYY HH:MM:SS.mmm`. +/// - Level als Text (`ERROR`, `WARN`, `INFO`, `DEBUG`). +/// - Tag in eckigen Klammern; bei leerem Tag entsteht ein leeres `[]`. +/// - Emoji-Präfix pro Level: 🛑/⚠️/ℹ️/🚧. +/// +/// Beispielausgabe: +/// - `ℹ️[18.08.2025 14:23:45.012][INFO][init]: Fertig` +/// +/// Hinweis: +/// - Diese Funktion formatiert nur; sie führt keinen I/O aus. fn format_message(tag: &str, message: &str, log_level: &LogLevel) -> String { let mut prefix: String = String::new(); @@ -84,6 +162,11 @@ fn format_message(tag: &str, message: &str, log_level: &LogLevel) -> String { } } +/// Initialisiert das Logdatei-Handle beim ersten Aufruf und liefert eine Referenz darauf. +/// +/// Rückgabe: +/// - `&'static Mutex>`: Das Mutex schützt den optionalen Dateihandler. +/// `None` bedeutet, dass das Öffnen fehlgeschlagen ist (z. B. fehlende Rechte). fn get_or_init_log_file() -> &'static Mutex> { LOG_FILE.get_or_init(|| { let file = open_log_file().ok(); @@ -91,6 +174,19 @@ fn get_or_init_log_file() -> &'static Mutex> { }) } +/// Öffnet (und erstellt bei Bedarf) die tagesbasierte Logdatei im Temp-Verzeichnis. +/// +/// Pfadaufbau: +/// - Basis: `std::env::temp_dir()` +/// - Unterordner: `-` +/// - Datei: `log-YYYY-MM-DD.log` im Append-Modus +/// +/// Rückgabe: +/// - `Ok(File)`, wenn der Ordner erstellt/gefunden und die Datei geöffnet/angelegt werden konnte. +/// - `Err(std::io::Error)`, wenn ein I/O-Fehler auftrat. +/// +/// Fehler: +/// - Gibt I/O-Fehler unverändert weiter (z. B. beim Erstellen des Ordners oder Öffnen der Datei). fn open_log_file() -> std::io::Result { let program = program_name(); let rand = "13692bbf-a93b-43e9-9cc6-f05f94a8cfb6"; @@ -114,6 +210,11 @@ fn open_log_file() -> std::io::Result { .open(dir) } +/// Liefert den Programmnamen (Dateistamm der aktuellen ausführbaren Datei). +/// +/// Rückgabe: +/// - Dateistamm der aktuellen Executable als `String`. +/// - Fallback `"app"`, wenn der Name nicht ermittelt werden kann. fn program_name() -> String { env::current_exe() .ok()