From 9c83bf3ef926baad343435dc5d7ba9c67ae290b5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 1 Sep 2026 11:31:27 +0800 Subject: [PATCH] log: allow 'note' to be silence, log CLI tips We currently have 1 note: it prints the server's listening address:port. Note is a special un-ignorable level. This keeps the "note" level, but logs it under a new scope: "note", so that it _can_ be silenced with a `--log-filter note`. Add a new note, on startup, that displays tips. Currently, only displays when --obey-robots is not enabled: NOTE note : config tips . . . . . . . . . . . . . . . . . . . [+0ms] robots = use '--obey-robots' to use a sites robots.txt meta = use '--log-filter note' to silence this message --- src/log.zig | 43 +++++++++++++++++++++++++++++++------------ src/main.zig | 16 ++++++++++++++++ src/server/Server.zig | 2 +- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/log.zig b/src/log.zig index c3234e180..a513776e4 100644 --- a/src/log.zig +++ b/src/log.zig @@ -34,6 +34,7 @@ pub const Scope = enum { http, js, mcp, + note, not_implemented, scheduler, storage, @@ -178,6 +179,19 @@ pub fn warnDisabledIFrame() void { } pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { + var kvs: [@typeInfo(@TypeOf(data)).@"struct".fields.len]KV = undefined; + initKVs(data, &kvs); + logKVs(scope, level, msg, &kvs); +} + +inline fn initKVs(data: anytype, kvs: []KV) void { + inline for (@typeInfo(@TypeOf(data)).@"struct".fields, 0..) |f, i| { + const value = @field(data, f.name); + kvs[i] = .{ .key = f.name, .value = Value.init(&value) }; + } +} + +pub fn logKVs(scope: Scope, level: Level, msg: []const u8, kvs: []const KV) void { if (enabled(scope, level) == false) { return; } @@ -193,7 +207,7 @@ pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { if (sink) |s| { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); - logTo(scope, level, msg, data, &w) catch |log_err| { + logToErased(scope, level, msg, kvs, &w) catch |log_err| { std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.real), @tagName(scope), @errorName(log_err), msg }); return; }; @@ -205,20 +219,16 @@ pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void { const stderr = std.debug.lockStderr(&buf); defer std.debug.unlockStderr(); - logTo(scope, level, msg, data, &stderr.file_writer.interface) catch |log_err| { - std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.real), @errorName(log_err), @tagName(scope), msg }); + logToErased(scope, level, msg, kvs, &stderr.file_writer.interface) catch |log_err| { + std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.real), @tagName(scope), @errorName(log_err), msg }); }; } -// Converts each field of `data` into a runtime Value so that a single copy of -// the formatting code (logToErased and below) can do the actual writing. +// Like `log`, but to an explicit writer and without the enabled/sink +// gating. Only used by tests. fn logTo(scope: Scope, level: Level, msg: []const u8, data: anytype, out: *std.Io.Writer) !void { - const fields = @typeInfo(@TypeOf(data)).@"struct".fields; - var kvs: [fields.len]KV = undefined; - inline for (fields, 0..) |f, i| { - const value = @field(data, f.name); - kvs[i] = .{ .key = f.name, .value = Value.init(&value) }; - } + var kvs: [@typeInfo(@TypeOf(data)).@"struct".fields.len]KV = undefined; + initKVs(data, &kvs); return logToErased(scope, level, msg, &kvs, out); } @@ -320,9 +330,18 @@ fn logPrettyPrefix(scope: Scope, level: Level, msg: []const u8, writer: *std.Io. } } -const KV = struct { +pub const KV = struct { key: []const u8, value: Value, + + // `vp` is a pointer, as with Value.init. A string literal is already one; + // for anything else pass `&value` and keep it alive until the log call. + pub fn init(key: []const u8, vp: anytype) KV { + return .{ + .key = key, + .value = .init(vp), + }; + } }; const Value = union(enum) { diff --git a/src/main.zig b/src/main.zig index e37ca8074..35a2073ba 100644 --- a/src/main.zig +++ b/src/main.zig @@ -94,6 +94,8 @@ fn run(allocator: Allocator, main_arena: Allocator, proc_args: std.process.Args) app.telemetry.record(.{ .run = {} }); + logConfigTips(app.config); + defer if (app.config.dumpMetricsOnExit()) { var writer = std.Io.File.stdout().writerStreaming(lp.io, &.{}); lp.metrics.write(&writer.interface); @@ -396,3 +398,17 @@ fn mcpThread(allocator: std.mem.Allocator, app: *App, cdp_server: ?*lp.Server, e log.fatal(.mcp, "mcp error", .{ .err = err }); }; } + +fn logConfigTips(config: *const Config) void { + var count: usize = 0; + var tips: [2]log.KV = undefined; + if (config.obeyRobots() == false) { + tips[count] = .init("robots", "use '--obey-robots' to use a sites robots.txt"); + count += 1; + } + + if (count > 0) { + tips[count] = .init("meta", "use '--log-filter note' to silence this message"); + log.logKVs(.note, .note, "config tips", tips[0 .. count + 1]); + } +} diff --git a/src/server/Server.zig b/src/server/Server.zig index ec1ded986..f05486350 100644 --- a/src/server/Server.zig +++ b/src/server/Server.zig @@ -151,7 +151,7 @@ pub fn init(app: *App, address: sys_net.IpAddress) !*Server { const listener = try bindListener(app.config, &bound_address); errdefer _ = std.c.close(listener); pollfds[1] = .{ .fd = listener, .events = posix.POLL.IN, .revents = 0 }; - log.note(.app, "server running", .{ .address = bound_address }); + log.note(.note, "server running", .{ .address = bound_address }); const port = bound_address.getPort(); const json_version_response = try buildJSONVersionResponse(app, port);