Have Zig's
std.log log to a file instead of to stderr
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}