Skip to content

[wip]feat(log): implement structured logging module#312

Draft
GrapeBaBa wants to merge 2 commits into
mainfrom
gr/feature/logging
Draft

[wip]feat(log): implement structured logging module#312
GrapeBaBa wants to merge 2 commits into
mainfrom
gr/feature/logging

Conversation

@GrapeBaBa

@GrapeBaBa GrapeBaBa commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a full-featured structured logging module for lodestar-z

Key features

  • Comptime static dispatch pipeline: Filter → Diagnostic → Append, all resolved at compile time with zero runtime overhead for disabled log levels
  • Three layout formats: TextLayout (human-readable with ANSI color), JsonLayout, LogfmtLayout
  • Multiple appender types: WriterAppend (sync), AsyncAppend (lock-free ring buffer + background drain), RollingFileWriter (size/date rotation), OpenTelemetryAppend (OTLP JSON)
  • Composable filters: LevelFilter, ScopeFilter, EnvFilter (parses RUST_LOG-style directives like warn,fork_choice=debug)
  • Diagnostics: StaticDiagnostic (pre-bound attrs), ThreadLocalDiagnostic (per-thread context)
  • std.log bridge: Drop-in std_options so existing std.log.scoped() calls route through the pipeline
  • Global dispatcher builders: initConsoleDispatcher, initFileDispatcher, initCombinedDispatcher for common setups
  • Microsecond timestamps: ISO 8601 UTC format (2024-08-11T22:44:57.172105Z)
  • stderr by default: Following Unix convention (stdout = data, stderr = diagnostics)
  • Extracted ring_buffer.zig: Generic lock-free SPSC ring buffer, reusable outside logging

Architecture

Logger(N) → AnyDispatcher → Dispatcher(Dispatches)
                               ├─ Dispatch(Filters, Diagnostics, Appends)
                               │    ├─ LevelFilter / ScopeFilter / EnvFilter
                               │    ├─ StaticDiagnostic / ThreadLocalDiagnostic
                               │    └─ WriterAppend(Layout) / AsyncAppend(Layout)
                               └─ Dispatch(...)

Module structure

Consolidated from 20+ separate files into 6 focused modules:

  • record.zig — Scope, Level, Attr, Record, comptime interface checks
  • filter.zig — LevelFilter, ScopeFilter, EnvFilter
  • diagnostic.zig — StaticDiagnostic, ThreadLocalDiagnostic
  • layout.zig — TextLayout, JsonLayout, LogfmtLayout, ISO 8601 formatting
  • append.zig — WriterAppend, AsyncAppend, RollingFileWriter, OpenTelemetryAppend
  • dispatch.zig — Dispatch, Dispatcher, AnyDispatcher, Logger

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a robust, structured logging system designed for high-performance and flexibility. By leveraging Zig's comptime capabilities, the system provides a static dispatch pipeline that minimizes runtime overhead while supporting complex logging requirements such as asynchronous file rotation, structured JSON/Logfmt output, and OpenTelemetry integration. The changes include a complete infrastructure for global dispatchers and a bridge to ensure seamless integration with existing std.log usage.

Highlights

  • Structured Logging Module: Introduced a new structured logging module in src/log/ featuring a comptime static dispatch pipeline (Filter → Diagnostic → Append).
  • Flexible Dispatch and Layouts: Implemented multiple layouts (Text, JSON, Logfmt) and appenders (sync, async SPSC ring buffer, rolling file, OpenTelemetry) for versatile logging configurations.
  • Global Dispatcher Infrastructure: Added global dispatcher infrastructure with async builders for console, file, and combined outputs, including a bridge for std.log migration.
  • Performance and Reliability: Utilized a high-performance SPSC ring buffer for async logging with Small Buffer Optimization (SBO) to minimize allocations.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@GrapeBaBa GrapeBaBa changed the title feat(log): structured logging with comptime dispatch, aligned with logforth feat(log): implement structured logging module Apr 12, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a structured logging library for the project, featuring a robust pipeline architecture with filters, diagnostic enrichment, and multiple appender types (console, file, and async). The implementation includes support for structured layouts (Text, JSON, Logfmt) and ensures performance through comptime-generic dispatch and small buffer optimization (SBO) for ring buffer messages. My review identified several instances where error handling was insufficient, specifically regarding silent error suppression in formatting and file operations, which violates the project's requirement that all errors must be handled.

