Logging
Compile-time configurable logging system with zero-cost when disabled.
Overview
The logging module provides:
- Zero-cost when disabled - Macros expand to no-op without feature flag
- Four severity levels - Debug, Info, Warning, Error
- Thread-safe - Safe concurrent access from any thread
- Flexible output - Console (stderr) and file backends
- Level filtering - Runtime minimum level configuration
Feature Flag
Logging requires the logging feature:
[dependencies]
hdds = { path = "../hdds/crates/hdds", features = ["logging"] }
When disabled, all logging macros compile to empty expressions with zero overhead.
Quick Start
use hdds::logging::{init_logger, ConsoleOutput, LogLevel};
use std::sync::Arc;
fn main() {
// Initialize early in main()
let console = Arc::new(ConsoleOutput::new(LogLevel::Debug));
init_logger(console, LogLevel::Debug);
// Use anywhere in your code
debug!("Detailed info: {}", value);
info!("Normal operation");
warn!("Suspicious activity: {}", reason);
error!("Critical failure: {}", cause);
}
Log Levels
pub enum LogLevel {
Debug = 0, // Detailed development information
Info = 1, // General operational information
Warning = 2, // Potentially harmful situations
Error = 3, // Error conditions
}
Messages below the configured level are filtered out.
Logging Macros
All macros use println!-style formatting:
// Debug-level (development)
debug!("Processing item {}: {:?}", id, data);
// Info-level (operations)
info!("Service started on port {}", port);
// Warning-level (potential issues)
warn!("Slow response detected: {}ms", elapsed);
// Error-level (failures)
error!("Connection lost: {}", error_code);
Output Format
[DEBUG] Processing item 42: SensorData { ... }
[INFO ] Service started on port 7400
[WARN ] Slow response detected: 150ms
[ERROR] Connection lost: ECONNRESET
Output Backends
ConsoleOutput
Writes to stderr with thread-safe internal mutex:
use hdds::logging::{ConsoleOutput, LogLevel};
use std::sync::Arc;
let console = Arc::new(ConsoleOutput::new(LogLevel::Debug));
FileOutput
Appends to a file (creates/truncates on init):
use hdds::logging::{FileOutput, LogLevel};
use std::sync::Arc;
let file = Arc::new(FileOutput::new("/var/log/hdds.log", LogLevel::Info)?);
Custom Output
Implement the Output trait:
use hdds::logging::{Output, LogLevel};
use std::io;
pub trait Output: Send + Sync {
fn write(&self, level: LogLevel, message: &str) -> io::Result<()>;
fn flush(&self) -> io::Result<()>;
}
Example custom output:
struct NetworkOutput {
endpoint: String,
}
impl Output for NetworkOutput {
fn write(&self, level: LogLevel, message: &str) -> io::Result<()> {
// Send to remote logging service...
Ok(())
}
fn flush(&self) -> io::Result<()> {
Ok(())
}
}
Initialization
init_logger
Initialize the global logger (call once, early in main):
use hdds::logging::{init_logger, ConsoleOutput, LogLevel};
use std::sync::Arc;
let output = Arc::new(ConsoleOutput::new(LogLevel::Debug));
init_logger(output, LogLevel::Debug);
init_logger can only be called once. Subsequent calls are safely ignored.
flush_logger
Flush buffered output (safe even if not initialized):
use hdds::logging::flush_logger;
flush_logger()?;
Function Tracing
When both logging and trace features are enabled:
fn parse_spdp_data(bytes: &[u8]) -> Result<Data> {
trace_fn!("parse_spdp_data");
// ...
}
Output:
[DEBUG] [ENTER:FNC] parse_spdp_data
Enable with:
hdds = { path = "../hdds/crates/hdds", features = ["logging", "trace"] }
Level Filtering
Two levels of filtering:
- Global filter - Set via
init_logger(output, level) - Output filter - Set via
ConsoleOutput::new(level)orFileOutput::new(path, level)
Messages must pass both filters to be written.
// Global: Info and above
// Output: Warning and above
// Result: Only Warning and Error are written
let console = Arc::new(ConsoleOutput::new(LogLevel::Warning));
init_logger(console, LogLevel::Info);
debug!("Filtered by global"); // Not written
info!("Filtered by output"); // Not written
warn!("Passes both filters"); // Written
error!("Passes both filters"); // Written
Thread Safety
All logging operations are thread-safe:
- Global logger uses
OnceLockfor lazy initialization - Logger state protected by
Mutex - Output backends use internal
Mutexfor concurrent access
use std::thread;
// Safe from multiple threads
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
info!("Thread {} logging", i);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
Zero-Cost Abstraction
When logging feature is disabled:
// Compiles to nothing (zero overhead)
#[cfg(not(feature = "logging"))]
macro_rules! debug {
($($arg:tt)*) => {};
}
No string formatting, no function calls, no runtime cost.
Integration Example
use hdds::logging::{init_logger, ConsoleOutput, FileOutput, LogLevel};
use std::sync::Arc;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Development: Console at Debug level
#[cfg(debug_assertions)]
{
let console = Arc::new(ConsoleOutput::new(LogLevel::Debug));
init_logger(console, LogLevel::Debug);
}
// Production: File at Info level
#[cfg(not(debug_assertions))]
{
let file = Arc::new(FileOutput::new("/var/log/hdds.log", LogLevel::Info)?);
init_logger(file, LogLevel::Info);
}
info!("Application started");
// ... application logic ...
flush_logger()?;
Ok(())
}
Related
- Telemetry - Metrics collection
- Admin API - Runtime debugging
- Environment Variables - Configuration