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

Configure Feed

Select the types of activity you want to include in your feed.

at main 172 lines 6.3 kB view raw
1//! `log_to_file` gives you a pre-made, easily configurable logging function that you can use 2//! instead of the `std.log.defaultLog` function from the Zig standard library. 3//! 4//! When compiled in debug mode logs will go to `./logs/<executable_name>.log`. When compiled in any 5//! other mode logs will go to `~/.local/state/<executable_name>/<executable_name>.log`. 6//! 7//! You can change where the logs are stored, or specify a different file name for the log file 8//! itself by changing `Options` in your root file. 9//! 10//! Copyright 2024 - 2025, Kristófer Reykjalín and the `log_to_file` contributors. 11//! 12//! SPDX-License-Identifier: BSD-3-Clause 13 14const std = @import("std"); 15const builtin = @import("builtin"); 16const root = @import("root"); 17 18/// Use to configure where log files are stored and what the log file is called. 19pub const Options = struct { 20 /// Set to change what the log file will be called. If unset the file name will be 21 /// `<executable_name>.log`. Falls back to `out.log` when executable name can't be determined. 22 log_file_name: ?[]const u8 = null, 23 /// Set to the directory where the log file will be stored. Supports absolute paths and relative 24 /// paths. If set to a relative path logs will be store relative to where the executable is run 25 /// from **not** where the executable is saved. 26 /// 27 /// If unset the logs will be stored in `./logs/` when compiled in debug mode and 28 /// `~/.local/state/<executable_name>/` in any other mode. 29 /// 30 /// Falls back to `~/.cache/logs/out.log` if executable name can't be determined. 31 storage_path: ?[]const u8 = null, 32}; 33 34var options: Options = .{ 35 .log_file_name = if (@hasDecl(root, "log_to_file_options")) 36 root.log_to_file_options.log_file_name 37 else 38 null, 39 .storage_path = if (@hasDecl(root, "log_to_file_options") and 40 root.log_to_file_options.storage_path != null) 41 root.log_to_file_options.storage_path 42 else if (builtin.mode == .Debug) 43 "logs" 44 else 45 null, 46}; 47 48var buffer_for_allocator: [std.fs.max_path_bytes * 10]u8 = undefined; 49var fb_allocator = std.heap.FixedBufferAllocator.init(&buffer_for_allocator); 50var allocator: std.heap.ThreadSafeAllocator = .{ 51 .child_allocator = fb_allocator.allocator(), 52}; 53const fba = allocator.allocator(); 54 55var write_to_log_mutex: std.Thread.Mutex = .{}; 56 57fn maybeInitLogFileName() void { 58 if (options.log_file_name != null) return; 59 60 var buf: [std.fs.max_path_bytes]u8 = undefined; 61 const exe_path = std.fs.selfExePath(&buf) catch { 62 options.log_file_name = "out.log"; 63 return; 64 }; 65 66 options.log_file_name = std.fmt.allocPrint( 67 fba, 68 "{s}.log", 69 .{std.fs.path.basename(exe_path)}, 70 ) catch { 71 options.log_file_name = "out.log"; 72 return; 73 }; 74} 75 76fn maybeInitStoragePath() void { 77 if (options.storage_path != null) return; 78 79 const home = std.process.getEnvVarOwned(fba, "HOME") catch { 80 options.storage_path = "logs"; 81 return; 82 }; 83 84 var buf: [std.fs.max_path_bytes]u8 = undefined; 85 const exe_path = std.fs.selfExePath(&buf) catch { 86 // Fallback to ephemeral logs in `~/.cache/logs/`. 87 options.storage_path = std.fs.path.join(fba, &.{ 88 home, 89 ".cache", 90 "logs", 91 }) catch { 92 options.storage_path = "logs"; 93 return; 94 }; 95 return; 96 }; 97 const exe_name = std.fs.path.basename(exe_path); 98 99 options.storage_path = std.fs.path.join(fba, &.{ 100 home, 101 ".local", 102 "state", 103 exe_name, 104 }) catch { 105 options.storage_path = "logs"; 106 return; 107 }; 108} 109 110/// Replacement function that sends `std.log.{debug,info,warn,err}` logs to a file instead of to 111/// stderr. Assign to `logFn` in `std.Options` in your root file. 112/// 113/// When compiled in debug mode logs will go to `./logs/<executable_name>.log`. When compiled in any 114/// other mode logs will go to `~/.local/state/<executable_name>/<executable_name>.log`. 115/// 116/// You can change where the logs are stored, or specify a different file name for the log file 117/// itself by changing `Options` in your root file. 118pub fn log_to_file( 119 comptime message_level: std.log.Level, 120 comptime scope: @TypeOf(.enum_literal), 121 comptime format: []const u8, 122 args: anytype, 123) void { 124 maybeInitStoragePath(); 125 if (options.storage_path == null) 126 return std.log.defaultLog(message_level, scope, format, args); 127 128 maybeInitLogFileName(); 129 if (options.log_file_name == null) 130 return std.log.defaultLog(message_level, scope, format, args); 131 132 // Get level text and log prefix. 133 // See https://ziglang.org/documentation/0.14.0/std/#std.log.defaultLog. 134 const level_txt = comptime message_level.asText(); 135 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; 136 137 // We could be logging from different threads so we use a mutex here. 138 write_to_log_mutex.lock(); 139 defer write_to_log_mutex.unlock(); 140 141 // Get a handle for the log file. 142 const cwd = std.fs.cwd(); 143 144 const log_dir = cwd.makeOpenPath(options.storage_path.?, .{}) catch 145 return std.log.defaultLog(message_level, scope, format, args); 146 147 const log = log_dir.createFile( 148 options.log_file_name.?, 149 .{ .truncate = false }, 150 ) catch return std.log.defaultLog(message_level, scope, format, args); 151 152 // Get a writer. 153 // See https://ziglang.org/documentation/0.15.1/std/#std.log.defaultLog. 154 var buffer: [64]u8 = undefined; 155 var log_writer = log.writer(&buffer); 156 157 // Move the write index to the end of the file. 158 const end_pos = log.getEndPos() catch return std.log.defaultLog(message_level, scope, format, args); 159 log_writer.seekTo(end_pos) catch return std.log.defaultLog(message_level, scope, format, args); 160 161 var writer = &log_writer.interface; 162 163 // Write to the log file. 164 // See https://ziglang.org/documentation/0.15.1/std/#std.log.defaultLog. 165 nosuspend { 166 writer.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch 167 return std.log.defaultLog(message_level, scope, format, args); 168 169 writer.flush() catch 170 return std.log.defaultLog(message_level, scope, format, args); 171 } 172}