Have Zig's std.log log to a file instead of to stderr

migrate to zig 0.14.0, improve configuration

reykjalin.org 0ab04e6c b30c6508

verified
+264 -69
+3
.gitignore
··· 1 1 .zig-cache 2 2 zig-out 3 + 4 + custom-logs 5 + logs
+23
CHANGELOG.md
··· 1 + # Changelog 2 + 3 + ## [work-in-progress] 2.0.0 4 + 5 + ### Bug fixes 6 + 7 + * Removed general purpose allocator initialization that was unused. This was optimized away by the 8 + Zig compiler, but the code shouldn't be there. 9 + 10 + ### Breaking changes 11 + 12 + * `pub const log_to_file_path` no longer works. Use `pub const log_to_file_options` instead. See 13 + [the readme](./README.md) for deatils or take a look at [the examples](./examples/README.md). 14 + * The default, no-config log location is now `./logs/out.log` **in `Debug` mode**. 15 + * The default, no-config log location is now `~/.local/logs/out.log` **in `ReleaseFast`, 16 + `ReleaseSmall`, and `ReleaseSafe` modes**. 17 + 18 + 19 + ## 1.0.0 20 + 21 + * Logs to `./log` by default. 22 + * You can change log file path by setting `pub const log_to_file_path = "/new/path/to/log"` in 23 + you root source file (typically `src/main.zig` or similar).
+45 -14
README.md
··· 1 1 # log_to_file.zig 2 2 3 - An easy way to change the default `std.log` functions to write to a file instead of logging to 4 - STDOUT. 5 - Simply add this to your project via 6 - 7 - and `std.log` functions will write to a file called `log` in your current working directory. 3 + An easy way to change Zig's default `std.log` functions to write to a file instead of logging to 4 + STDOUT/STDERR. 8 5 9 - > **NOTE** 10 - > 6 + > [!NOTE] 11 7 > **This is not a logging library!** 12 8 > It offers a function you can pass to Zig's `std_options`'s `.logFn` so `std.log` calls write to a 13 9 > file instead of STDOUT. 14 10 11 + > [!IMPORTANT] 12 + > **Version 2.0.0 introduced breaking changes**. Read through this readme to see what changed, or 13 + > check [the changelog](./CHANGELOG.md). 14 + 15 15 ## Usage 16 16 17 17 1. Install the library by running 18 - `zig fetch --save https://git.sr.ht/~reykjalin/log_to_file.zig/archive/1.0.0.tar.gz` 18 + `zig fetch --save git+https://git.sr.ht/~reykjalin/log_to_file.zig` 19 19 in your project. 20 + * You can also use `zig fetch --save git+https://github.com/reykjalin/log_to_file.zig.git` if you prefer. 20 21 2. Add the library as a dependency in your project's `build.zig`: 21 22 22 23 ```zig ··· 46 47 }; 47 48 ``` 48 49 49 - Now, whenever you call the `std.log` functions they should be written to a `log` file in your 50 - current working directory. 50 + Now, whenever you call the `std.log` functions they should be written to a `logs/out.log` file in 51 + your current working directory when you make a `Debug` build, and `~/.local/logs/out.log` in a 52 + `Relase` build. 51 53 52 54 ## Configuration 53 55 56 + [The examples](./examples/README.md) are a great resource to play with to understand how the 57 + library works. 58 + 54 59 If you'd like to write logs to a different path you can configure that by adding this to your root 55 60 source file (typically `src/main.zig` or similar): 56 61 57 62 ```zig 58 - // Relative to current working directory: 59 - pub const log_to_file_path = "logs/app.log"; 63 + const ltf = @import("log_to_file"); 64 + 65 + // Logs will be saved to: 66 + // * ./logs/log in Debug mode. 67 + // * ~/.local/logs/log in Release mode. 68 + pub const log_to_file_options: ltf.Options = .{ 69 + .log_file_name = "log", 70 + }; 71 + 72 + // Logs will be saved to: 73 + // * ./new-logs/out.log in Debug mode. 74 + // * ./new-logs/out.log in Release mode. 75 + pub const log_to_file_options: ltf.Options = .{ 76 + .storage_path = "new-logs", 77 + }; 78 + 79 + // Logs will be saved to: 80 + // * ./app-logs/out.log in Debug mode. 81 + // * ./app-logs/out.log in Release mode. 82 + pub const log_to_file_options: ltf.Options = .{ 83 + .log_file_name = "app.log", 84 + .storage_path = "app-logs", 85 + }; 60 86 61 - // Absolute path: 62 - pub const log_to_file_path = "/var/logs/app.log"; 87 + // Logs will be saved to: 88 + // * /var/logs/app.log in Debug mode. 89 + // * /var/logs/app.log in Release mode. 90 + pub const log_to_file_options: ltf.Options = .{ 91 + .log_file_name = "app.log", 92 + .storage_path = "/var/logs", 93 + }; 63 94 ```
+37 -4
build.zig
··· 4 4 const std = @import("std"); 5 5 const builtin = @import("builtin"); 6 6 7 - const minimum_zig_version = std.SemanticVersion.parse("0.13.0") catch unreachable; 7 + const minimum_zig_version = std.SemanticVersion.parse("0.14.0") catch unreachable; 8 + 9 + const Example = enum { 10 + defaults, 11 + custom_log_file, 12 + custom_storage_path, 13 + custom_storage_path_and_log_file, 14 + }; 8 15 9 16 pub fn build(b: *std.Build) void { 10 17 if (comptime (builtin.zig_version.order(minimum_zig_version) == .lt)) { ··· 22 29 const target = b.standardTargetOptions(.{}); 23 30 const optimize = b.standardOptimizeOption(.{}); 24 31 25 - const strip = b.option(bool, "strip", "Omit debug information"); 32 + // const strip = b.option(bool, "strip", "Omit debug information") orelse 33 + // if (optimize == .Debug) false else true; 26 34 27 - _ = b.addModule("log_to_file", .{ 35 + const ltf = b.addModule("log_to_file", .{ 28 36 .root_source_file = b.path("src/log_to_file.zig"), 29 37 .target = target, 30 38 .optimize = optimize, 31 - .strip = strip, 39 + // .strip = strip, 32 40 }); 41 + 42 + const maybe_example = b.option(Example, "example", "Run an example"); 43 + if (maybe_example) |example| { 44 + const source_file = switch (example) { 45 + .defaults => b.path("examples/defaults.zig"), 46 + .custom_log_file => b.path("examples/custom_log_file.zig"), 47 + .custom_storage_path => b.path("examples/custom_storage_path.zig"), 48 + .custom_storage_path_and_log_file => b.path("examples/custom_storage_path_and_log_file.zig"), 49 + }; 50 + 51 + const exe = b.addExecutable(.{ 52 + .name = "example", 53 + .root_source_file = source_file, 54 + .target = target, 55 + .optimize = optimize, 56 + }); 57 + 58 + exe.root_module.addImport("log_to_file", ltf); 59 + 60 + b.installArtifact(exe); 61 + 62 + const run_exe = b.addRunArtifact(exe); 63 + const run_step = b.step("run", "Run an example"); 64 + run_step.dependOn(&run_exe.step); 65 + } 33 66 }
+6 -27
build.zig.zon
··· 1 - // Copyright 2024 - 2024, Kristófer Reykjalín and the log_to_file contributors. 1 + // Copyright 2024 - 2025, Kristófer Reykjalín and the log_to_file contributors. 2 2 // SPDX-License-Identifier: BSD-3-Clause 3 3 4 4 .{ 5 - // This is the default name used by packages depending on this one. For 6 - // example, when a user runs `zig fetch --save <url>`, this field is used 7 - // as the key in the `dependencies` table. Although the user can choose a 8 - // different name, most users will stick with this provided value. 9 - // 10 - // It is redundant to include "zig" in this name because it is already 11 - // within the Zig package namespace. 12 - .name = "log_to_file", 13 - 14 - // This is a [Semantic Version](https://semver.org/). 15 - // In a future version of Zig it will be used for package deduplication. 16 - .version = "1.0.0", 17 - 18 - // This field is optional. 19 - // This is currently advisory only; Zig does not yet do anything 20 - // with this value. 21 - .minimum_zig_version = "0.13.0", 22 - 23 - // Specifies the set of files and directories that are included in this package. 24 - // Only files and directories listed here are included in the `hash` that 25 - // is computed for this package. Only files listed here will remain on disk 26 - // when using the zig package manager. As a rule of thumb, one should list 27 - // files required for compilation plus any license(s). 28 - // Paths are relative to the build root. Use the empty string (`""`) to refer to 29 - // the build root itself. 30 - // A directory listed here means that all files within, recursively, are included. 5 + .name = .log_to_file, 6 + .fingerprint = 0x53bc1f48033cc1bd, 7 + .version = "2.0.0", 8 + .minimum_zig_version = "0.14.0", 31 9 .paths = .{ 32 10 "build.zig", 33 11 "build.zig.zon", 34 12 "src", 13 + "CHANGELOG.md", 35 14 "LICENSE", 36 15 "README.md", 37 16 },
+19
examples/README.md
··· 1 + # Examples 2 + 3 + You can run the examples by running `zig build -Dexample=<example> run` where `<example>` is one 4 + of: 5 + 6 + 1. `defaults` - demonstrates no-config behavior. 7 + 2. `custom_log_file` - demonstrates custom log file name behavior. 8 + 3. `custom_storage_path` - demonstrates custom storage path behavior. 9 + 4. `custom_storage_path_and_log_file` - demonstrates custom storage path and log file name 10 + behavior. 11 + 12 + ## Running examples in release mode 13 + 14 + The command above will run the examples in `Debug` mode. You can also run them in `Release` mode 15 + with `zig build -Doptimize=<example> -Doptimize=ReleaseFast run`. 16 + 17 + > [!IMPORTANT] 18 + > When in release mode the `defaults` and `custom_log_file` examples will store logs in 19 + > `~/.local/logs/out.log` and `~/.local/logs/custom-file.log` respectively.
+17
examples/custom_log_file.zig
··· 1 + const std = @import("std"); 2 + const ltf = @import("log_to_file"); 3 + 4 + pub const std_options: std.Options = .{ 5 + .logFn = ltf.log_to_file, 6 + }; 7 + 8 + pub const log_to_file_options: ltf.Options = .{ 9 + .log_file_name = "custom-file.log", 10 + }; 11 + 12 + pub fn main() void { 13 + std.log.debug("hello world!", .{}); 14 + std.log.info("hello world!", .{}); 15 + std.log.warn("hello world!", .{}); 16 + std.log.err("hello world!", .{}); 17 + }
+15
examples/custom_storage_path.zig
··· 1 + const std = @import("std"); 2 + const ltf = @import("log_to_file"); 3 + 4 + pub const std_options: std.Options = .{ 5 + .logFn = ltf.log_to_file, 6 + }; 7 + 8 + pub const log_to_file_options: ltf.Options = .{ .storage_path = "custom-logs" }; 9 + 10 + pub fn main() void { 11 + std.log.debug("hello world!", .{}); 12 + std.log.info("hello world!", .{}); 13 + std.log.warn("hello world!", .{}); 14 + std.log.err("hello world!", .{}); 15 + }
+18
examples/custom_storage_path_and_log_file.zig
··· 1 + const std = @import("std"); 2 + const ltf = @import("log_to_file"); 3 + 4 + pub const std_options: std.Options = .{ 5 + .logFn = ltf.log_to_file, 6 + }; 7 + 8 + pub const log_to_file_options: ltf.Options = .{ 9 + .log_file_name = "custom-log.txt", 10 + .storage_path = "custom-logs", 11 + }; 12 + 13 + pub fn main() void { 14 + std.log.debug("hello world!", .{}); 15 + std.log.info("hello world!", .{}); 16 + std.log.warn("hello world!", .{}); 17 + std.log.err("hello world!", .{}); 18 + }
+13
examples/defaults.zig
··· 1 + const std = @import("std"); 2 + const ltf = @import("log_to_file"); 3 + 4 + pub const std_options: std.Options = .{ 5 + .logFn = ltf.log_to_file, 6 + }; 7 + 8 + pub fn main() void { 9 + std.log.debug("hello world!", .{}); 10 + std.log.info("hello world!", .{}); 11 + std.log.warn("hello world!", .{}); 12 + std.log.err("hello world!", .{}); 13 + }
+68 -24
src/log_to_file.zig
··· 1 - // Copyright 2024 - 2024, Kristófer Reykjalín and the log_to_file contributors. 1 + // Copyright 2024 - 2025, Kristófer Reykjalín and the log_to_file contributors. 2 2 // SPDX-License-Identifier: BSD-3-Clause 3 3 4 4 const std = @import("std"); 5 + const builtin = @import("builtin"); 5 6 const root = @import("root"); 6 7 7 - const log_file_path: []const u8 = if (@hasDecl(root, "log_to_file_path")) 8 - root.log_to_file_path 8 + pub const Options = struct { 9 + log_file_name: ?[]const u8 = null, 10 + storage_path: ?[]const u8 = null, 11 + }; 12 + 13 + const PrivateOptions = struct { 14 + log_file_name: []const u8, 15 + storage_path: ?[]const u8 = null, 16 + }; 17 + 18 + var options: PrivateOptions = if (@hasDecl(root, "log_to_file_options")) 19 + .{ 20 + .log_file_name = root.log_to_file_options.log_file_name orelse "out.log", 21 + .storage_path = if (root.log_to_file_options.storage_path) |p| 22 + p 23 + else if (builtin.mode == .Debug) 24 + "logs" 25 + else 26 + null, 27 + } 9 28 else 10 - "log"; 29 + .{ 30 + .log_file_name = "out.log", 31 + .storage_path = if (builtin.mode == .Debug) 32 + "logs" 33 + else 34 + null, 35 + }; 36 + 37 + var buffer_for_allocator: [std.fs.max_path_bytes * 10]u8 = undefined; 38 + var fb_allocator = std.heap.FixedBufferAllocator.init(&buffer_for_allocator); 39 + var allocator: std.heap.ThreadSafeAllocator = .{ 40 + .child_allocator = fb_allocator.allocator(), 41 + }; 42 + const fba = allocator.allocator(); 43 + 44 + var write_to_log_mutex: std.Thread.Mutex = .{}; 45 + 46 + fn maybeInitStoragePath() void { 47 + if (options.storage_path != null) return; 48 + 49 + const home = std.process.getEnvVarOwned(fba, "HOME") catch { 50 + return; 51 + }; 52 + 53 + options.storage_path = std.fs.path.join(fba, &.{ 54 + home, 55 + ".local", 56 + "logs", 57 + }) catch { 58 + options.storage_path = "logs"; 59 + return; 60 + }; 61 + } 11 62 12 63 pub fn log_to_file( 13 64 comptime message_level: std.log.Level, ··· 15 66 comptime format: []const u8, 16 67 args: anytype, 17 68 ) void { 69 + maybeInitStoragePath(); 70 + if (options.storage_path == null) return; 71 + 18 72 // Get level text and log prefix. 19 - // See https://ziglang.org/documentation/master/std/#std.log.defaultLog. 73 + // See https://ziglang.org/documentation/0.14.0/std/#std.log.defaultLog. 20 74 const level_txt = comptime message_level.asText(); 21 75 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; 22 76 23 - // Get an allocator to use for getting path to the log file. 24 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 25 - defer { 26 - const deinit_status = gpa.deinit(); 27 - //fail test; can't try in defer as defer is executed after we return 28 - if (deinit_status == .leak) { 29 - std.log.err("memory leak in custom logger", .{}); 30 - } 31 - } 32 - 77 + // Get a handle for the log file. 33 78 const cwd = std.fs.cwd(); 34 - 35 - const log_dir_path = std.fs.path.dirname(log_file_path); 36 - const log_file_basename = std.fs.path.basename(log_file_path); 37 - 38 - const log_dir = if (log_dir_path) |dir| cwd.makeOpenPath(dir, .{}) catch return else cwd; 39 - 79 + const log_dir = cwd.makeOpenPath(options.storage_path.?, .{}) catch return; 40 80 const log = log_dir.createFile( 41 - log_file_basename, 81 + options.log_file_name, 42 82 .{ .truncate = false }, 43 83 ) catch return; 44 84 ··· 46 86 log.seekFromEnd(0) catch return; 47 87 48 88 // Get a writer. 49 - // See https://ziglang.org/documentation/master/std/#std.log.defaultLog. 89 + // See https://ziglang.org/documentation/0.14.0/std/#std.log.defaultLog. 50 90 const log_writer = log.writer(); 51 91 var bw = std.io.bufferedWriter(log_writer); 52 92 const writer = bw.writer(); 53 93 94 + // We could be logging from different threads so we use a mutex here. 95 + write_to_log_mutex.lock(); 96 + defer write_to_log_mutex.unlock(); 97 + 54 98 // Write to the log file. 55 - // See https://ziglang.org/documentation/master/std/#std.log.defaultLog. 99 + // See https://ziglang.org/documentation/0.14.0/std/#std.log.defaultLog. 56 100 nosuspend { 57 101 writer.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return; 58 102 bw.flush() catch return;