vanguards_rs/logger.rs
1//! Logging infrastructure for vanguards-rs.
2//!
3//! This module provides logging functionality using the tracing ecosystem.
4//! It supports output to stdout, files, and syslog, with configurable log levels.
5//!
6//! # Overview
7//!
8//! The logging system provides:
9//!
10//! - **Multiple output destinations**: stdout, file, or syslog
11//! - **Configurable log levels**: From DEBUG to ERROR
12//! - **Python vanguards compatibility**: `plog` function matches Python API
13//! - **Environment variable override**: `RUST_LOG` can override configured level
14//!
15//! # Log Levels
16//!
17//! From most to least verbose:
18//!
19//! | Level | Description | Use Case |
20//! |-------|-------------|----------|
21//! | [`Debug`](crate::LogLevel::Debug) | Low-level debugging | Development only |
22//! | [`Info`](crate::LogLevel::Info) | Informational messages | Verbose operation |
23//! | [`Notice`](crate::LogLevel::Notice) | Notable events | Default level |
24//! | [`Warn`](crate::LogLevel::Warn) | Warning conditions | Potential issues |
25//! | [`Error`](crate::LogLevel::Error) | Error conditions | Failures |
26//!
27//! # Example
28//!
29//! ```rust,no_run
30//! use vanguards_rs::{LogLevel, logger};
31//!
32//! // Initialize logging to stdout at NOTICE level
33//! logger::init(LogLevel::Notice, None).unwrap();
34//!
35//! // Log messages using the plog function
36//! logger::plog(LogLevel::Notice, "Vanguards started");
37//! logger::plog(LogLevel::Info, "Connected to Tor");
38//! logger::plog(LogLevel::Warn, "High timeout rate detected");
39//! ```
40//!
41//! # Output Destination Examples
42//!
43//! ```rust,no_run
44//! use vanguards_rs::{LogLevel, logger};
45//!
46//! // Log to stdout (default)
47//! logger::init(LogLevel::Notice, None).unwrap();
48//!
49//! // Log to a file
50//! logger::init(LogLevel::Debug, Some("/var/log/vanguards.log")).unwrap();
51//!
52//! // Log to syslog
53//! logger::init(LogLevel::Notice, Some(":syslog:")).unwrap();
54//! ```
55//!
56//! # What This Module Does NOT Do
57//!
58//! - **Log rotation**: Use external tools like logrotate
59//! - **Log aggregation**: Use external services for centralized logging
60//! - **Structured logging**: Currently outputs plain text only
61//!
62//! # See Also
63//!
64//! - [`crate::config::LogLevel`] - Log level enumeration
65//! - [`crate::logguard`] - Log buffering for circuit debugging
66//! - [tracing crate](https://docs.rs/tracing) - Underlying logging framework
67
68use std::io::Write;
69use std::os::unix::net::UnixDatagram;
70use std::path::Path;
71use std::sync::OnceLock;
72use tracing::{debug, error, info, warn};
73use tracing_subscriber::fmt::format::FmtSpan;
74use tracing_subscriber::EnvFilter;
75
76use crate::config::LogLevel;
77use crate::error::{Error, Result};
78
79static LOGGER_INITIALIZED: OnceLock<()> = OnceLock::new();
80
81/// Initialize the logging system.
82///
83/// This function sets up the tracing subscriber with the specified log level
84/// and output destination. It should be called once at application startup.
85/// Subsequent calls are no-ops.
86///
87/// # Arguments
88///
89/// * `level` - The minimum log level to output
90/// * `logfile` - Output destination:
91/// - `None` - Log to stdout with ANSI colors
92/// - `Some(":syslog:")` - Log to system syslog
93/// - `Some(path)` - Log to file at the specified path
94///
95/// # Returns
96///
97/// `Ok(())` on success, or an error if initialization fails.
98///
99/// # Errors
100///
101/// Returns [`Error::Io`] if:
102/// - The log file cannot be created or opened
103/// - The syslog socket cannot be found (Linux: `/dev/log`, macOS: `/var/run/syslog`)
104///
105/// Returns [`Error::Config`] if:
106/// - The tracing subscriber cannot be set (usually means already initialized)
107///
108/// # Example
109///
110/// ```rust,no_run
111/// use vanguards_rs::{LogLevel, logger};
112///
113/// // Log to stdout (with colors)
114/// logger::init(LogLevel::Notice, None).unwrap();
115///
116/// // Log to file (no colors)
117/// logger::init(LogLevel::Debug, Some("/var/log/vanguards.log")).unwrap();
118///
119/// // Log to syslog
120/// logger::init(LogLevel::Notice, Some(":syslog:")).unwrap();
121/// ```
122///
123/// # Notes
124///
125/// - The `RUST_LOG` environment variable can override the configured level
126/// - File logging appends to existing files
127/// - Syslog messages are prefixed with "vanguards:"
128///
129/// # See Also
130///
131/// - [`plog`] - Log messages after initialization
132/// - [`crate::config::LogLevel`] - Available log levels
133pub fn init(level: LogLevel, logfile: Option<&str>) -> Result<()> {
134 if LOGGER_INITIALIZED.get().is_some() {
135 return Ok(());
136 }
137
138 let filter = match level {
139 LogLevel::Debug => "debug",
140 LogLevel::Info => "info",
141 LogLevel::Notice => "info",
142 LogLevel::Warn => "warn",
143 LogLevel::Error => "error",
144 };
145
146 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter));
147
148 match logfile {
149 None => {
150 let subscriber = tracing_subscriber::fmt()
151 .with_env_filter(env_filter)
152 .with_target(false)
153 .with_thread_ids(false)
154 .with_span_events(FmtSpan::NONE)
155 .with_ansi(true)
156 .finish();
157 tracing::subscriber::set_global_default(subscriber)
158 .map_err(|e| Error::Config(format!("failed to set logger: {}", e)))?;
159 }
160 Some(":syslog:") => {
161 init_syslog(env_filter)?;
162 }
163 Some(path) => {
164 init_file_logger(path, env_filter)?;
165 }
166 }
167
168 LOGGER_INITIALIZED.get_or_init(|| ());
169 Ok(())
170}
171
172fn init_syslog(env_filter: EnvFilter) -> Result<()> {
173 let syslog_path = if Path::new("/dev/log").exists() {
174 "/dev/log"
175 } else if Path::new("/var/run/syslog").exists() {
176 "/var/run/syslog"
177 } else {
178 return Err(Error::Config("no syslog socket found".to_string()));
179 };
180
181 let subscriber = tracing_subscriber::fmt()
182 .with_env_filter(env_filter)
183 .with_target(false)
184 .with_thread_ids(false)
185 .with_ansi(false)
186 .with_writer(move || {
187 UnixDatagram::unbound()
188 .and_then(|sock| {
189 sock.connect(syslog_path)?;
190 Ok(SyslogWriter { socket: sock })
191 })
192 .unwrap_or_else(|_| SyslogWriter {
193 socket: UnixDatagram::unbound().unwrap(),
194 })
195 })
196 .finish();
197
198 tracing::subscriber::set_global_default(subscriber)
199 .map_err(|e| Error::Config(format!("failed to set logger: {}", e)))?;
200
201 Ok(())
202}
203
204struct SyslogWriter {
205 socket: UnixDatagram,
206}
207
208impl Write for SyslogWriter {
209 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
210 let msg = format!("vanguards: {}", String::from_utf8_lossy(buf));
211 self.socket.send(msg.as_bytes())?;
212 Ok(buf.len())
213 }
214
215 fn flush(&mut self) -> std::io::Result<()> {
216 Ok(())
217 }
218}
219
220fn init_file_logger(path: &str, env_filter: EnvFilter) -> Result<()> {
221 let file = std::fs::OpenOptions::new()
222 .create(true)
223 .append(true)
224 .open(path)?;
225
226 let subscriber = tracing_subscriber::fmt()
227 .with_env_filter(env_filter)
228 .with_target(false)
229 .with_thread_ids(false)
230 .with_ansi(false)
231 .with_writer(std::sync::Mutex::new(file))
232 .finish();
233
234 tracing::subscriber::set_global_default(subscriber)
235 .map_err(|e| Error::Config(format!("failed to set logger: {}", e)))?;
236
237 Ok(())
238}
239
240/// Log a message at the specified level.
241///
242/// This function provides a Python vanguards-compatible logging interface.
243/// It maps log levels to tracing macros.
244///
245/// # Arguments
246///
247/// * `level` - The log level for this message
248/// * `message` - The message to log
249///
250/// # Level Mapping
251///
252/// | LogLevel | tracing macro |
253/// |----------|---------------|
254/// | Debug | `debug!` |
255/// | Info | `info!` |
256/// | Notice | `info!` |
257/// | Warn | `warn!` |
258/// | Error | `error!` |
259///
260/// # Example
261///
262/// ```rust
263/// use vanguards_rs::{LogLevel, logger};
264///
265/// logger::plog(LogLevel::Notice, "Vanguards started");
266/// logger::plog(LogLevel::Warn, "Connection lost, retrying...");
267/// logger::plog(LogLevel::Error, "Failed to connect to Tor");
268/// ```
269///
270/// # Notes
271///
272/// - Messages are only output if the level meets the configured minimum
273/// - Notice maps to `info!` since tracing doesn't have a notice level
274///
275/// # See Also
276///
277/// - [`init`] - Initialize logging before calling plog
278/// - [`plog_fmt`](crate::plog_fmt) - Formatted logging macro
279pub fn plog(level: LogLevel, message: &str) {
280 match level {
281 LogLevel::Debug => debug!("{}", message),
282 LogLevel::Info => info!("{}", message),
283 LogLevel::Notice => info!("{}", message),
284 LogLevel::Warn => warn!("{}", message),
285 LogLevel::Error => error!("{}", message),
286 }
287}
288
289/// Log a formatted message at the specified level.
290///
291/// This macro provides printf-style formatting for log messages.
292///
293/// # Example
294///
295/// ```rust
296/// use vanguards_rs::{LogLevel, plog_fmt};
297///
298/// plog_fmt!(LogLevel::Notice, "Connected to Tor version {}", "0.4.7.0");
299/// plog_fmt!(LogLevel::Info, "Layer2 guards: {}", 4);
300/// ```
301#[macro_export]
302macro_rules! plog_fmt {
303 ($level:expr, $($arg:tt)*) => {
304 $crate::logger::plog($level, &format!($($arg)*))
305 };
306}