Skip to content

Ripgrep logger.rs: Code Companion

Reference code for the logger.rs lecture. Sections correspond to the lecture document.


Section 1: The Log Crate Facade

/*!
Defines a super simple logger that works with the `log` crate.

We don't do anything fancy. We just need basic log levels and the ability to
print to stderr. We therefore avoid bringing in extra dependencies just for
this functionality.
*/

use log::Log;

The facade pattern:

// Library code (doesn't know about Logger):
log::debug!("Processing file: {}", path.display());
log::trace!("Binary detection: {:?}", mode);

// Application code (provides the implementation):
Logger::init()?;  // Now debug!/trace! flow to our Logger

Log levels (from lowest to highest priority):

log::trace!("...");  // Most verbose, rarely needed
log::debug!("...");  // Debugging information
log::info!("...");   // General information
log::warn!("...");   // Warnings
log::error!("...");  // Errors

Section 2: The Logger Struct

/// The simplest possible logger that logs to stderr.
///
/// This logger does no filtering. Instead, it relies on the `log` crates
/// filtering via its global max_level setting.
#[derive(Debug)]
pub(crate) struct Logger(());

/// A singleton used as the target for an implementation of the `Log` trait.
const LOGGER: &'static Logger = &Logger(());

Zero-sized type (ZST):

// Logger contains () which has size 0
std::mem::size_of::<Logger>()  // Returns 0

// No runtime cost — exists only for type system
// The reference &Logger also has a fixed size (pointer)
// but points to "nothing" in a sense

Why the newtype pattern:

// Can't do this:
impl Log for () { ... }  // Error: can't impl foreign trait on foreign type

// Can do this:
struct Logger(());
impl Log for Logger { ... }  // OK: Logger is our type

Section 3: Initialization

impl Logger {
    /// Create a new logger that logs to stderr and initialize it as the
    /// global logger. If there was a problem setting the logger, then an
    /// error is returned.
    pub(crate) fn init() -> Result<(), log::SetLoggerError> {
        log::set_logger(LOGGER)
    }
}

Usage in ripgrep startup:

// In argument processing (simplified):
if args.debug {
    log::set_max_level(log::LevelFilter::Debug);
    Logger::init()?;
} else if args.trace {
    log::set_max_level(log::LevelFilter::Trace);
    Logger::init()?;
} else {
    log::set_max_level(log::LevelFilter::Off);
    // Logger::init() not needed — no logs will be emitted
}

Level filtering:

log::set_max_level(log::LevelFilter::Debug);
// Now: trace! → skipped, debug!/info!/warn!/error! → logged

log::set_max_level(log::LevelFilter::Off);
// Now: all log macros → skipped (no overhead)

Section 4: The Log Trait — enabled

impl Log for Logger {
    fn enabled(&self, _: &log::Metadata<'_>) -> bool {
        // We set the log level via log::set_max_level, so we don't need to
        // implement filtering here.
        true
    }
    // ...
}

Why always return true:

Log macro expansion (simplified):

log::debug!("message")
if log::max_level() >= Debug {        // First filter (fast)
    if logger.enabled(metadata) {      // Second filter (we return true)
        logger.log(record);
    }
}

Since max_level() handles filtering, enabled() just says "yes, proceed"

Section 5: The Log Trait — log

fn log(&self, record: &log::Record<'_>) {
    match (record.file(), record.line()) {
        (Some(file), Some(line)) => {
            eprintln_locked!(
                "{}|{}|{}:{}: {}",
                record.level(),
                record.target(),
                file,
                line,
                record.args()
            );
        }
        (Some(file), None) => {
            eprintln_locked!(
                "{}|{}|{}: {}",
                record.level(),
                record.target(),
                file,
                record.args()
            );
        }
        _ => {
            eprintln_locked!(
                "{}|{}: {}",
                record.level(),
                record.target(),
                record.args()
            );
        }
    }
}

Output format examples:

DEBUG|ripgrep::search|search.rs:42: searching file.txt
TRACE|ignore::walk|walk.rs:128: skipping hidden directory
DEBUG|grep_searcher: mmap enabled for large file

Record fields:

record.level()   // DEBUG, TRACE, etc.
record.target()  // Usually module path: "ripgrep::search"
record.file()    // Source file: Some("search.rs") or None
record.line()    // Line number: Some(42) or None
record.args()    // The formatted message

Thread-safety via eprintln_locked:

