diff --git a/.gitignore b/.gitignore index 3389c86..2fc31d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .zig-cache/ zig-out/ +log diff --git a/README.md b/README.md index 0250a78..9bcbcab 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,26 @@ Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pr > [!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 dependendy to any module in your *build.zig*: + +```zig +const ztime_dependency = b.dependency("ztime", .{ + .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 The following snippet shows an example usage of `zlog` to change the default log format and add pretty printing to the user defined types: @@ -58,6 +78,8 @@ const Struct = struct { }; pub fn main() void { + // without explict 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 +97,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 +109,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,60 +119,30 @@ const zlog = @import("zlog"); This will result in the following output: ``` -[2025-07-17 19:41:43] [debug](main): Debug message 42 -[2025-07-17 19:41:43] [info](main): Info message { 1, 2, 3, 4 } -[2025-07-17 19:41:43] [info](main): Info message "This is a test" -[2025-07-17 19:41:43] [warning](main): Warning message main.Options = enum { - a, - b, - c, -} = .a -[2025-07-17 19:41:43] [warning](main): Warning message main.TaggedUnion = union(enum) { - one: i16, - two: u32, - three: []const u8, - nothing, -} = .{ .nothing = void } -[2025-07-17 19:41:43] [warning](main): Warning message main.Union = union { - int: i32, - boolean: bool, -} = @7fffd1c71048 -[2025-07-17 19:41:43] [error](main): 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 }, -} -[2025-07-17 19:41:43] [error](main): 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. +- *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 messages to. Without a path no log file will be created and logged to. Can be used in parallel with the *stderr* option +- *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 @@ -162,4 +155,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. diff --git a/build.zig b/build.zig index fa9f088..723b426 100644 --- a/build.zig +++ b/build.zig @@ -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 }, }, }), diff --git a/build.zig.zon b/build.zig.zon index ff8a611..9e6f51b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/src/main.zig b/src/main.zig index c6637cb..71117de 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,6 +46,8 @@ const Struct = struct { }; pub fn main() void { + // without explict 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"); diff --git a/src/root.zig b/src/root.zig index 432612a..0f31be5 100644 --- a/src/root.zig +++ b/src/root.zig @@ -5,18 +5,17 @@ fn logFn( comptime format: []const u8, args: anytype, ) void { - const prefix = if (scope == .default) ": " else "(\x1b[2m" ++ @tagName(scope) ++ "\x1b[0m): "; - // TODO this should only happen for messages that are written to *stderr* in files the escape codes are only annoying - 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]", - }; - // 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"; 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, .{ @@ -28,32 +27,63 @@ fn logFn( var buffer = std.fs.File.Writer.init(.{ .handle = fd }, &buf); var writer = &buffer.interface; - defer writer.flush() catch {}; + defer writer.flush() catch unreachable; - log_writing(writer, complete_format, args); + log_writing(writer, complete_format, false, args); } if (comptime build_options.stderr) { + 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"; + var buffer = stderr().writer(&buf); var writer = &buffer.interface; - defer writer.flush() catch {}; + defer writer.flush() catch unreachable; std.debug.lockStdErr(); defer std.debug.unlockStdErr(); - log_writing(writer, complete_format, args); + log_writing(writer, complete_format, true, args); } } -inline fn log_writing(writer: *std.Io.Writer, 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: +/// `// :: ` +/// +/// 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 { @@ -197,4 +227,3 @@ 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;