Skip to content

Instantly share code, notes, and snippets.

@kassane
Forked from leecannon/std_log.md
Last active November 18, 2024 10:34
Show Gist options
  • Save kassane/a81d1ae2fa2e8c656b91afee8b949426 to your computer and use it in GitHub Desktop.
Save kassane/a81d1ae2fa2e8c656b91afee8b949426 to your computer and use it in GitHub Desktop.
Quick overview of Zig's `std.log`

A simple overview of Zig's std.log for Zig v0.12.0 or higher

Logging functionality that supports:

  • If a log message should be printed is determined at comptime, meaning zero overhead for unprinted messages (so just leave the code peppered with debug logs, but when it makes sense scope them; so downstream users can filter them out)
  • Scoped log messages
  • Different log levels per scope
  • Overrideable log output (write to file, database, etc.)
  • All the standard std.fmt formatting magic

Basic Usage:

Just call std.log.X where X is the desired log level, for example:

std.log.info("This is an info message", .{});
std.log.err("This is an err message - {}", .{42});

Output:

info: This is an info message
err: This is an err message - 42

Log Levels:

The log level of a message determines if it will be printed.

Only if the level of the message is of an equal or greater severity than the global log level will it be printed (there is also overriding of the log level per scope to take into account, see below).

The log levels are, in order of severity:

  • err - Error: A bug has been detected or something has gone wrong but it is recoverable.
  • warn - Warning: it is uncertain if something has gone wrong or not, but the circumstances would be worth investigating.
  • info - Informational: general messages about the state of the program.
  • debug - Debug: messages only useful for debugging.

But what is the global log level?

The default log level is determined by the build mode like this:

// The default log level is based on build mode.
pub const default_level: Level = switch (builtin.mode) {
    .Debug => .debug,
    .ReleaseSafe => .info,
    .ReleaseFast, .ReleaseSmall => .err,
};

Overriding this is easy just define a public log_level decl in the root file (where ever main is), like this:

pub const std_options = .{
    // Set the log level to info
    .log_level = .info,
};

Scopes

All log messages include a scope.

When using the simple std.log.info and friends the scope default is used implicitly, the default log output function does not print the scope for messages with default as the scope.

In order to produce log messages with a different scope a scoped log needs to be created:

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

my_log.info("Hello from my_scope", .{});
std.log.info("Hello from default scope", .{});

Output:

info(my_scope): Hello from my_scope
info: Hello from default scope

Overriding the log output

Overriding the log output allows great flexibility, libraries you use can just blindly log and the application decides where these message go; whether that is to stdout/stderr, file, database, socket, serial port, etc.

To to do so define a public log function in the root file (where ever main is), like this:

pub const std_options = .{
    .logFn = myLogFn,
};

pub fn myLogFn(
    comptime level: std.log.Level,
    comptime scope: @TypeOf(.EnumLiteral),
    comptime format: []const u8,
    args: anytype,
) void {
   // Implementation
}

Checking the global log level and per scope log level is already done for you, meaning this function will only be called when there is a log message that should be output.

References

@johnbcodes
Copy link

Thanks for updating the original!

scope_levels has been changed to log_scope_levels

@johnbcodes
Copy link

It also looks like there were changes recently whereby declarations must be inside a root std_options declaration, like so:

pub const std_options = struct {
    pub const log_level = .debug;

    pub const log_scope_levels = &[_]std.log.ScopeLevel{
        .{ .scope = .library_a, .level = .debug },
        .{ .scope = .library_b, .level = .info },
    };
};

Sample program:

const std = @import("std");

pub const std_options = struct {
    pub const log_level = .debug;

    pub const log_scope_levels = &[_]std.log.ScopeLevel{
        .{ .scope = .library_a, .level = .debug },
        .{ .scope = .library_b, .level = .info },
    };
};

const lib_a = std.log.scoped(.library_a);
const lib_b = std.log.scoped(.library_b);

pub fn main() !void {
    lib_a.debug("Lib A", .{});
    lib_b.debug("Lib B Debug", .{}); // No output
    lib_b.info("Lib B Info", .{});
    lib_b.warn("Lib B Warn", .{});
    lib_b.err("Lib B Err", .{});
}

Output:

❯ zig build run
debug(library_a): Lib A
info(library_b): Lib B Info
warning(library_b): Lib B Warn
error(library_b): Lib B Err

@kassane
Copy link
Author

kassane commented Sep 18, 2023

Wow. Thanks for improvement. (updated)

@david-haerer
Copy link

david-haerer commented Mar 17, 2024

To override the log output, the custom log function must also be declared in the std_options, i.e.

pub const std_options = .{
    .logFn = log,
};

To append the logs to a file ~/.local/share/my-app.log, I'm using the following log function:

pub fn log(
    comptime level: std.log.Level,
    comptime scope: @Type(.EnumLiteral),
    comptime format: []const u8,
    args: anytype,
) void {
    const allocator = std.heap.page_allocator;
    const home = std.os.getenv("HOME") orelse {
        std.debug.print("Failed to read $HOME.\n", .{});
        return;
    };
    const path = std.fmt.allocPrint(allocator, "{s}/{s}", .{ home, ".local/share/my-app.log" }) catch |err| {
        std.debug.print("Failed to create log file path: {}\n", .{err});
        return;
    };
    defer allocator.free(path);

    const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| {
        std.debug.print("Failed to open log file: {}\n", .{err});
        return;
    };
    defer file.close();

    const stat = file.stat() catch |err| {
        std.debug.print("Failed to get stat of log file: {}\n", .{err});
        return;
    };
    file.seekTo(stat.size) catch |err| {
        std.debug.print("Failed to seek log file: {}\n", .{err});
        return;
    };

    const prefix = "[" ++ comptime level.asText() ++ "] " ++ "(" ++ @tagName(scope) ++ ") ";

    var buffer: [256]u8 = undefined;
    const message = std.fmt.bufPrint(buffer[0..], prefix ++ format ++ "\n", args) catch |err| {
        std.debug.print("Failed to format log message with args: {}\n", .{err});
        return;
    };
    file.writeAll(message) catch |err| {
        std.debug.print("Failed to write to log file: {}\n", .{err});
    };
}

See https://ziglang.org/documentation/master/std/#std.log for a code example in the the official docs.
See ziglang/zig#14375 (comment) for the discussion about appending to a file.

Edit: Remove debug print from when I was testing the log function.
Edit 2: Update name of log file to match the example path.

@kassane
Copy link
Author

kassane commented Mar 19, 2024

updated again - custom logFn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment