zig-skills/references/std-log.md

7.0 KiB

std.log (Zig 0.16.0)

Standardized logging interface with configurable scopes, levels, and output.

Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html

When writing custom log functions in Zig 0.16, use @EnumLiteral() instead of removed @Type(.enum_literal), use std.Io writers for direct file/stdout/stderr output, and route timestamps through a shared std.Io-aware helper.

Quick Reference

Function Purpose
log.err(fmt, args) Log error (something went wrong)
log.warn(fmt, args) Log warning (uncertain if wrong)
log.info(fmt, args) Log info (general state)
log.debug(fmt, args) Log debug (debugging only)
log.scoped(.name) Create scoped logger

Basic Usage

const std = @import("std");
const log = std.log;

pub fn main() void {
    log.info("Starting application", .{});
    log.debug("Debug value: {}", .{x});  // Hidden in release builds
    log.warn("Config missing, using defaults", .{});
    log.err("Failed to connect: {s}", .{@errorName(e)});
}

Log Levels

Level Build Mode Default Purpose
.err Enabled by the default configuration Something went wrong
.warn Enabled by the default configuration Uncertain if wrong, worth investigating
.info Enabled in common default modes General program state
.debug Commonly Debug-only by default Messages only useful for debugging

Compile-time level/scope filtering is controlled by std.options; a custom logFn also decides what its sink actually emits.

Default level by build mode:

  • Debug: .debug (all messages)
  • ReleaseSafe/Fast/Small: .info (no debug messages)

Scoped Logging

Create loggers with custom scopes for filtering:

const std = @import("std");

// Library logger with custom scope
const log = std.log.scoped(.my_library);

pub fn doWork() void {
    log.info("Processing...", .{});   // Prefixed with (my_library)
    log.debug("Details: {}", .{x});
}

Multiple scopes in one file:

const network_log = std.log.scoped(.network);
const db_log = std.log.scoped(.database);

fn fetchData() void {
    network_log.info("Connecting...", .{});
    db_log.debug("Query: {s}", .{sql});
}

Configuration via std_options

Configure logging in your root file:

const std = @import("std");

pub const std_options: std.Options = .{
    // Global log level
    .log_level = .warn,  // Only show warn and err

    // Per-scope levels (override global)
    .log_scope_levels = &.{
        .{ .scope = .my_library, .level = .debug },  // Full debug for this scope
        .{ .scope = .noisy_lib, .level = .err },     // Errors only
    },

    // Custom log function
    .logFn = myLogFn,
};

Custom Log Function

Replace the default log output:

const std = @import("std");

pub const std_options: std.Options = .{
    .logFn = myLogFn,
};

fn myLogFn(
    comptime level: std.log.Level,
    comptime scope: @EnumLiteral(),
    comptime format: []const u8,
    args: anytype,
) void {
    // Filter: only errors from unknown scopes
    const scope_prefix = switch (scope) {
        .my_app, .default => @tagName(scope),
        else => if (@intFromEnum(level) <= @intFromEnum(std.log.Level.err))
            @tagName(scope)
        else
            return,  // Skip non-error from other scopes
    };

    const level_txt = comptime level.asText();
    const prefix = "[" ++ level_txt ++ "] (" ++ scope_prefix ++ "): ";

    var buf: [64]u8 = undefined;
    const locked = std.debug.lockStderr(&buf);
    defer std.debug.unlockStderr(); // flushes the returned writer
    locked.file_writer.interface.print(prefix ++ format ++ "\n", args) catch return;
}

Check if Logging Enabled

Avoid expensive computations when logging is disabled:

const log = std.log.scoped(.my_scope);

fn process() void {
    // Check before expensive operation
    if (std.log.logEnabled(.debug, .my_scope)) {
        const debug_info = computeExpensiveDebugInfo();
        log.debug("Info: {}", .{debug_info});
    }

    // For default scope
    if (std.log.logEnabled(.debug, .default)) {
        std.log.debug("Debug message", .{});
    }
}

Level Methods

const level: std.log.Level = .warn;

// Get text representation
const text = level.asText();  // "warning"

// Compare levels (lower = more severe)
const is_error_or_worse = @intFromEnum(level) <= @intFromEnum(std.log.Level.err);

Default Log Function

Forward to the standard implementation:

fn myLogFn(
    comptime level: std.log.Level,
    comptime scope: @EnumLiteral(),
    comptime format: []const u8,
    args: anytype,
) void {
    // Emit the timestamp and message under one stderr lock so concurrent
    // records cannot split the prefix from the message.
    var buf: [128]u8 = undefined;
    const locked = std.debug.lockStderr(&buf);
    defer std.debug.unlockStderr();
    locked.file_writer.interface.print(
        "[{d}] " ++ format ++ "\n",
        .{applicationTimestampNow().toNanoseconds()} ++ args,
    ) catch return;
}

Output Format

Default output format:

level: message                     # default scope
level(scope): message              # named scope

Examples:

info: Server started on port 8080
warning(database): Connection pool exhausted
error(network): Failed to resolve hostname
debug: Variable x = 42

Common Patterns

Conditional Debug Logging

fn processItem(item: Item) void {
    if (comptime std.log.logEnabled(.debug, .default)) {
        log.debug("Processing: {}", .{item});
    }
    // ... process
}

Error Context Logging

fn loadConfig(io: std.Io, path: []const u8) !Config {
    return std.Io.Dir.cwd().openFile(io, path, .{}) catch |err| {
        log.err("Failed to open config '{s}': {s}", .{path, @errorName(err)});
        return err;
    };
}

Library Logging Pattern

// In library code
pub const log = std.log.scoped(.my_lib);

// Users can filter with:
// .log_scope_levels = &.{ .{ .scope = .my_lib, .level = .warn } }

Log to File

fn fileLogFn(
    comptime level: std.log.Level,
    comptime scope: @EnumLiteral(),
    comptime format: []const u8,
    args: anytype,
) void {
    const io = applicationIo();
    // Returns a long-lived handle opened during startup with an explicit
    // create/append policy. Plain write_only would require an existing file
    // and reopening each record could overwrite earlier data.
    const file = applicationLogFile();

    var buf: [256]u8 = undefined;
    var writer = file.writer(io, &buf);
    const w = &writer.interface;

    const level_txt = comptime level.asText();
    const scope_txt = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")";

    w.print("[{s}]{s} " ++ format ++ "\n", .{level_txt, scope_txt} ++ args) catch return;
    w.flush() catch return;
}

If multiple tasks share this sink, serialize the complete record and keep the file/writer alive; opening, seeking, writing, and closing separately for each record is not atomic.