Skip to main content

cossh/log/
mod.rs

1//! Debug and session logging primitives.
2
3mod debug;
4mod errors;
5mod formatter;
6mod macros;
7mod ssh;
8
9pub use errors::LogError;
10
11use once_cell::sync::Lazy;
12use std::sync::{
13    Arc,
14    atomic::{AtomicBool, AtomicU8, Ordering},
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18#[repr(u8)]
19/// Debug logging verbosity levels.
20pub enum DebugVerbosity {
21    /// Debug logging disabled.
22    Off = 0,
23    /// Safe debug logging enabled.
24    Safe = 1,
25    /// Raw debug logging enabled (may include sensitive output).
26    Raw = 2,
27}
28
29impl DebugVerbosity {
30    /// Convert CLI debug flag count (`-d`, `-dd`) to verbosity.
31    pub fn from_count(count: u8) -> Self {
32        match count {
33            0 => Self::Off,
34            1 => Self::Safe,
35            _ => Self::Raw,
36        }
37    }
38
39    fn from_stored(value: u8) -> Self {
40        match value {
41            0 => Self::Off,
42            1 => Self::Safe,
43            _ => Self::Raw,
44        }
45    }
46}
47
48/// Sanitize session name for use in log filenames.
49pub fn sanitize_session_name(raw: &str) -> String {
50    let mut sanitized = String::with_capacity(raw.len());
51    let mut has_valid = false;
52
53    for ch in raw.chars() {
54        if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
55            sanitized.push(ch);
56            has_valid = true;
57        } else {
58            sanitized.push('_');
59        }
60    }
61
62    if !has_valid || sanitized == "." || sanitized == ".." {
63        return "session".to_string();
64    }
65
66    sanitized
67}
68
69// Global flags for enabling different logging types.
70static DEBUG_VERBOSITY: AtomicU8 = AtomicU8::new(DebugVerbosity::Off as u8);
71static SSH_LOGGING: AtomicBool = AtomicBool::new(false);
72
73// Global logger instance to avoid repeated logger construction.
74pub static LOGGER: Lazy<Logger> = Lazy::new(Logger::new);
75
76#[derive(Debug, Clone, Copy)]
77/// Internal log level enum for debug logger formatting.
78pub enum LogLevel {
79    Debug,
80    Info,
81    Warning,
82    Error,
83}
84
85impl LogLevel {
86    fn as_str(&self) -> &'static str {
87        match self {
88            LogLevel::Debug => "DEBUG",
89            LogLevel::Info => "INFO",
90            LogLevel::Warning => "WARN",
91            LogLevel::Error => "ERROR",
92        }
93    }
94}
95
96#[derive(Clone, Default)]
97/// Runtime logger for debug and SSH session output.
98pub struct Logger {
99    debug_logger: debug::DebugLogger,
100    ssh_logger: ssh::SshLogger,
101}
102
103impl Logger {
104    /// Construct a logger with both channels disabled.
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Enable safe debug logging.
110    pub fn enable_debug(&self) {
111        self.enable_debug_with_verbosity(DebugVerbosity::Safe);
112    }
113
114    /// Enable debug logging with explicit verbosity.
115    pub fn enable_debug_with_verbosity(&self, verbosity: DebugVerbosity) {
116        DEBUG_VERBOSITY.store(verbosity as u8, Ordering::SeqCst);
117    }
118
119    /// Disable debug logging and flush pending messages.
120    pub fn disable_debug(&self) {
121        let was_enabled = self.is_debug_enabled();
122        DEBUG_VERBOSITY.store(DebugVerbosity::Off as u8, Ordering::SeqCst);
123        if was_enabled {
124            let _ = self.debug_logger.flush();
125        }
126    }
127
128    /// Enable SSH session logging.
129    pub fn enable_ssh_logging(&self) {
130        SSH_LOGGING.store(true, Ordering::SeqCst);
131    }
132
133    /// Disable SSH session logging.
134    pub fn disable_ssh_logging(&self) {
135        SSH_LOGGING.store(false, Ordering::SeqCst);
136    }
137
138    /// Return currently configured debug verbosity.
139    pub fn debug_verbosity(&self) -> DebugVerbosity {
140        DebugVerbosity::from_stored(DEBUG_VERBOSITY.load(Ordering::SeqCst))
141    }
142
143    /// Returns `true` when safe or raw debug is enabled.
144    pub fn is_debug_enabled(&self) -> bool {
145        self.debug_verbosity() >= DebugVerbosity::Safe
146    }
147
148    /// Returns `true` only when raw debug logging is enabled.
149    pub fn is_raw_debug_enabled(&self) -> bool {
150        self.debug_verbosity() >= DebugVerbosity::Raw
151    }
152
153    /// Returns `true` when SSH session logging is enabled.
154    pub fn is_ssh_logging_enabled(&self) -> bool {
155        SSH_LOGGING.load(Ordering::SeqCst)
156    }
157
158    /// Write a debug-level message if debug logging is enabled.
159    pub fn log_debug(&self, message: &str) -> Result<(), LogError> {
160        if self.is_debug_enabled() {
161            self.debug_logger.log(LogLevel::Debug, message)?;
162        }
163        Ok(())
164    }
165
166    /// Write an info-level message if debug logging is enabled.
167    pub fn log_info(&self, message: &str) -> Result<(), LogError> {
168        if self.is_debug_enabled() {
169            self.debug_logger.log(LogLevel::Info, message)?;
170        }
171        Ok(())
172    }
173
174    /// Write a warning-level message if debug logging is enabled.
175    pub fn log_warn(&self, message: &str) -> Result<(), LogError> {
176        if self.is_debug_enabled() {
177            self.debug_logger.log(LogLevel::Warning, message)?;
178        }
179        Ok(())
180    }
181
182    /// Write an error-level message if debug logging is enabled.
183    pub fn log_error(&self, message: &str) -> Result<(), LogError> {
184        if self.is_debug_enabled() {
185            self.debug_logger.log(LogLevel::Error, message)?;
186        }
187        Ok(())
188    }
189
190    /// Flush debug logger output.
191    pub fn flush_debug(&self) -> Result<(), LogError> {
192        self.debug_logger.flush()
193    }
194
195    /// Write one sanitized SSH session log line when enabled.
196    pub fn log_ssh(&self, message: &str) -> Result<(), LogError> {
197        if self.is_ssh_logging_enabled() {
198            self.ssh_logger.log(message)?;
199        }
200        Ok(())
201    }
202
203    /// Write one raw SSH chunk when enabled.
204    pub fn log_ssh_raw(&self, message: &str) -> Result<(), LogError> {
205        if self.is_ssh_logging_enabled() {
206            self.ssh_logger.log_raw(message)?;
207        }
208        Ok(())
209    }
210
211    /// Write one shared raw SSH chunk without cloning the string.
212    pub fn log_ssh_raw_shared(&self, message: Arc<String>) -> Result<(), LogError> {
213        if self.is_ssh_logging_enabled() {
214            self.ssh_logger.log_raw_shared(message)?;
215        }
216        Ok(())
217    }
218
219    /// Flush SSH session log output.
220    pub fn flush_ssh(&self) -> Result<(), LogError> {
221        if self.is_ssh_logging_enabled() {
222            self.ssh_logger.flush()?;
223        }
224        Ok(())
225    }
226}
227
228#[cfg(test)]
229#[path = "../test/log.rs"]
230mod tests;