Skip to content

Logging

Logging means recording what a program is doing.

Logging means recording what a program is doing.

A log message is not ordinary output. It is a diagnostic record. It helps you understand what happened before, during, or after a program ran.

Programs use logs for:

startup messages

configuration details

warnings

errors

debugging information

performance notes

network requests

database operations

security events

A command-line program may print output for the user. A server may write logs for the operator. These are different purposes.

Printing vs Logging

This is printing:

std.debug.print("result = {}\n", .{result});

It sends text somewhere, usually standard error.

This is logging:

std.log.info("server started on port {}", .{port});

It records an event with a severity level.

Logging usually has structure:

the level

the message

the module or scope

sometimes a timestamp

sometimes file and line information

The exact output format depends on the logging backend.

Log Levels

Logs usually have levels.

LevelMeaning
debugDetailed information for debugging
infoNormal useful events
warnSomething unusual happened
errSomething failed

Use the level to express importance.

A failed file open may be err.

A fallback configuration may be warn.

A successful startup may be info.

Internal details during development may be debug.

Basic Logging

Zig provides logging through std.log.

const std = @import("std");

pub fn main() void {
    std.log.info("program started", .{});
    std.log.warn("using default configuration", .{});
    std.log.err("could not connect to server", .{});
}

Each call has the same basic shape:

std.log.info("message with {}", .{value});

The first argument is a format string.

The second argument is a tuple of values.

This is the same formatting style used by std.debug.print.

Logging Values

You can include values in log messages:

const std = @import("std");

pub fn main() void {
    const port: u16 = 8080;
    const debug = true;

    std.log.info("port = {}", .{port});
    std.log.info("debug = {}", .{debug});
}

For strings, use {s}:

const name = "server";
std.log.info("starting {s}", .{name});

For quick debugging of complex values, {any} may be useful:

std.log.debug("value = {any}", .{value});

For public or long-term logs, prefer deliberate messages over raw debug dumps.

Debug Logs

Debug logs are for details that are useful while developing or diagnosing a problem.

std.log.debug("parsed {} records", .{count});

A debug log should not normally be needed to understand ordinary program operation.

Good debug logs answer questions like:

What input did this function receive?

Which branch did the code take?

How many items were processed?

How long did this step take?

Do not flood logs with meaningless debug messages. Too much logging can hide the important line.

Info Logs

Info logs describe normal events.

std.log.info("server listening on 127.0.0.1:9000", .{});

Good info logs are useful to someone operating the program.

Examples:

program started

configuration loaded

server listening

database migration complete

background job finished

Do not log every tiny operation at info level. Save info logs for events that matter.

Warning Logs

Warning logs mean something unusual happened, but the program can continue.

std.log.warn("config file missing, using defaults", .{});

Warnings are useful when the program recovers from a problem.

Examples:

optional file missing

deprecated option used

retrying after temporary failure

slow response detected

unexpected but recoverable input

A warning should make the reader think: this may need attention.

Error Logs

Error logs mean something failed.

std.log.err("failed to open config file: {}", .{err});

Use error logs for real failures.

Examples:

could not open required file

database connection failed

HTTP request failed

invalid configuration

child process exited with failure

Be careful not to log the same error many times at different layers. Duplicate error logs make debugging harder.

Log Once at the Boundary

A common mistake is logging an error inside a helper function and then returning the error, where the caller logs it again.

Example:

fn loadConfig() !void {
    std.log.err("failed to load config", .{});
    return error.ConfigFailed;
}

pub fn main() !void {
    try loadConfig();
}

This is usually poor design for a library function.

Better:

fn loadConfig() !void {
    return error.ConfigFailed;
}

pub fn main() void {
    loadConfig() catch |err| {
        std.log.err("failed to load config: {}", .{err});
        return;
    };
}

Log at the boundary where you have enough context and know what should happen next.

For a command-line program, that boundary is often main.

For a server, it may be the request handler, connection handler, or job runner.

Logging Errors

Errors are values in Zig.

You can log them directly:

const result = openFile() catch |err| {
    std.log.err("openFile failed: {}", .{err});
    return;
};

The output includes the error name.

A better message adds context:

const path = "config.json";

const file = openConfig(path) catch |err| {
    std.log.err("could not open config file {s}: {}", .{ path, err });
    return;
};

The path matters. Without it, the log only says an operation failed.

Useful error logs usually include:

what operation failed

which input was involved

the error value

what the program will do next, if relevant

Scoped Logs

Large programs often use log scopes.

A scope identifies which subsystem produced the message.

Examples:

server
database
http
cache
parser

Zig supports scoped logging through std.log.scoped.

const std = @import("std");

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

pub fn main() void {
    log.info("started", .{});
    log.warn("slow request", .{});
}

