440: Log Pattern Macros
Tutorial Video
Text description (accessibility)
This video demonstrates the "440: Log Pattern Macros" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Logging requires consistent metadata: timestamp, level, file, line number, and the actual message. Key difference from OCaml: 1. **Location capture**: Rust uses `file!()` and `line!()` macros in the expansion; OCaml uses `__LOC__` string (containing file:line:col).
Tutorial
The Problem
Logging requires consistent metadata: timestamp, level, file, line number, and the actual message. Writing eprintln!("[INFO] {}:{}: {}", file!(), line!(), msg) at every log site is verbose and inconsistent. Log macros capture this metadata automatically at the call site using file!() and line!() built-in macros. The log crate standardizes this with log::info!, log::warn!, log::error! macros that also support multiple backends. Understanding the underlying macro pattern explains why log calls show the correct source location.
Log macros appear in virtually every serious Rust codebase via the log crate, tracing for structured logging, and custom logging macros for specific output formats.
🎯 Learning Outcomes
file!() and line!() capture the call site location in macrosformat!($($arg:tt)*) inside a macro passes format strings and arguments throughinfo!, warn!, error!) wrap a general log! macrotrace_fn! as a structural logging pattern for function entry/exit trackinglog crate abstracts over logging backends using the same macro interfaceCode Example
#![allow(clippy::all)]
//! Log Pattern Macros
//!
//! Logging and tracing utilities.
/// Log with level and location.
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {
eprintln!(
"[{}] {}:{}: {}",
$level,
file!(),
line!(),
format!($($arg)*)
);
};
}
/// Convenience macros.
#[macro_export]
macro_rules! info { ($($arg:tt)*) => { log!("INFO", $($arg)*); }; }
#[macro_export]
macro_rules! warn { ($($arg:tt)*) => { log!("WARN", $($arg)*); }; }
#[macro_export]
macro_rules! error { ($($arg:tt)*) => { log!("ERROR", $($arg)*); }; }
/// Trace function entry/exit.
#[macro_export]
macro_rules! trace_fn {
($name:expr, $body:block) => {{
eprintln!("--> Entering {}", $name);
let result = $body;
eprintln!("<-- Exiting {}", $name);
result
}};
}
pub fn example_logged() -> i32 {
// Would use: info!("Starting computation");
42
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_example_logged() {
assert_eq!(example_logged(), 42);
}
#[test]
fn test_trace_fn() {
let result = trace_fn!("test", { 1 + 2 });
assert_eq!(result, 3);
}
#[test]
fn test_format_in_log() {
// Just verify it compiles
let x = 5;
log!("DEBUG", "value = {}", x);
}
#[test]
fn test_info_macro() {
info!("test message");
}
#[test]
fn test_nested_trace() {
let result = trace_fn!("outer", { trace_fn!("inner", { 10 }) });
assert_eq!(result, 10);
}
}Key Differences
file!() and line!() macros in the expansion; OCaml uses __LOC__ string (containing file:line:col).$($arg:tt)* passes format arguments through; OCaml's functions use ('a, Format.formatter, unit) format types.info!(...) and warn!(...) are macros, not functions; OCaml's Logs.info and Logs.warn are functions with callbacks.tracing crate adds spans and structured fields to Rust logs; OCaml's Logs is simpler, lacking structured fields without PPX.OCaml Approach
OCaml's Logs library provides level-based logging with Logs.info, Logs.warn, Logs.err. Source location is captured via __LOC__ special value. Ppx_log provides [%log.info ...] syntax capturing locations automatically. The tracing ecosystem doesn't have a direct OCaml equivalent, though Eio's traces come close for structured concurrent tracing.
Full Source
#![allow(clippy::all)]
//! Log Pattern Macros
//!
//! Logging and tracing utilities.
/// Log with level and location.
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {
eprintln!(
"[{}] {}:{}: {}",
$level,
file!(),
line!(),
format!($($arg)*)
);
};
}
/// Convenience macros.
#[macro_export]
macro_rules! info { ($($arg:tt)*) => { log!("INFO", $($arg)*); }; }
#[macro_export]
macro_rules! warn { ($($arg:tt)*) => { log!("WARN", $($arg)*); }; }
#[macro_export]
macro_rules! error { ($($arg:tt)*) => { log!("ERROR", $($arg)*); }; }
/// Trace function entry/exit.
#[macro_export]
macro_rules! trace_fn {
($name:expr, $body:block) => {{
eprintln!("--> Entering {}", $name);
let result = $body;
eprintln!("<-- Exiting {}", $name);
result
}};
}
pub fn example_logged() -> i32 {
// Would use: info!("Starting computation");
42
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_example_logged() {
assert_eq!(example_logged(), 42);
}
#[test]
fn test_trace_fn() {
let result = trace_fn!("test", { 1 + 2 });
assert_eq!(result, 3);
}
#[test]
fn test_format_in_log() {
// Just verify it compiles
let x = 5;
log!("DEBUG", "value = {}", x);
}
#[test]
fn test_info_macro() {
info!("test message");
}
#[test]
fn test_nested_trace() {
let result = trace_fn!("outer", { trace_fn!("inner", { 10 }) });
assert_eq!(result, 10);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_example_logged() {
assert_eq!(example_logged(), 42);
}
#[test]
fn test_trace_fn() {
let result = trace_fn!("test", { 1 + 2 });
assert_eq!(result, 3);
}
#[test]
fn test_format_in_log() {
// Just verify it compiles
let x = 5;
log!("DEBUG", "value = {}", x);
}
#[test]
fn test_info_macro() {
info!("test message");
}
#[test]
fn test_nested_trace() {
let result = trace_fn!("outer", { trace_fn!("inner", { 10 }) });
assert_eq!(result, 10);
}
}
Deep Comparison
OCaml vs Rust: macro log pattern
See example.rs and example.ml for side-by-side implementations.
Key Points
Exercises
log! macro to accept an optional writer: impl Write argument: log_to!(writer, INFO, "message: {}", value). Default to eprintln! when no writer is specified.slog!(level = "INFO", component = "db", "query {}", sql) that outputs {"level":"INFO","component":"db","file":"...","line":...,"msg":"..."} as JSON. Use serde_json or manual formatting.span!(name, { /* body */ }) that generates a unique span ID, logs entry with the span ID, executes the body, then logs exit with elapsed time and the same span ID — enabling correlation of nested spans in logs.