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.
| Level | Meaning |
|---|---|
debug | Detailed information for debugging |
info | Normal useful events |
warn | Something unusual happened |
err | Something 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
parserZig 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:
.serverThis 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.logThe basic pattern is the same as file writing:
open log file
defer close log file
write log line
flush if neededA 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 8080Structured 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.