Loading pageā¦
Rust walkthroughs
Loading pageā¦
log crate's log_enabled! macro help with conditional logging overhead?The log_enabled! macro allows you to check whether a specific log level is enabled before executing expensive logging operations. This prevents the cost of computing log arguments when the output would be discarded anyway, which is especially important for debug and trace logging that often involves complex string formatting or data introspection.
use log::{debug, info};
fn process_data(data: &mut [u8]) {
// Expensive operation happens BEFORE debug! checks level
debug!("Processing {} bytes: {:?}", data.len(), expensive_format(data));
// Even at INFO level, expensive_format() still runs!
// Its result is just discarded
}
fn expensive_format(data: &[u8]) -> String {
// Simulated expensive computation
data.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(":")
}Arguments to logging macros are always evaluated, even when the log level is disabled.
use log::Level;
fn level_checking() {
// The logging facade checks the level at runtime
// Debug logging at INFO level:
// 1. Arguments are evaluated
// 2. Level is checked (DEBUG < INFO)
// 3. Output is discarded
// The check happens INSIDE the macro
// Arguments are evaluated BEFORE the check
// This is why expensive arguments are costly
}Arguments are evaluated eagerly before the level check occurs.
use log::{debug, log_enabled, Level};
fn process_data(data: &mut [u8]) {
// Check BEFORE computing expensive values
if log_enabled!(Level::Debug) {
debug!("Processing {} bytes: {:?}", data.len(), expensive_format(data));
}
// Now expensive_format only runs if DEBUG is enabled
}
fn expensive_format(data: &[u8]) -> String {
data.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(":")
}The log_enabled! macro guards expensive computations.
use log::{log_enabled, Level};
fn syntax_examples() {
// Check if DEBUG level is enabled
if log_enabled!(Level::Debug) {
// Debug logging is active
}
// Check if TRACE level is enabled
if log_enabled!(Level::Trace) {
// Trace logging is active
}
// Check against the logger's max level
if log_enabled!(Level::Info) {
// Info logging is active
}
// Target-specific check (advanced)
if log_enabled!(target: "my_module", Level::Debug) {
// Debug logging for specific target
}
}The macro accepts a Level variant and optionally a target.
use log::{debug, log_enabled, Level};
use std::time::Instant;
fn performance_demo() {
let data = vec![0u8; 1000];
// WITHOUT log_enabled!
let start = Instant::now();
for _ in 0..10_000 {
debug!("Data: {:?}", expensive_format(&data));
}
let without_check = start.elapsed();
// WITH log_enabled!
let start = Instant::now();
for _ in 0..10_000 {
if log_enabled!(Level::Debug) {
debug!("Data: {:?}", expensive_format(&data));
}
}
let with_check = start.elapsed();
// When DEBUG is disabled:
// without_check: expensive_format runs 10,000 times
// with_check: expensive_format never runs
println!("Without check: {:?}", without_check);
println!("With check: {:?}", with_check);
}
fn expensive_format(data: &[u8]) -> String {
// Simulated expensive work
data.iter().map(|b| b.to_string()).collect::<Vec<_>>().join(",")
}When logging is disabled, the check avoids all computation.
use log::{debug, trace, log_enabled, Level};
struct ComplexState {
data: Vec<Vec<u8>>,
metadata: std::collections::HashMap<String, String>,
}
impl ComplexState {
fn process(&self) {
// GOOD: Cheap operations don't need guarding
debug!("Processing state");
debug!("Data count: {}", self.data.len());
// GOOD: Guard expensive operations
if log_enabled!(Level::Debug) {
debug!("Full state: {:?}", self);
}
// GOOD: Guard complex computations
if log_enabled!(Level::Trace) {
for (i, chunk) in self.data.iter().enumerate() {
trace!("Chunk {}: {:?}", i, hex_dump(chunk));
}
}
// GOOD: Guard serialization
if log_enabled!(Level::Debug) {
let json = serde_json::to_string_pretty(&self.metadata).unwrap();
debug!("Metadata JSON:\n{}", json);
}
}
}
fn hex_dump(data: &[u8]) -> String {
// Expensive hex formatting
data.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(" ")
}Guard expensive operations like serialization, complex formatting, and iteration.
use log::{debug, info, warn, log_enabled, Level};
fn unnecessary_guards() {
// UNNECESSARY: Simple formatting is cheap
if log_enabled!(Level::Debug) {
debug!("Count: {}", 42); // This is already cheap
}
// JUST WRITE:
debug!("Count: {}", 42);
// UNNECESSARY: Info and above are usually enabled in production
if log_enabled!(Level::Info) {
info!("Important event");
}
// JUST WRITE:
info!("Important event");
// UNNECESSARY: Error logging should always be fast
if log_enabled!(Level::Error) {
warn!("Something went wrong");
}
// JUST WRITE:
warn!("Something went wrong");
}Simple formatting and high-priority logging don't need guards.
use log::{debug, log_enabled, Level};
// Module-specific logging configuration
fn module_logging() {
// Check for specific logging target
if log_enabled!(target: "database", Level::Debug) {
debug!(target: "database", "Query executed");
}
if log_enabled!(target: "network", Level::Trace) {
trace!(target: "network", "Packet received: {:?}", format_packet());
}
}
fn format_packet() -> String {
// Expensive packet formatting
String::new()
}Target-specific checks enable fine-grained logging control.
use log::{debug, trace, log_enabled, Level};
// Pattern 1: Detailed iteration logging
fn process_items(items: &[Item]) {
debug!("Processing {} items", items.len());
if log_enabled!(Level::Trace) {
for (i, item) in items.iter().enumerate() {
trace!("Item {}: {:?}", i, item);
}
}
// Process...
}
// Pattern 2: Conditional state dump
fn validate_state(state: &State) -> Result<(), Error> {
if log_enabled!(Level::Debug) {
debug!("Validating state: {:#?}", state);
}
// Validate...
Ok(())
}
// Pattern 3: Expensive diagnostic computation
fn diagnose_issue(data: &[u8]) -> String {
// Expensive diagnostic computation
format!("Length: {}, Hash: {:x}",
data.len(),
blake3::hash(data)) // Hypothetical
}
fn handle_data(data: &[u8]) {
if log_enabled!(Level::Debug) {
debug!("Diagnostics: {}", diagnose_issue(data));
}
}These patterns avoid expensive work when logging is disabled.
use log::Level;
fn level_hierarchy() {
// Levels from lowest to highest:
// Trace < Debug < Info < Warn < Error
// If INFO is enabled:
// - Trace is disabled
// - Debug is disabled
// - Info is enabled
// - Warn is enabled
// - Error is enabled
// log_enabled! respects this hierarchy
if log_enabled!(Level::Debug) {
// Only runs if current level is Debug or lower
}
}The level hierarchy determines which logs are emitted.
use log::{debug, error, info, log_enabled, trace, warn, Level};
fn combined_usage() {
// Multiple checks for different levels
if log_enabled!(Level::Trace) {
trace!("Very detailed: {:?}", full_dump());
} else if log_enabled!(Level::Debug) {
debug!("Summary: {:?}", summary());
}
// Nested expensive operations
if log_enabled!(Level::Debug) {
let state = compute_expensive_state();
debug!("State: {:?}", state);
if log_enabled!(Level::Trace) {
trace!("State details: {:?}", detailed_state(&state));
}
}
}
fn full_dump() -> String { String::new() }
fn summary() -> String { String::new() }
fn compute_expensive_state() -> String { String::new() }
fn detailed_state(s: &str) -> String { String::new() }Checks can be combined for layered logging.
use log::{debug, log_enabled, Level};
struct RequestContext {
request_id: String,
user_id: u64,
timestamp: i64,
}
fn handle_request(ctx: &RequestContext, data: &[u8]) {
// Cheap to log
debug!("[{}] Handling request for user {}", ctx.request_id, ctx.user_id);
// Expensive to compute, guard it
if log_enabled!(Level::Debug) {
let hex_data: String = data.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.chunks(16)
.map(|c| c.join(" "))
.collect::<Vec<_>>()
.join("\n");
debug!("[{}] Request payload:\n{}", ctx.request_id, hex_data);
}
}Guard expensive context-specific logging.
use log::{debug, log_enabled, trace, Level};
fn demonstrate_behavior() {
// In debug builds, logging might be more verbose
// In release builds, only info/warn/error might be enabled
// This expensive trace logging:
if log_enabled!(Level::Trace) {
trace!("Full execution trace: {:?}", build_trace());
}
// Will:
// - Run in debug builds (if trace enabled)
// - Skip entirely in release (if trace disabled)
}
fn build_trace() -> String {
// Expensive trace building
String::new()
}The macro adapts to the logging configuration at runtime.
// What log_enabled! expands to (conceptually):
// log_enabled!(Level::Debug)
// becomes something like:
// log::logger().enabled(&log::Record::builder().level(Level::Debug).build())
// This is a runtime check against the current logger's max level
// It's very fast - just a comparison in most implementations
use log::{log_enabled, Level};
fn check_cost() {
// The log_enabled! check itself is very cheap
// It's just a level comparison
let start = std::time::Instant::now();
for _ in 0..1_000_000 {
let _ = log_enabled!(Level::Debug);
}
let elapsed = start.elapsed();
// This is typically sub-microsecond for 1 million checks
println!("1M checks: {:?}", elapsed);
}The check itself is extremely cheap; it just avoids expensive argument evaluation.
use log::{debug, info, log_enabled, trace, Level};
use std::collections::HashMap;
struct Cache {
data: HashMap<String, Vec<u8>>,
hits: u64,
misses: u64,
}
impl Cache {
fn get(&mut self, key: &str) -> Option<&[u8]> {
// Always log cache operations at debug
debug!("Cache lookup for key: {}", key);
let result = self.data.get(key);
if result.is_some() {
self.hits += 1;
debug!("Cache hit for {}", key);
} else {
self.misses += 1;
debug!("Cache miss for {}", key);
}
// Guard expensive dump
if log_enabled!(Level::Trace) {
trace!("Cache state: {} entries, {} hits, {} misses",
self.data.len(), self.hits, self.misses);
for (k, v) in &self.data {
trace!(" {} -> {} bytes", k, v.len());
}
}
result.map(|v| v.as_slice())
}
fn stats(&self) -> String {
// This is called explicitly, so no guard needed
format!("Hits: {}, Misses: {}, Ratio: {:.2}%",
self.hits, self.misses,
if self.hits + self.misses > 0 {
100.0 * self.hits as f64 / (self.hits + self.misses) as f64
} else {
0.0
})
}
}Production code benefits from strategic use of log_enabled!.
log_enabled! prevents wasted computation in logging:
When to use it:
| Scenario | Use log_enabled!? |
|----------|-------------------|
| Simple formatting ("count: {}") | No |
| Integer/string interpolation | No |
| Serialization (serde_json::to_string) | Yes |
| Iteration over collections | Yes |
| Complex computations | Yes |
| Debug/Trace level logging | Often |
| Info/Warn/Error level | Rarely |
Performance impact:
// WITHOUT guard (DEBUG disabled):
// expensive_format() runs, result discarded
debug!("Data: {:?}", expensive_format(&data));
// WITH guard (DEBUG disabled):
// expensive_format() never runs
if log_enabled!(Level::Debug) {
debug!("Data: {:?}", expensive_format(&data));
}Best practices:
Common pattern:
use log::{debug, log_enabled, Level};
fn process(data: &[u8]) {
// Cheap: no guard needed
debug!("Processing {} bytes", data.len());
// Expensive: guard it
if log_enabled!(Level::Debug) {
debug!("Data: {:?}", expensive_format(data));
}
}log_enabled! is a zero-cost abstraction when logging is disabledāthe check prevents all argument evaluation overhead.