Comment thread src/log/append.zig
Comment on lines +589 to +621
fn formatOtlp(record: *const Record, writer: anytype) void {
writer.writeAll("{") catch return;

writer.print("\"timeUnixNano\":{d}", .{@as(i128, record.timestamp_us) * 1_000}) catch return;

const sev = severityNumber(record.level);
writer.print(",\"severityNumber\":{d}", .{sev}) catch return;
writer.writeAll(",\"severityText\":\"") catch return;
writer.writeAll(severityText(record.level)) catch return;
writer.writeAll("\"") catch return;

writer.writeAll(",\"body\":{\"stringValue\":\"") catch return;
writeOtlpJsonEscaped(writer, record.message);
writer.writeAll("\"}") catch return;

writer.writeAll(",\"attributes\":[") catch return;

writer.writeAll("{\"key\":\"scope\",\"value\":{\"stringValue\":\"") catch return;
writer.writeAll(record.scope_name) catch return;
writer.writeAll("\"}}") catch return;

var iter = record.attrIterator();
while (iter.next()) |attr| {
writer.writeAll(",{\"key\":\"") catch return;
writer.writeAll(attr.key) catch return;
writer.writeAll("\",\"value\":") catch return;
writeOtlpValue(writer, attr.value);
writer.writeAll("}") catch return;
}

writer.writeAll("]") catch return;
writer.writeAll("}\n") catch return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Throughout formatOtlp, write operations use catch return, which silently ignores errors. This can lead to incomplete or malformed OTLP JSON log entries being sent to the collector, which might reject them or parse them incorrectly. This violates the style guide's rule 160: "All errors must be handled."

Consider changing formatOtlp to return an error (!void) and using try for all write operations. The error can then be handled in the append function, for example by calling reportAppendError.

References
  1. Rule 160 states that all errors must be handled. The current code uses catch return which silently ignores potential I/O errors during formatting, leading to potentially corrupt data. (link)

Comment thread src/log/layout.zig
Comment on lines +61 to +93
pub fn format(self: *const TextLayout, record: *const Record, writer: anytype) void {
formatTimestamp(writer, record.timestamp_us);
writer.writeByte(' ') catch return;

if (self.color) {
writer.writeAll(levelColor(record.level)) catch return;
writer.writeAll(rec.asText(record.level)) catch return;
writer.writeAll(ansi_reset) catch return;
} else {
writer.writeAll(rec.asText(record.level)) catch return;
}
writer.writeByte(' ') catch return;

if (!std.mem.eql(u8, record.scope_name, "default")) {
writer.writeByte('(') catch return;
writer.writeAll(record.scope_name) catch return;
writer.writeAll("): ") catch return;
} else {
writer.writeAll(": ") catch return;
}

writer.writeAll(record.message) catch return;

var iter = record.attrIterator();
while (iter.next()) |attr| {
writer.writeByte(' ') catch return;
writer.writeAll(attr.key) catch return;
writer.writeByte('=') catch return;
attr.formatValue(writer);
}

writer.writeByte('\n') catch return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Throughout the layout formatters (TextLayout, JsonLayout, LogfmtLayout), write operations use catch return. This silently ignores I/O errors and can result in partially written, corrupted log lines. This violates the style guide's rule 160: "All errors must be handled."

A better approach would be for format functions to return an error (!void) and use try for all write operations. The caller (the appender) could then handle the error appropriately, for instance by calling reportAppendError. This ensures that formatting failures are not silent.

References
  1. Rule 160 states that all errors must be handled. The current code uses catch return which silently ignores potential I/O errors during formatting, leading to potentially corrupt log lines. (link)

Comment thread examples/log_smoke.zig
Comment on lines +149 to +150
std.fs.cwd().deleteFile("smoke.log") catch {};
std.fs.cwd().deleteFile("smoke_combined.log") catch {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The errors from deleteFile are being ignored with catch {}. While this is an example file, the style guide requires all errors to be handled (Rule 160). Silently ignoring file system errors can hide underlying problems. It would be better to at least print the error to stderr.

    std.fs.cwd().deleteFile("smoke.log") catch |err| std.debug.print("Failed to delete smoke.log: {s}\n", .{@errorName(err)});
    std.fs.cwd().deleteFile("smoke_combined.log") catch |err| std.debug.print("Failed to delete smoke_combined.log: {s}\n", .{@errorName(err)});

References
  1. Rule 160 states that all errors must be handled. The current code uses catch {} which silently ignores potential file system errors during cleanup. (link)

Comment thread src/log/append.zig
const written = fbs.getWritten();

if (written.len < format_buf_size) {
self.rolling_writer.writer().writeAll(written) catch return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Errors from rolling_writer.writer().writeAll are being silently ignored with catch return. This violates the style guide's rule 160: "All errors must be handled." These errors should be reported to stderr using the reportAppendError utility function, similar to how other appenders handle write errors.

                self.rolling_writer.writer().writeAll(written) catch |err| reportAppendError(err, "FileAppend.writeAll");

References
  1. Rule 160 states that all errors must be handled. The current code uses catch return which silently ignores potential I/O errors. (link)

Comment thread src/log/append.zig
var list = std.ArrayList(u8).init(self.allocator);
defer list.deinit();
self.layout.format(record, list.writer());
self.rolling_writer.writer().writeAll(list.items) catch return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Errors from rolling_writer.writer().writeAll are being silently ignored with catch return. This violates the style guide's rule 160: "All errors must be handled." These errors should be reported to stderr using the reportAppendError utility function, similar to how other appenders handle write errors.

            self.rolling_writer.writer().writeAll(list.items) catch |err| reportAppendError(err, "FileAppend.writeAll");

References
  1. Rule 160 states that all errors must be handled. The current code uses catch return which silently ignores potential I/O errors. (link)

Comment thread src/log/filter.zig
}

pub fn addOverride(self: *ScopeFilter, scope_name: []const u8, min_level: std.log.Level) void {
self.overrides.append(.{ .scope_name = scope_name, .min_level = min_level }) catch {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error from self.overrides.append is silently ignored with catch {}. If the number of overrides exceeds max_scopes (32), append will fail and the override will not be added. This failure is silent, which can be very confusing for a user who expects their configuration to be applied. According to the style guide (Rule 160), all errors must be handled. It would be better to log a warning if the limit is reached.

        self.overrides.append(.{ .scope_name = scope_name, .min_level = min_level }) catch |_| {
            std.debug.print("log.ScopeFilter: could not add override for '{s}', max_scopes ({d}) reached.\n", .{
                scope_name, max_scopes,
            });
        };

References
  1. Rule 160 states that all errors must be handled. The current code uses catch {} which silently ignores the error when the maximum number of scope overrides is exceeded. (link)

Comment thread src/log/record.zig
Comment on lines +38 to +51
pub fn parseLevel(text: []const u8) ?Level {
var buf: [8]u8 = undefined;
if (text.len == 0 or text.len >= buf.len) return null;
for (text, 0..) |c, i| buf[i] = std.ascii.toLower(c);
const lower = buf[0..text.len];

if (std.mem.eql(u8, lower, "err")) return .err;
if (std.mem.eql(u8, lower, "error")) return .err;
if (std.mem.eql(u8, lower, "warn")) return .warn;
if (std.mem.eql(u8, lower, "warning")) return .warn;
if (std.mem.eql(u8, lower, "info")) return .info;
if (std.mem.eql(u8, lower, "debug")) return .debug;
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fixed-size buffer buf: [8]u8 in parseLevel is brittle. While it's large enough for the current standard log levels, it makes the function less robust to future changes, such as the introduction of custom or longer log level names. A more robust approach would be to use std.ascii.eqlIgnoreCase to perform the comparison without creating a lowercased copy.

pub fn parseLevel(text: []const u8) ?Level {
    if (std.ascii.eqlIgnoreCase(text, "err")) return .err;
    if (std.ascii.eqlIgnoreCase(text, "error")) return .err;
    if (std.ascii.eqlIgnoreCase(text, "warn")) return .warn;
    if (std.ascii.eqlIgnoreCase(text, "warning")) return .warn;
    if (std.ascii.eqlIgnoreCase(text, "info")) return .info;
    if (std.ascii.eqlIgnoreCase(text, "debug")) return .debug;
    return null;
}

@GrapeBaBa GrapeBaBa force-pushed the gr/feature/logging branch from 2f474a2 to 416b6fa Compare April 12, 2026 03:07
@GrapeBaBa GrapeBaBa force-pushed the gr/feature/logging branch from 416b6fa to 266f087 Compare April 12, 2026 03:08
Replace comptime-generic Dispatch/Dispatcher with runtime-configurable
structs using BoundedArray, matching logforth's caller-owned pipeline
design.

- Add AnyFilter, AnyDiagnostic, AnyAppend type-erased interfaces
- Add .any() to all concrete filter/diagnostic/appender types
- Rewrite Dispatch and Dispatcher as concrete runtime structs
- Remove DefaultSync dev default; global dispatcher starts as noop
- Callers must explicitly build and set their pipeline
- Simplify LevelFilter/parseLevel/FilterResult tests to table-driven
@GrapeBaBa GrapeBaBa changed the title feat(log): implement structured logging module [wip]feat(log): implement structured logging module May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant