Compare commits
16 Commits
4e9fddc593
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b3c8a3ab1c | |||
| 29c6e48d86 | |||
| 40ced30a57 | |||
| 57631f1905 | |||
|
b868bb1300
|
|||
|
4147c47f7e
|
|||
|
e813a3e195
|
|||
|
6f62c61897
|
|||
|
f065c08e91
|
|||
|
f43034cea9
|
|||
|
69da9265b8
|
|||
|
bd33f9c8f9
|
|||
|
a211aafed6
|
|||
|
411a6dc358
|
|||
|
cd99e5b4d3
|
|||
|
ab0898f9c4
|
@@ -17,7 +17,13 @@ jobs:
|
||||
- name: Setup zig installation
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
version: latest
|
||||
- name: Lint check
|
||||
run: zig fmt --check .
|
||||
- name: Spell checking
|
||||
uses: crate-ci/typos@v1.39.0
|
||||
with:
|
||||
config: ./.typos-config
|
||||
- name: Run tests
|
||||
run: zig build --release=fast
|
||||
- name: Release build artifacts
|
||||
|
||||
@@ -16,11 +16,11 @@ jobs:
|
||||
- name: Setup zig installation
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
version: latest
|
||||
- name: Lint check
|
||||
run: zig fmt --check .
|
||||
- name: Spell checking
|
||||
uses: crate-ci/typos@v1.25.0
|
||||
uses: crate-ci/typos@v1.39.0
|
||||
with:
|
||||
config: ./.typos-config
|
||||
- name: Run tests
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
log
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
[files]
|
||||
extend-exclude = []
|
||||
|
||||
[default.extend-words]
|
||||
WRONLY = "WRONLY"
|
||||
|
||||
92
README.md
92
README.md
@@ -2,8 +2,25 @@
|
||||
|
||||
Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pretty print function for easy overwriting of user defined types.
|
||||
|
||||
> [!CAUTION]
|
||||
> Only builds using the zig master version are tested to work.
|
||||
## Install
|
||||
|
||||
Add or update this library as a dependency in your zig project run the following command:
|
||||
|
||||
```sh
|
||||
zig fetch --save git+https://gitea.yves-biener.de/yves-biener/zlog
|
||||
```
|
||||
|
||||
Afterwards add the library as a dependency to any module in your *build.zig*:
|
||||
|
||||
```zig
|
||||
const zlog_dependency = b.dependency("zlog", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.timestamp = true, // default (only required if non-default value shall be used)
|
||||
.stderr = true, // default (only required if non-default value shall be used)
|
||||
.file = "", // default (only required if non-default value shall be used)
|
||||
});
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -58,6 +75,8 @@ const Struct = struct {
|
||||
};
|
||||
|
||||
pub fn main() void {
|
||||
// without explicit scope (i.e. `.default` scope)
|
||||
std.log.info("Without explicit scope or `.default` scope", .{});
|
||||
// initialize zlog with the scope of `main`
|
||||
const log = std.log.scoped(.main);
|
||||
// NOTE the scope of `default` will result in no scoping being printed in
|
||||
@@ -75,7 +94,7 @@ pub fn main() void {
|
||||
const void_tagged_union: TaggedUnion = .nothing;
|
||||
const uniun: Union = .{ .boolean = true };
|
||||
|
||||
// NOTE: Depending on the optimization target some of these log messages
|
||||
// NOTE Depending on the optimization target some of these log messages
|
||||
// will not show, which is inline with the behavior of `std.log`.
|
||||
log.debug("Debug message {any}", .{int_var});
|
||||
log.info("Info message {any}", .{array_var});
|
||||
@@ -87,6 +106,7 @@ pub fn main() void {
|
||||
log.err("Error message {any}", .{tagged_union});
|
||||
}
|
||||
|
||||
// apply *zlog* logging options to `std` logger
|
||||
pub const std_options = zlog.std_options;
|
||||
|
||||
const std = @import("std");
|
||||
@@ -96,62 +116,30 @@ const zlog = @import("zlog");
|
||||
This will result in the following output:
|
||||
|
||||
```
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;12mdebug[0m]([2mmain[0m): Debug message 42
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;10minfo[0m]([2mmain[0m): Info message { 1, 2, 3, 4 }
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;10minfo[0m]([2mmain[0m): Info message "This is a test"
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.Options = enum {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
} = .a
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
} = .{ .nothing = void }
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.Union = union {
|
||||
int: i32,
|
||||
boolean: bool,
|
||||
} = @7fffd1c71048
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;9merror[0m]([2mmain[0m): Error message main.Struct = struct {
|
||||
.a = 42,
|
||||
.b = true,
|
||||
.c = []u16: { 1, 2, 3, 4, 5 },
|
||||
.d = "string",
|
||||
.e = main.Options = enum {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
} = .b,
|
||||
.f = main.TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
} = .{ .one = -5 },
|
||||
}
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;9merror[0m]([2mmain[0m): Error message main.TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
} = .{ .three = "Three" }
|
||||
2025/11/02 11:57:29 [info] Without explicit scope or `.default` scope
|
||||
2025/11/02 11:57:29 [debug](main) Debug message 42
|
||||
2025/11/02 11:57:29 [info](main) Info message { 1, 2, 3, 4 }
|
||||
2025/11/02 11:57:29 [info](main) Info message "This is a test"
|
||||
2025/11/02 11:57:29 [warning](main) Warning message .a
|
||||
2025/11/02 11:57:29 [warning](main) Warning message .{ .nothing = void }
|
||||
2025/11/02 11:57:29 [warning](main) Warning message .{ ... }
|
||||
2025/11/02 11:57:29 [error](main) Error message .{ .a = 42, .b = true, .c = { 1, 2, 3, 4, 5 }, .d = { 115, 116, 114, 105, 110, 103 }, .e = .b, .f = .{ .one = -5 } }
|
||||
2025/11/02 11:57:29 [error](main) Error message .{ .three = { 84, 104, 114, 101, 101 } }
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
For more details about the output customization see the configuration options of the `zlog` module. Following options are available:
|
||||
|
||||
- _timestamp_ (default: `true`): Prepend the current timestamp before each log message.
|
||||
- _stderr_ (default: `true`): Print log messages to stderr.
|
||||
> [!CAUTION]
|
||||
> Currently not working as log output is not appended and only the last log message will be in the resulting log file! This is a not-yet-implemented feature of the standard library of zig! See this [issue](https://github.com/ziglang/zig/issues/14375) for more details.
|
||||
> For now you should instead leave this option as it is and pipe the corresponding stderr outputs to a logfile instead.
|
||||
- *timestamp* (default: `true`): Prepend the current timestamp before each log message. If disabled the date and time (i.e. `2025/11/02 11:57:29 `) are not prepended to each log message.
|
||||
- *stderr* (default: `true`): Print log messages to stderr.
|
||||
- *file* (default: `""`): File path to log (appending; creates if does not exist) messages to. Without a path no log file will be created and logged to. Can be used in parallel with the *stderr* option.
|
||||
|
||||
## Tips
|
||||
---
|
||||
|
||||
The following list shows some tips on how to use logging more effectively. These tips do not apply just to `zlog` (and not even only to zig code).
|
||||
> [!tip]
|
||||
> The following list shows some tips on how to use logging more effectively.
|
||||
> These tips do not apply just to `zlog` (and not even only to zig code).
|
||||
|
||||
- Use `errdefer` to directly print messages on failures in the same function they occur:
|
||||
```zig
|
||||
@@ -164,4 +152,4 @@ The following list shows some tips on how to use logging more effectively. These
|
||||
break :port try fmt.parseInt(u16, buf[0 .. len -| 1], 10);
|
||||
};
|
||||
```
|
||||
- When looking through the output of the log (i.e. written to disk by `program 2> log` when using the _stderr_ build option) the `log` file contains control code characters (*ansi*) for the colored outputs. You can still see them correctly with the following command `less -R log`.
|
||||
- When looking through the output of the log (i.e. written to disk by `program 2> log` when using the *stderr* build option) the `log` file contains control code characters (*ansi*) for the colored outputs. You can still see them correctly with the following command `less -R log`. When using the *file* build option the *ansi* control characters are not omitted when logging to the file.
|
||||
|
||||
@@ -27,11 +27,6 @@ pub fn build(b: *std.Build) void {
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const ztime_dependency = b.dependency("ztime", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const zlog_module = b.addModule("zlog", .{
|
||||
// In this case the main source file is merely a path, however, in more
|
||||
// complicated build scripts, this could be a generated file.
|
||||
@@ -39,7 +34,6 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "ztime", .module = ztime_dependency.module("ztime") },
|
||||
.{ .name = "build_options", .module = options_module },
|
||||
},
|
||||
});
|
||||
@@ -51,7 +45,6 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "ztime", .module = ztime_dependency.module("ztime") },
|
||||
.{ .name = "zlog", .module = zlog_module },
|
||||
},
|
||||
}),
|
||||
@@ -93,7 +86,6 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "ztime", .module = ztime_dependency.module("ztime") },
|
||||
.{ .name = "build_options", .module = options_module },
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
.{
|
||||
.name = .zlog,
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
.version = "0.16.0",
|
||||
.fingerprint = 0x55b82e3347a594e8,
|
||||
// version name should match the zig version except for the last number,
|
||||
// which stands for the version inside a given zig version
|
||||
.version = "0.15.0",
|
||||
.dependencies = .{
|
||||
.ztime = .{
|
||||
.url = "git+https://gitea.yves-biener.de/yves-biener/ztime#6a2f853f36aa55146e089d7c0c3f041012c0f8fe",
|
||||
.hash = "ztime-0.0.0-e3nBJL5xAAC_guuqgVnOZncpvwH76NnKRC7JJ_zTF9rV",
|
||||
},
|
||||
},
|
||||
.minimum_zig_version = "0.15.0",
|
||||
.minimum_zig_version = "0.16.0-dev.463+f624191f9",
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
|
||||
@@ -46,6 +46,8 @@ const Struct = struct {
|
||||
};
|
||||
|
||||
pub fn main() void {
|
||||
// without explicit scope (i.e. `.default` scope)
|
||||
std.log.info("Without explicit scope or `.default` scope", .{});
|
||||
// initialize zlog with the scope of `main`
|
||||
const log = std.log.scoped(.main);
|
||||
// NOTE the scope of `default` will result in no scoping being printed in
|
||||
@@ -63,7 +65,7 @@ pub fn main() void {
|
||||
const void_tagged_union: TaggedUnion = .nothing;
|
||||
const uniun: Union = .{ .boolean = true };
|
||||
|
||||
// NOTE: Depending on the optimization target some of these log messages
|
||||
// NOTE Depending on the optimization target some of these log messages
|
||||
// will not show, which is inline with the behavior of `std.log`.
|
||||
log.debug("Debug message {any}", .{int_var});
|
||||
log.info("Info message {any}", .{array_var});
|
||||
@@ -75,6 +77,7 @@ pub fn main() void {
|
||||
log.err("Error message {any}", .{tagged_union});
|
||||
}
|
||||
|
||||
// apply *zlog* logging options to `std` logger
|
||||
pub const std_options = zlog.std_options;
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
101
src/root.zig
101
src/root.zig
@@ -5,56 +5,86 @@ fn logFn(
|
||||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
// TODO provide build time configuration to allow tweaking corresponding output
|
||||
// - change output file for writing messages to (default `stderr`)
|
||||
// - write into own file for each level?
|
||||
var buf: [128]u8 = undefined;
|
||||
if (comptime build_options.file.len > 0) {
|
||||
const prefix = if (scope == .default) " " else "(" ++ @tagName(scope) ++ ") ";
|
||||
const level_txt = switch (comptime message_level) {
|
||||
.err => "[error]",
|
||||
.warn => "[warning]",
|
||||
.info => "[info]",
|
||||
.debug => "[debug]",
|
||||
};
|
||||
// TODO let user configure the format he wants to use for logging and use a pretty good default one?
|
||||
const complete_format = level_txt ++ prefix ++ format ++ "\n";
|
||||
// TODO use common logging format, such that in integrates well with other logging frameworks
|
||||
// (i.e. golang's logger, log4j, etc.)
|
||||
const fd = std.posix.open(build_options.file, .{
|
||||
.CREAT = true,
|
||||
.APPEND = true,
|
||||
.ACCMODE = .WRONLY,
|
||||
}, 0o600) catch @panic("Could not append to log file");
|
||||
defer std.posix.close(fd);
|
||||
|
||||
const prefix = if (scope == .default) ": " else "(\x1b[2m" ++ @tagName(scope) ++ "\x1b[0m): ";
|
||||
const level_txt = switch (comptime message_level) {
|
||||
.err => "[\x1b[38;5;9merror\x1b[0m]",
|
||||
.warn => "[\x1b[38;5;11mwarning\x1b[0m]",
|
||||
.info => "[\x1b[38;5;10minfo\x1b[0m]",
|
||||
.debug => "[\x1b[38;5;12mdebug\x1b[0m]",
|
||||
};
|
||||
const complete_format = level_txt ++ prefix ++ format ++ "\n";
|
||||
if (comptime build_options.file.len != 0) {
|
||||
// TODO handle errors accordingly (i.e. panic?)
|
||||
// NOTE with zig 0.13.0 there is currently no way to open files to append (except to use libc or talk directly to posix, which this lib should not have to do)
|
||||
const file = std.fs.openFileAbsolute(build_options.file, .{
|
||||
.mode = .read_write,
|
||||
}) catch std.fs.createFileAbsolute(build_options.file, .{
|
||||
.truncate = false,
|
||||
}) catch @panic("Failed to open and/or create configured log file");
|
||||
defer file.close();
|
||||
var file: fs.File = .{ .handle = fd };
|
||||
var buffer = file.writer(&buf);
|
||||
var writer = &buffer.interface;
|
||||
defer writer.flush() catch unreachable;
|
||||
|
||||
var buffer = io.bufferedWriter(file.writer());
|
||||
defer buffer.flush() catch {};
|
||||
|
||||
const writer = buffer.writer();
|
||||
log_writing(writer, fmt, args);
|
||||
log_writing(writer, complete_format, false, args);
|
||||
}
|
||||
|
||||
if (comptime build_options.stderr) {
|
||||
var buffer = io.bufferedWriter(stderr().deprecatedWriter());
|
||||
defer buffer.flush() catch {};
|
||||
const prefix = if (scope == .default) " " else "(\x1b[2m" ++ @tagName(scope) ++ "\x1b[0m) ";
|
||||
const level_txt = switch (comptime message_level) {
|
||||
.err => "[\x1b[38;5;9merror\x1b[0m]",
|
||||
.warn => "[\x1b[38;5;11mwarning\x1b[0m]",
|
||||
.info => "[\x1b[38;5;10minfo\x1b[0m]",
|
||||
.debug => "[\x1b[38;5;12mdebug\x1b[0m]",
|
||||
};
|
||||
const complete_format = level_txt ++ prefix ++ format ++ "\n";
|
||||
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
const writer = buffer.writer();
|
||||
log_writing(writer, complete_format, args);
|
||||
var buffer = fs.File.stderr().writer(&buf);
|
||||
var writer = &buffer.interface;
|
||||
defer writer.flush() catch unreachable;
|
||||
|
||||
log_writing(writer, complete_format, true, args);
|
||||
}
|
||||
}
|
||||
|
||||
inline fn log_writing(writer: anytype, comptime format: []const u8, args: anytype) void {
|
||||
inline fn log_writing(writer: *std.Io.Writer, comptime format: []const u8, comptime ansi: bool, args: anytype) void {
|
||||
nosuspend {
|
||||
if (build_options.timestamp) log_timestamp(writer);
|
||||
if (build_options.timestamp) log_timestamp(writer, ansi);
|
||||
writer.print(format, args) catch return;
|
||||
}
|
||||
}
|
||||
|
||||
inline fn log_timestamp(writer: anytype) void {
|
||||
writer.print("[\x1b[1m{any}\x1b[0m] ", .{ztime.DateTime.now()}) catch return;
|
||||
/// Prepend the current timestamp in the following format:
|
||||
/// `<year>/<month>/<day> <hour>:<minute>:<sec> `
|
||||
///
|
||||
/// NOTE all information are displayed using numbers
|
||||
inline fn log_timestamp(writer: anytype, comptime ansi: bool) void {
|
||||
const time = std.posix.clock_gettime(.REALTIME) catch @panic("Cannot request timestamp");
|
||||
const epoch_secs: std.time.epoch.EpochSeconds = .{ .secs = @intCast(time.sec) };
|
||||
const day_secs = epoch_secs.getDaySeconds();
|
||||
const year_and_day = epoch_secs.getEpochDay().calculateYearDay();
|
||||
const month_and_day = year_and_day.calculateMonthDay();
|
||||
writer.print(
|
||||
if (comptime ansi)
|
||||
"\x1b[1m{d}/{d:0>2}/{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}\x1b[0m "
|
||||
else
|
||||
"{d}/{d:0>2}/{d:0>2} {d:0>2}:{d:0>2}:{d:0>2} ",
|
||||
.{
|
||||
year_and_day.year,
|
||||
month_and_day.month.numeric(),
|
||||
month_and_day.day_index + 1,
|
||||
day_secs.getHoursIntoDay(),
|
||||
day_secs.getMinutesIntoHour(),
|
||||
day_secs.getSecondsIntoMinute(),
|
||||
},
|
||||
) catch return;
|
||||
}
|
||||
|
||||
pub fn pretty_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype) !void {
|
||||
@@ -183,7 +213,7 @@ fn inner_format(object: anytype, comptime format: []const u8, options: fmt.Forma
|
||||
},
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }),
|
||||
},
|
||||
else => try fmt.format(writer, "{any}", .{object}),
|
||||
else => try fmt.format(writer, format, .{object}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +222,7 @@ pub const std_options: std.Options = .{
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const io = std.io;
|
||||
const fs = std.fs;
|
||||
const stderr = fs.File.stderr;
|
||||
const log = std.log;
|
||||
const fmt = std.fmt;
|
||||
const build_options = @import("build_options");
|
||||
const ztime = if (build_options.timestamp) @import("ztime") else null;
|
||||
|
||||
Reference in New Issue
Block a user