// From messages.rs — ensures debug output doesn't interleave
eprintln_locked!("{}|{}: {}", level, target, message);
// Locks stdout (to coordinate with search output)
// Then locks stderr
// Writes complete message atomically

Section 6: The Log Trait — flush

fn flush(&self) {
    // We use eprintln_locked! which is flushed on every call.
}

Why empty:

// eprintln_locked! does:
writeln!(stderr, ...)?;  // writeln flushes line-buffered streams
drop(stdout);            // Releases lock, implicit flush

// No buffering in our logger → nothing to flush

Contrast with buffered logger:

// A buffered logger might do:
fn log(&self, record: &Record) {
    self.buffer.push(format!("{}", record.args()));
    if self.buffer.len() > 100 {
        self.flush();
    }
}

fn flush(&self) {
    for msg in self.buffer.drain(..) {
        eprintln!("{}", msg);
    }
}

Section 7: Integration with ripgrep

Activation via flags:

rg --debug pattern file.txt
# Output includes: DEBUG|...|search.rs:N: ...

rg --trace pattern file.txt
# Output includes: TRACE|...|... (even more verbose)

RIPGREP_LOG=debug rg pattern file.txt
# Same as --debug, via environment variable

Example log output during search:

DEBUG|ripgrep::flags::hiargs: using 8 threads
DEBUG|ripgrep::search|search.rs:255: file.txt: binary detection: Quit
TRACE|ignore::walk|walk.rs:892: ignoring .git (hidden directory)
TRACE|grep_searcher::searcher: searching with memory map
DEBUG|ripgrep::search|search.rs:347: file.txt: found 3 matches

Log statements in search.rs:

// From the actual search.rs:
log::trace!("{}: binary detection: {:?}", path.display(), bin);

No overhead when disabled:

// When max_level is Off:
log::debug!("expensive: {}", compute_something());
// Expands to roughly:
if log::max_level() >= Debug {  // false, so...
    // This entire block is skipped
    // compute_something() is never called
}

Quick Reference: Log Crate API

// Setting up
log::set_max_level(LevelFilter::Debug);  // Enable debug and above
log::set_logger(&LOGGER)?;               // Register our implementation

// Emitting logs (from anywhere in the codebase)
log::trace!("very verbose: {}", detail);
log::debug!("debugging: {}", info);
log::info!("general info: {}", msg);
log::warn!("warning: {}", issue);
log::error!("error: {}", err);

// Level filters
log::LevelFilter::Off    // Nothing
log::LevelFilter::Error  // Only errors
log::LevelFilter::Warn   // Warn and above
log::LevelFilter::Info   // Info and above
log::LevelFilter::Debug  // Debug and above
log::LevelFilter::Trace  // Everything

Complete Module (72 lines)

use log::Log;

#[derive(Debug)]
pub(crate) struct Logger(());

const LOGGER: &'static Logger = &Logger(());

impl Logger {
    pub(crate) fn init() -> Result<(), log::SetLoggerError> {
        log::set_logger(LOGGER)
    }
}

impl Log for Logger {
    fn enabled(&self, _: &log::Metadata<'_>) -> bool {
        true
    }

    fn log(&self, record: &log::Record<'_>) {
        match (record.file(), record.line()) {
            (Some(file), Some(line)) => {
                eprintln_locked!(
                    "{}|{}|{}:{}: {}",
                    record.level(), record.target(), file, line, record.args()
                );
            }
            (Some(file), None) => {
                eprintln_locked!(
                    "{}|{}|{}: {}",
                    record.level(), record.target(), file, record.args()
                );
            }
            _ => {
                eprintln_locked!(
                    "{}|{}: {}",
                    record.level(), record.target(), record.args()
                );
            }
        }
    }

    fn flush(&self) {}
}

Relationship to Other Modules

┌─────────────────────────────────────────────────────────────┐
│                    ripgrep binary                           │
├─────────────────────────────────────────────────────────────┤
│  main.rs         │ Calls Logger::init() on --debug/--trace │
│  logger.rs       │ Implements Log trait (this file)         │
│  messages.rs     │ Provides eprintln_locked!                │
├─────────────────────────────────────────────────────────────┤
│                    Library crates                           │
├─────────────────────────────────────────────────────────────┤
│  grep-searcher   │ log::trace!("mmap decision...")          │
│  ignore          │ log::debug!("skipping hidden...")        │
│  grep-printer    │ log::trace!("formatting match...")       │
└─────────────────────────────────────────────────────────────┘

Log flow:
Library code → log::debug!() → max_level check → Logger::log() → eprintln_locked!() → stderr