A pretty logger for Zig, inspired by pretty_env_logger.
The logger works together with the std.log API.
It provides a logFn function that can be set to your std.Options.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{});
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
Update to latest version:
zig fetch --save git+https://github.com/knutwalker/env-logger.gitAdd to build.zig:
exe.root_module.addImport("env-logger", b.dependency("env-logger", .{}).module("env-logger"));Important
env-logger tracks Zig 0.16.0
Setting up the logger happens in two steps:
- Call
env_logger.setup(.{})and set the value as yourstd.Options. - Call
env_logger.init(init, .{})once and as early as possible to initialize the logger. The first argument is thestd.process.Initinstance and provides theIoinstance, environment, and allocators.
env-logger will read the ZIG_LOG environment variable and parse it as the log level.
It logs colored messages to stderr by default.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
Zig does not define a .trace log level in std.log.Level.
env-logger can still log trace messages at a .trace level.
First enable this in the setup opts.
To log a trace message, prefix a debug message with TRACE: (including the colon and space).
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{
.enable_trace_level = true,
});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{});
if (!env_logger.defaultLevelEnabled(.trace)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=trace ...`\n", .{});
}
std.log.debug("TRACE: debug message", .{});
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
By default, the logger will look for the ZIG_LOG environment variable in order to configure the log level.
If you want to use a different environment variable, set the name in thefilter option.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{
.filter = .{ .env = .{ .name = "MY_LOG_ENV" } },
});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env MY_LOG_ENV=debug ...`\n", .{});
}
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
Scoped loggers other than the .default scope will be included in the log message.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
const log = std.log.scoped(.scope);
log.debug("debug message", .{});
log.info("info message", .{});
log.warn("warn message", .{});
log.err("error message", .{});
}
env-logger sets the log_level in std.Options to .debug in order to apply to actual level at runtime.
You can set the min_log_level in setup to override this and full disable certain levels.
const std = @import("std");
const env_logger = @import("env_logger");
// Setting a log_level here will always discard any message of a lower level
// even if the filter would allow them. Higher levels can still be filtered.
pub const std_options = env_logger.setup(.{ .min_log_level = .info });
pub fn main(init: std.process.Init) !void {
// can also set the runtime level directly without parsing
env_logger.init(init, .{ .filter = .{ .level = .debug } });
// std.log.logEnabled returns false for debug, which will effectively
// remove all calls to log.debug at comptime
try std.testing.expect(std.log.logEnabled(.debug, .default) == false);
// env_logger still reports that debug is enabled according to the filter logic
try std.testing.expect(env_logger.levelEnabled(.default, .debug) == true);
// struct does not have a format function, but debug calls are eliminated
// so this is never reported by the compiler and compilation succeeds
std.log.debug("you will never see me: {f}", .{struct {}});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
In case you want to set other std.Options, you can use the env_logger.setupWith function.
Alternatively, you can use the env_logger.loggerFn function and set the logFn field.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setupWith(
.{},
// The second argument is the std.Options that will be set.
.{
.fmt_max_depth = 64,
},
);
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
You can disable the level and logger parts of the log message and only render the message itself.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{
.render_level = false,
.render_logger = false,
});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
const log = std.log.scoped(.scope);
log.debug("debug message", .{});
log.info("info message", .{});
log.warn("warn message", .{});
log.err("error message", .{});
}
You can also add timestamps to the log messages.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{
.render_timestamp = true,
});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
By default, the logger logs to stderr, but it can also be configured to log to stdout, append to a file, or write to a writer.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
var args = try init.minimal.args.iterateAllocator(init.gpa);
defer args.deinit();
_ = args.next() orelse return; // skip the executable name
var output: env_logger.InitOptions.Output = .stderr;
var buf: ?std.Io.Writer.Allocating = null;
defer if (buf) |*b| b.deinit();
if (args.next()) |output_filename| {
if (std.mem.eql(u8, output_filename, "-")) {
output = .stdout;
} else if (std.mem.eql(u8, output_filename, "+")) {
buf = .init(init.gpa);
output = .{ .writer = &buf.?.writer };
} else {
const output_file = try std.Io.Dir.cwd().createFile(
init.io,
output_filename,
// To append to the log, set `truncate` to false and `read` to true.
// Alternatively, use `file_start` to always write from the start
// and not require read permissions.
.{ .read = true, .truncate = false },
);
output = .{ .file = output_file };
}
}
env_logger.init(init, .{ .output = output });
// deinit will close any eventual file handles
defer env_logger.deinit();
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
if (buf) |*b| {
std.debug.print("Contents of buffer:\n{s}\n", .{b.written()});
}
}
By default, the logger will detect if the terminal supports colors and use them.
You can disable this by setting the enable_color option to false.
Alternatively, you can force the logger to use colors by setting the force_color option to true.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init) !void {
env_logger.init(init, .{
// disable all use of colors,
.enable_color = false,
// force the use of colors, also for files and writers
.force_color = true,
});
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
// try piping stderr to a file, it's still colored
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
Parsing and constructing the log filter requires allocations.
The default pattern is to pass the std.process.Init instance to init.
This uses the allocators setup by the juicy main function.
Skipping this via initRaw uses a std.heap.page_allocator and leaks memory.
If you want to use a different allocator, you can set the allocator option in init.
You can also drop down to initMin and pass std.process.Init.Minimal.
Since the filter is supposed to be kept for the remainder
of the program's lifetime, you can set two different allocators, one
for all the parsing (e.g. a gpa, like the DebugAllocator), and another
one for the final filter allocation (e.g. an arena allocator).
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main(init: std.process.Init.Minimal) !void {
var gpa: std.heap.DebugAllocator(.{ .verbose_log = true }) = .init;
defer if (gpa.deinit() == .leak) @panic("memory leak");
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
env_logger.initMin(init, .{ .allocator = .{ .split = .{
.parse_gpa = gpa.allocator(),
.filter_arena = arena.allocator(),
} } });
if (!env_logger.defaultLevelEnabled(.debug)) {
std.debug.print("To see all log messages, run with `env ZIG_LOG=debug ...`\n", .{});
}
std.log.debug("debug message", .{});
std.log.info("info message", .{});
std.log.warn("warn message", .{});
std.log.err("error message", .{});
}
Generally parsing the filter options at runtime requires allocations, even of a single level is provided.
It is possible to use env-logger allocation-free, but the API is not as straight-forward.
const std = @import("std");
const env_logger = @import("env_logger");
pub const std_options = env_logger.setup(.{});
pub fn main() !void {
// the only allocation free filter is using the `filter` branch of the `filter` enum.
// A `Filter` can be built by wrapping a static `ScopeLevel`.
// Consider the lifetime of that reference to be static, it must not move or change.
const level: env_logger.ScopeLevel = .of(.debug);
const single_filter: env_logger.Filter = .single(&level);
_ = single_filter;
// An alternative is to wrap an non-const slice.
// The same lifetime considerations apply.
var filters: [2]env_logger.ScopeLevel = .{ .scoped(.scope, .debug), .of(.info) };
const filter: env_logger.Filter = .filters(&filters);
// avoid the default leaky allocation
const gpa = std.mem.Allocator.failing;
// The last allocation to avoid is the default write buffer
var buf: [1024]u8 = undefined;
// initRaw does require any juicy-main init instance
env_logger.initRaw(.{
.allocator = .{ .arena = gpa },
.filter = .{ .filter = filter },
.write_buffer = &buf,
});
std.log.debug("default debug message (not visible)", .{});
const scoped = std.log.scoped(.scope);
scoped.debug("scoped debug message", .{});
std.log.info("info message", .{});
scoped.info("scoped info message", .{});
}
Contributions are welcome!
Please open an issue or a pull request if you have any suggestions or improvements.
env-logger is licensed under the MIT License