The scope is:

.server

This helps distinguish messages from different parts of the program.

A parser can use:

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

A database layer can use:

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

Scoped logs become more useful as the program grows.

Custom Log Functions

For small programs, the default logging behavior is enough.

For larger programs, you may want custom logging.

A custom logger can decide:

where logs go

which levels are enabled

how messages are formatted

whether timestamps are included

whether colors are used

whether logs are written as JSON

Zig allows programs to override the default logging function at compile time.

The exact customization details belong in an advanced section, but the idea is simple: std.log is an interface, and your program can control the implementation.

Logging to Files

Some programs should write logs to files.

For example:

server.log
worker.log
error.log

The basic pattern is the same as file writing:

open log file
defer close log file

write log line
flush if needed

A real file logger needs more design:

Should the file append or overwrite?

Should logs rotate when the file grows?

Should each line include a timestamp?

Should errors go to a separate file?

Should logs be buffered?

For beginner programs, standard error is usually enough. File logging becomes useful when the program runs for a long time or runs without a terminal.

Structured Logging

Plain text logs are easy to read.

Example:

server started on port 8080

Structured logs are easier for machines to process.

Example JSON log:

{"level":"info","event":"server_started","port":8080}

Structured logs are common in servers and distributed systems.

They make it easier to search, filter, count, and analyze logs.

For small command-line tools, plain logs are fine.

For production services, structured logs are often better.

Do Not Log Secrets

Logs often live longer than you expect.

They may be copied to files, monitoring systems, crash reports, shared debugging sessions, or cloud services.

Never log secrets.

Do not log:

passwords

API keys

session tokens

authorization headers

private keys

database credentials

full payment details

personal data unless necessary

Bad:

std.log.info("token = {s}", .{token});

Better:

std.log.info("token loaded", .{});

When debugging authentication, log whether a token exists, not the token itself.

Logging User Input

User input can be hostile or huge.

Be careful when logging it.

Problems include:

very long strings

terminal escape sequences

private information

binary data

malformed UTF-8

A safe log may include a length or a sanitized preview:

std.log.warn("invalid input, length={}", .{input.len});

This is safer than dumping the whole input.

Performance Cost

Logging has a cost.

Formatting takes CPU time.

Writing logs takes I/O.

Allocating log strings takes memory.

Large logs take disk space.

Debug logging inside tight loops can slow a program dramatically.

Example:

for (items) |item| {
    std.log.debug("item = {}", .{item});
}

This may be fine for five items. It may be terrible for five million.

Use logging deliberately in hot paths.

Logging and Tests

Tests should not depend on log text unless the log output is part of the behavior being tested.

Logs are diagnostics. They may change as the code improves.

For tests, check return values, state changes, output files, or explicit results.

Use logs to help debug failing tests, not as the main assertion target.

A Small Example

Here is a small program that logs configuration loading.

const std = @import("std");

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

const Config = struct {
    port: u16,
    debug: bool,
};

fn loadConfig() !Config {
    log.debug("loading configuration", .{});

    const config = Config{
        .port = 8080,
        .debug = false,
    };

    log.info("configuration loaded: port={}, debug={}", .{
        config.port,
        config.debug,
    });

    return config;
}

pub fn main() void {
    const config = loadConfig() catch |err| {
        log.err("configuration failed: {}", .{err});
        return;
    };

    std.log.info("starting server on port {}", .{config.port});
}

This example shows three habits.

Use a scope for the subsystem.

Log useful events.

Log errors at the boundary.

A Better Error Message

Compare these two logs:

std.log.err("failed: {}", .{err});

and:

std.log.err("could not open config file {s}: {}", .{ path, err });

The second one is better.

It tells you what failed.

It tells you which file was involved.

It includes the error value.

Good logs reduce guessing.

Common Mistakes

Do not use logs as a replacement for error handling.

Do not log secrets.

Do not log the same error repeatedly at every layer.

Do not write vague messages like failed.

Do not use info for noisy internal details.

Do not ignore the performance cost of logging in tight loops.

Do not rely on logs for program correctness.

The Core Pattern

For simple logs:

std.log.info("message", .{});

For values:

std.log.info("processed {} items", .{count});

For errors:

operation() catch |err| {
    std.log.err("operation failed: {}", .{err});
    return;
};

For scoped logs:

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

log.debug("parsed token: {s}", .{token});

What You Should Remember

Logging records diagnostic events.

Printing is for output. Logging is for understanding program behavior.

Use log levels deliberately.

Use scopes in larger programs.

Include context in error logs.

Log errors where you know what they mean.

Do not log secrets.

Be careful with user input.

Logging should make a program easier to operate and debug without becoming noise.