Skip to main content

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);
note

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:

  1. Global filter - Set via init_logger(output, level)
  2. Output filter - Set via ConsoleOutput::new(level) or FileOutput::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 OnceLock for lazy initialization
  • Logger state protected by Mutex
  • Output backends use internal Mutex for 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(())
}