Skip to content

chore: slog update, pre-release v6#90

Merged
rustatian merged 4 commits into
masterfrom
pre-release/v6-beta
Mar 20, 2026
Merged

chore: slog update, pre-release v6#90
rustatian merged 4 commits into
masterfrom
pre-release/v6-beta

Conversation

@rustatian
Copy link
Copy Markdown
Member

@rustatian rustatian commented Mar 20, 2026

Reason for This PR

Closes: roadrunner-server/roadrunner#1750
Closes: roadrunner-server/roadrunner#1708

Description of Changes

Migrate the logger plugin from go.uber.org/zap to Go's standard log/slog. This is a breaking change for v6.

What changed

Core migration:

  • BuildLogger() now returns *slog.Logger via BuildResult (which also tracks []io.Closer for proper resource cleanup)
  • NamedLogger() returns *slog.Logger instead of *zap.Logger, with channel logger caching
  • Plugin.Stop() closes file handles opened for log output (fixes fd leak)
  • New RawHandler (message-only slog handler) for raw mode
  • New newJSONHandler for production mode with zap-compatible key mapping (ts/level/msg)
  • Uses slog.DiscardHandler (Go 1.24+) for off/none modes
  • slog.NewTextHandler for development mode

Bug fix (#1708):

  • file_logger_options errors are now surfaced properly instead of being silently swallowed

Dependencies removed:

  • go.uber.org/zap, go.uber.org/multierr
  • github.com/fatih/color, github.com/mattn/go-colorable, github.com/mattn/go-isatty

Files deleted:

  • encoder.go (zap-specific colored encoders — colors removed per design decision)
  • tests/pkgs.txt (replaced by coverpkg=./...)

Other:

  • Module path: logger/v5logger/v6
  • schema.json: removed "panic" from LogLevel enum (only debug/info/warn/error supported)
  • CI: PHP 8.5, coverpkg=./..., codecov sync with amqp plugin approach
  • Tests updated to slog API, wg.Go(), t.Context(), lint fixes

api-plugins (prerequisite, separate repo)

  • logger/interface.go: Named.NamedLogger returns *slog.Logger, Log interface removed
  • go.mod: zap dependency removed
  • Released as v6.0.0-beta.2

License Acceptance

By submitting this pull request, I confirm that my contribution is made under
the terms of the MIT license.

PR Checklist

  • All commits in this PR are signed (git commit -s).
  • The reason for this PR is clearly provided (issue no. or explanation).
  • The description of changes is clear and encompassing.
  • Any required documentation changes (code and docs) are included in this PR.
  • Any user-facing changes are mentioned in CHANGELOG.md.
  • All added/changed functionality is tested.

Note: TestLoggerRawErr and TestFileLogger currently fail because they depend on HTTP/server/RPC v5 plugins that still use the old zap-based Logger interface. They will pass once those plugins are migrated to v6.

Summary by CodeRabbit

  • New Features

    • Switched logging backend to Go's standard log/slog.
    • Added raw message-only mode and a pluggable custom-format logging mode with time formatting and placeholders.
    • Added graceful shutdown for logger resources.
  • Documentation

    • Added package-level docs describing modes and formatting.
  • Bug Fixes

    • Removed obsolete panic log level from public schema.
  • Chores

    • Module bumped to v6; CI, tests, and ignore rules updated.

Signed-off-by: Valery Piashchynski <piashchynski.valery@gmail.com>
@rustatian rustatian self-assigned this Mar 20, 2026
Copilot AI review requested due to automatic review settings March 20, 2026 11:40
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Replaces Uber Zap with Go’s stdlib log/slog across the logger plugin: adds FormatHandler and RawHandler, changes BuildLogger to return a BuildResult (Logger + Closers), implements output writer resolution (stdout/stderr/files), removes Zap encoders/deps, updates schema, tests, and CI; module bumped to v6. (50 words)

Changes

Cohort / File(s) Summary
Core logger implementation
config.go, logger.go, plugin.go
Replaced Zap with slog. Config.BuildLogger() now returns (*BuildResult, error) containing *slog.Logger and []io.Closer. Added output writer resolution (stdout/stderr/file paths), collects closers, and updated plugin lifecycle to store and close resources.
New slog handlers
format_handler.go, handler.go
Added FormatHandler (placeholder-based formatted output with attrs/groups/source resolution) and RawHandler (message-only writer). Both implement slog.Handler, are concurrency-safe, and provide constructors and WithAttrs/WithGroup behavior.
Tests & test helpers
tests/logger_test.go, tests/plugin.go, format_handler_test.go, tests/pkgs.txt, tests/go.mod, tests/configs/.rr-custom-format.yaml
Tests migrated to slog, added FormatHandler tests and a custom-format fixture, adjusted test sync patterns and HTTP request usage, removed tests/pkgs.txt, and bumped test module to v6.
Removed Zap utilities & deps
encoder.go, go.mod
Removed Zap-specific colored encoders and eliminated direct go.uber.org/zap and github.com/fatih/color requirements; module path bumped v5 → v6 and dependency entries simplified.
Schema & docs
schema.json, doc.go
Added package documentation; introduced format and time_format config fields; removed file_logger_options from schema and tightened LogLevel enum (removed panic).
CI & repo files
.github/workflows/linux.yml, .gitignore
CI: bump PHP 8.4→8.5, change Go coverage -coverpkg to ./..., upgrade artifact actions and broaden artifact paths, adjust coverage aggregation/filtering. .gitignore now ignores .claude/settings.local.json.
Composer test fixture
tests/php_test_files/composer.json
Bumped guzzlehttp/guzzle ^6.5→^7.0, replaced guzzle6 adapter with guzzle7 adapter, removed php-http/message-factory.

Sequence Diagram(s)

sequenceDiagram
    participant Plugin
    participant Config
    participant Resolver
    participant FileSystem
    participant HandlerFactory

    Plugin->>Config: BuildLogger()
    Config->>Resolver: resolveOutputWriter(cfg.Output)
    Resolver->>FileSystem: open file(s) or return stdout/stderr writer
    FileSystem-->>Resolver: io.Writer + io.Closer(s)
    Resolver-->>Config: io.MultiWriter + Closers
    Config->>HandlerFactory: choose handler (FormatHandler/Raw/JSON) based on Mode/Format
    HandlerFactory-->>Config: slog.Handler (wrapped writer)
    Config-->>Plugin: BuildResult{Logger:*slog.Logger, Closers:[]io.Closer}
    Plugin->>Plugin: store Logger and Closers
    Note right of Plugin: On Stop -> iterate Closers and Close()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped from Zap to slog today,

Messages find paths, clear as day,
Formats dance, raw lines sing true,
Closers tidy what I do,
V6-bound, I nibble code anew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: slog update, pre-release v6' is specific and clearly relates to the main change: migrating from zap to slog and bumping the major version.
Description check ✅ Passed The PR description comprehensively covers all required template sections: clear reasoning (issue #1750 and #1708), detailed change descriptions, license acceptance, and completed PR checklist items.
Linked Issues check ✅ Passed The PR successfully addresses both linked issues: #1750 (slog migration with new handlers, BuildLogger signature, NamedLogger returns *slog.Logger) and #1708 (file_logger_options errors now surfaced, resource cleanup via Plugin.Stop()).
Out of Scope Changes check ✅ Passed All changes are within scope: slog migration, dependency removal (zap, color), new handler implementations, schema updates, test adaptations, CI/PHP updates, and module path bump to v6 are all aligned with the two linked issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pre-release/v6-beta
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the RoadRunner logger plugin to a v6 module and migrates the implementation and tests from zap to Go’s log/slog, including adding a custom raw-mode handler and adjusting CI coverage collection.

Changes:

  • Switch core logger/plugin interfaces and implementations to log/slog (including a new RawHandler and JSON attribute remapping).
  • Update tests and test module dependencies to target github.com/roadrunner-server/logger/v6.
  • Adjust CI workflow steps and coverage aggregation to reflect the new module version and artifact layout.

Reviewed changes

Copilot reviewed 14 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
plugin.go Moves plugin base logger to *slog.Logger and introduces closer tracking for shutdown.
config.go Replaces zap configuration with slog handlers, output writer resolution, and level parsing.
logger.go Implements named loggers with per-channel overrides and caching for slog loggers.
handler.go Adds slog raw-mode handler and production JSON handler configuration.
doc.go Adds package documentation for the slog-backed logger plugin.
schema.json Updates schema log levels (drops panic).
tests/logger_test.go Updates tests to import/logger v6 and refactors request creation & assertions.
tests/plugin.go Updates test plugin to use slog logger API.
tests/go.mod / tests/go.sum Bumps logger dependency to v6 and updates transitive deps.
tests/pkgs.txt Removes package list previously used for -coverpkg.
.github/workflows/linux.yml Updates CI test/coverage steps and artifact handling for codecov.
.gitignore Adds .claude/settings.local.json to ignored files.
go.mod / go.sum Renames module path to /v6 and removes zap-related dependencies.
encoder.go Removes zap-specific encoders (no longer needed with slog).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread logger.go Outdated
Comment thread config.go Outdated
Comment thread config.go Outdated
Comment thread config.go
Comment thread .github/workflows/linux.yml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
.gitignore (1)

20-20: Consider removing PHP-related ignore pattern.

The **/composer.lock entry is PHP-specific (Composer package manager) and appears unrelated to this Go project. While this doesn't impact functionality, cleaning up unrelated ignore patterns improves .gitignore clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 20, Remove the PHP-specific gitignore entry
'**/composer.lock' from .gitignore; locate the pattern string '**/composer.lock'
and delete that line so the repo's ignore rules only contain Go-relevant
patterns and unrelated PHP/Composer entries are not present.
go.mod (1)

1-11: LGTM - clean module definition for v6.

The module correctly bumps to v6 with minimal dependencies. The removal of go.uber.org/zap and github.com/fatih/color aligns with the slog migration. Go 1.26 is valid; consider updating the toolchain to go1.26.1, the latest stable patch version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go.mod` around lines 1 - 11, Update the Go toolchain version in the module
definition: in the go.mod content for module
github.com/roadrunner-server/logger/v6, change the toolchain directive from
"go1.26.0" to "go1.26.1" so the project uses the latest stable patch release
while keeping the rest of the module declarations (require block and go 1.26)
unchanged.
config.go (1)

213-227: Silent fallback to LevelDebug for invalid levels may mask configuration errors.

The parseLevel function silently defaults to LevelDebug for unrecognized values. While the JSON schema restricts valid values, runtime configuration errors (typos like "debg" or "warn ing") will silently produce debug-level logging instead of surfacing the misconfiguration.

Consider logging a warning or returning an error for invalid level strings to help users identify configuration mistakes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.go` around lines 213 - 227, The parseLevel function currently returns
slog.LevelDebug on any unrecognized string which hides config typos; change
parseLevel(s string) to return (slog.Level, error) (keep the same cases for
"debug","info","warn","warning","error") and when the default branch is hit
return an explicit error that includes the original input (e.g.,
fmt.Errorf("invalid log level: %q", s)); update any callers of parseLevel to
handle the error (log a clear warning or fail fast) so misconfigured values are
surfaced instead of silently becoming LevelDebug.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config.go`:
- Around line 118-141: The call to cfg.resolveOutputWriter() opens writers and
returns w and closers but when cfg.FileLogger != nil the code ignores w (and any
opened closers) causing a resource leak; update the logic in the function
containing resolveOutputWriter and the FileLogger branch so either (A) move the
resolveOutputWriter() call after the cfg.FileLogger nil-check to avoid opening
outputs when using FileLogger, or (B) if you must call resolveOutputWriter()
early, ensure you add w (and any returned closers) to the returned Closers slice
or explicitly close w before returning; specifically modify the branch that
builds the lumberjack.Logger and returns the BuildResult so that w and the
original closers from resolveOutputWriter are not discarded (referencing
cfg.resolveOutputWriter, variable w, variable closers, cfg.FileLogger,
lumberjack.Logger, and BuildResult).

In `@logger.go`:
- Around line 44-46: The code currently panics on configuration error in
NamedLogger (panic(err)); instead handle the error gracefully by either changing
NamedLogger to return (Logger, error) or catch err inside NamedLogger and
fallback to the base logger while logging the configuration failure (e.g.,
processLogger.Error("NamedLogger config invalid", "err", err)). Locate the panic
in NamedLogger, remove panic(err), and implement one of the two fixes: 1)
propagate the error from NamedLogger to callers (update its signature and call
sites) or 2) swallow the error, log it via the base logger and return the base
logger instance as the fallback so the application continues running without
crashing.
- Around line 41-53: The channel logger closers returned by cfg.BuildLogger()
(res.Closers) are currently dropped, causing resource leaks; modify the Log
struct to store channel-specific closers (e.g., a map[string][]io.Closer or
similar) and when creating a channel logger in the function that calls
cfg.BuildLogger() save res.Closers into that map keyed by name before caching
the logger in l.cache; add a Close() method on Log that iterates and closes all
stored closers (and clears the map), and ensure consumers (e.g., Plugin.Stop()
or callers of ServiceLogger()) call Log.Close() on shutdown so file handles and
buffers are flushed and released.

In `@plugin.go`:
- Around line 85-92: Stop only closing p.closers misses closers created by
per-channel loggers from NamedLogger (as discussed in logger.go), causing file
handle leaks for channel-specific file_logger_options; update the logger
creation flow (where NamedLogger and BuildLogger create file writers) to
register their closer(s) into the Plugin's closer collection (p.closers) so Stop
will close them all, and then ensure Stop(p *Plugin).Stop iterates and closes
every registered closer (including those added by NamedLogger), taking care to
use the same synchronization/lock semantics as p.closers manipulation to avoid
races.

In `@tests/logger_test.go`:
- Around line 135-142: The test is flaky because time.Sleep(time.Second) doesn't
guarantee the server is listening; replace that sleep with a retry/poll loop
that attempts http.NewRequestWithContext(t.Context(), http.MethodGet,
"http://127.0.0.1:34999", nil) and http.DefaultClient.Do(req) repeatedly with
short delays and exponential backoff (or a fixed small interval) until a
successful response or a deadline (use t.Context() for cancellation); locate the
startup/wait logic around time.Sleep and swap it for the polling loop so the
test only proceeds when the server actually accepts connections.

---

Nitpick comments:
In @.gitignore:
- Line 20: Remove the PHP-specific gitignore entry '**/composer.lock' from
.gitignore; locate the pattern string '**/composer.lock' and delete that line so
the repo's ignore rules only contain Go-relevant patterns and unrelated
PHP/Composer entries are not present.

In `@config.go`:
- Around line 213-227: The parseLevel function currently returns slog.LevelDebug
on any unrecognized string which hides config typos; change parseLevel(s string)
to return (slog.Level, error) (keep the same cases for
"debug","info","warn","warning","error") and when the default branch is hit
return an explicit error that includes the original input (e.g.,
fmt.Errorf("invalid log level: %q", s)); update any callers of parseLevel to
handle the error (log a clear warning or fail fast) so misconfigured values are
surfaced instead of silently becoming LevelDebug.

In `@go.mod`:
- Around line 1-11: Update the Go toolchain version in the module definition: in
the go.mod content for module github.com/roadrunner-server/logger/v6, change the
toolchain directive from "go1.26.0" to "go1.26.1" so the project uses the latest
stable patch release while keeping the rest of the module declarations (require
block and go 1.26) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48005200-6861-4be9-bd67-c554196ffdb9

📥 Commits

Reviewing files that changed from the base of the PR and between 479599f and 93ed22b.

⛔ Files ignored due to path filters (3)
  • go.sum is excluded by !**/*.sum
  • go.work.sum is excluded by !**/*.sum
  • tests/go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • .github/workflows/linux.yml
  • .gitignore
  • config.go
  • doc.go
  • encoder.go
  • go.mod
  • handler.go
  • logger.go
  • plugin.go
  • schema.json
  • tests/go.mod
  • tests/logger_test.go
  • tests/pkgs.txt
  • tests/plugin.go
💤 Files with no reviewable changes (2)
  • tests/pkgs.txt
  • encoder.go

Comment thread config.go Outdated
Comment thread logger.go Outdated
Comment thread logger.go
Comment thread plugin.go
Comment thread tests/logger_test.go Outdated
Signed-off-by: Valery Piashchynski <piashchynski.valery@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
tests/logger_test.go (1)

131-138: ⚠️ Potential issue | 🟡 Minor

Test flakiness: HTTP server may not be ready after 1-second sleep.

The pipeline failure confirms that time.Sleep(time.Second) is insufficient to guarantee the HTTP server is listening. The test fails with "connection refused" because the server hasn't started accepting connections yet.

Replace the fixed sleep with a retry loop that polls for server readiness.

🔧 Proposed fix: poll for server readiness
 	time.Sleep(time.Second)
-
-	req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://127.0.0.1:34999", nil)
-	assert.NoError(t, err)
-
-	resp, err := http.DefaultClient.Do(req)
-	assert.NoError(t, err)
+
+	var resp *http.Response
+	var lastErr error
+	for i := 0; i < 20; i++ {
+		req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://127.0.0.1:34999", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		resp, lastErr = http.DefaultClient.Do(req)
+		if lastErr == nil {
+			break
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+	assert.NoError(t, lastErr)
 	require.NotNil(t, resp)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/logger_test.go` around lines 131 - 138, Replace the fragile fixed sleep
in tests/logger_test.go (the time.Sleep(time.Second) before calling
http.NewRequestWithContext and http.DefaultClient.Do) with a retry/poll loop
that waits for the test HTTP server to accept connections: repeatedly attempt a
lightweight probe (e.g., net.Dial or an HTTP GET) against the server address
with short intervals until success or a total timeout elapses, then proceed to
create the request with http.NewRequestWithContext and call
http.DefaultClient.Do; this ensures the server is ready and prevents
intermittent "connection refused" failures.
logger.go (2)

25-31: ⚠️ Potential issue | 🟠 Major

Resource leak: channel logger closers are still discarded.

The res.Closers returned by cfg.BuildLogger() on line 26 are not captured or stored. These contain file handles and lumberjack loggers that will never be closed during shutdown, causing file descriptor leaks and unflushed log buffers.

The Log struct needs a mechanism to track channel-specific closers (e.g., a closers []io.Closer field) and expose a Close() method, which should be called during plugin shutdown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@logger.go` around lines 25 - 31, The channel-specific io.Closer values
returned from cfg.BuildLogger() are discarded in the if-block that resolves
channels (when reading l.channels.Channels[name]); capture and store them on the
Log instance so they can be closed at shutdown. Add a closers []io.Closer field
to the Log struct, append res.Closers (from BuildLogger) when constructing the
channel logger (the branch that returns res.Logger.With("logger", name)), and
implement a Close() method on Log that iterates and closes all collected closers
(and any global closers), then ensure plugin shutdown calls Log.Close(). This
preserves res.Logger.With(...) behavior while preventing file
descriptor/log-buffer leaks.

27-29: ⚠️ Potential issue | 🟡 Minor

Panic on logger build error is still present.

Using panic(err) for configuration errors will crash the entire application if a channel has invalid configuration. Consider returning an error or falling back to the base logger with an error log message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@logger.go` around lines 27 - 29, The panic(err) call in the logger build path
should be replaced with graceful error handling: change the builder function
that currently calls panic(err) (the function that constructs the logger and
checks err) to either return the error to the caller (i.e., add an error return
and propagate it) or, if you prefer a runtime fallback, log the error via the
base logger and continue using a minimal/default logger instance; specifically
remove panic(err), handle the failing error from the logger construction (err
variable) by calling baseLogger.Error(...) with the error details and
returning/using a default logger instead (or update the function signature to
return (Logger, error) and return err). Ensure you update all call sites of that
builder to handle the returned error or accept the fallback.
🧹 Nitpick comments (1)
config.go (1)

118-141: Unnecessary file opens when FileLogger is configured.

When cfg.FileLogger != nil, the resolveOutputWriter() on line 118 still opens any files specified in cfg.Output, but the returned writer w is never used—only the lumberjack logger writes. While the closers are properly tracked (line 135), opening files that won't be written to is wasteful.

Consider moving resolveOutputWriter() after the FileLogger check or skipping it when FileLogger is configured.

♻️ Proposed fix: skip resolveOutputWriter when FileLogger is set
 	switch mode {
 	case off, none:
 		return &BuildResult{Logger: slog.New(slog.DiscardHandler)}, nil
 	case production, development, raw:
 		// handled below
 	default:
 		// Unknown mode — fall through to development-like behavior.
 	}

+	// File Logger: use lumberjack writer with JSON handler.
+	if cfg.FileLogger != nil {
+		cfg.FileLogger.InitDefaults()
+
+		lj := &lumberjack.Logger{
+			Filename:   cfg.FileLogger.LogOutput,
+			MaxSize:    cfg.FileLogger.MaxSize,
+			MaxAge:     cfg.FileLogger.MaxAge,
+			MaxBackups: cfg.FileLogger.MaxBackups,
+			Compress:   cfg.FileLogger.Compress,
+		}
+
+		return &BuildResult{
+			Logger:  slog.New(slog.NewJSONHandler(lj, &slog.HandlerOptions{Level: level})),
+			Closers: []io.Closer{lj},
+		}, nil
+	}
+
 	w, closers, err := cfg.resolveOutputWriter()
 	if err != nil {
 		return nil, errors.E(op, err)
 	}

-	// File Logger: use lumberjack writer with JSON handler.
-	if cfg.FileLogger != nil {
-		cfg.FileLogger.InitDefaults()
-
-		lj := &lumberjack.Logger{
-			Filename:   cfg.FileLogger.LogOutput,
-			MaxSize:    cfg.FileLogger.MaxSize,
-			MaxAge:     cfg.FileLogger.MaxAge,
-			MaxBackups: cfg.FileLogger.MaxBackups,
-			Compress:   cfg.FileLogger.Compress,
-		}
-
-		closers = append(closers, lj)
-
-		return &BuildResult{
-			Logger:  slog.New(slog.NewJSONHandler(lj, &slog.HandlerOptions{Level: level})),
-			Closers: closers,
-		}, nil
-	}
-
 	var handler slog.Handler
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.go` around lines 118 - 141, The code currently calls
cfg.resolveOutputWriter() (producing w and closers) before checking
cfg.FileLogger, which causes unnecessary file opens when FileLogger is set;
change the flow in the function containing resolveOutputWriter, so that the
cfg.FileLogger != nil branch is evaluated first and returns the BuildResult with
the lumberjack.Logger and closers without calling resolveOutputWriter; if other
branches still need resolveOutputWriter, call it only when cfg.FileLogger is
nil, ensuring you still append any returned closers to the BuildResult.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config.go`:
- Around line 47-51: The InitDefaults method on FileLoggerConfig incorrectly
sets LogOutput to a directory (os.TempDir()); update InitDefaults (function
InitDefaults on type FileLoggerConfig) to either leave LogOutput empty so
lumberjack uses its own default, or build a proper file path (e.g.,
filepath.Join(os.TempDir(), fmt.Sprintf("%s-lumberjack.log",
filepath.Base(os.Args[0])))) and assign that to LogOutput before returning;
ensure references to LogOutput are consistent with lumberjack.Logger's Filename
expectation and import/path utilities (filepath, os, fmt) as needed.

---

Duplicate comments:
In `@logger.go`:
- Around line 25-31: The channel-specific io.Closer values returned from
cfg.BuildLogger() are discarded in the if-block that resolves channels (when
reading l.channels.Channels[name]); capture and store them on the Log instance
so they can be closed at shutdown. Add a closers []io.Closer field to the Log
struct, append res.Closers (from BuildLogger) when constructing the channel
logger (the branch that returns res.Logger.With("logger", name)), and implement
a Close() method on Log that iterates and closes all collected closers (and any
global closers), then ensure plugin shutdown calls Log.Close(). This preserves
res.Logger.With(...) behavior while preventing file descriptor/log-buffer leaks.
- Around line 27-29: The panic(err) call in the logger build path should be
replaced with graceful error handling: change the builder function that
currently calls panic(err) (the function that constructs the logger and checks
err) to either return the error to the caller (i.e., add an error return and
propagate it) or, if you prefer a runtime fallback, log the error via the base
logger and continue using a minimal/default logger instance; specifically remove
panic(err), handle the failing error from the logger construction (err variable)
by calling baseLogger.Error(...) with the error details and returning/using a
default logger instead (or update the function signature to return (Logger,
error) and return err). Ensure you update all call sites of that builder to
handle the returned error or accept the fallback.

In `@tests/logger_test.go`:
- Around line 131-138: Replace the fragile fixed sleep in tests/logger_test.go
(the time.Sleep(time.Second) before calling http.NewRequestWithContext and
http.DefaultClient.Do) with a retry/poll loop that waits for the test HTTP
server to accept connections: repeatedly attempt a lightweight probe (e.g.,
net.Dial or an HTTP GET) against the server address with short intervals until
success or a total timeout elapses, then proceed to create the request with
http.NewRequestWithContext and call http.DefaultClient.Do; this ensures the
server is ready and prevents intermittent "connection refused" failures.

---

Nitpick comments:
In `@config.go`:
- Around line 118-141: The code currently calls cfg.resolveOutputWriter()
(producing w and closers) before checking cfg.FileLogger, which causes
unnecessary file opens when FileLogger is set; change the flow in the function
containing resolveOutputWriter, so that the cfg.FileLogger != nil branch is
evaluated first and returns the BuildResult with the lumberjack.Logger and
closers without calling resolveOutputWriter; if other branches still need
resolveOutputWriter, call it only when cfg.FileLogger is nil, ensuring you still
append any returned closers to the BuildResult.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 779c72e3-4f89-4f3f-bd0f-68d60ed1a7e1

📥 Commits

Reviewing files that changed from the base of the PR and between 93ed22b and 83883bd.

⛔ Files ignored due to path filters (2)
  • go.work.sum is excluded by !**/*.sum
  • tests/go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • config.go
  • handler.go
  • logger.go
  • tests/logger_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • handler.go

Comment thread config.go Outdated
Signed-off-by: Valery Piashchynski <piashchynski.valery@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/php_test_files/composer.json (1)

6-23: ⚠️ Potential issue | 🔴 Critical

Remove php-http/guzzle7-adapter:^1.1 or provide an alternative solution for PHP 8.5 compatibility.

The fixture now requires php-http/guzzle7-adapter:^1.1, which constrains PHP to ^7.3 | ^8.0 and does not support PHP 8.5. This dependency will fail to install during CI runs with PHP 8.5, blocking test execution. Either replace the adapter with a version that supports PHP 8.5 or conditionally exclude this dependency for PHP 8.5+ environments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/php_test_files/composer.json` around lines 6 - 23, The composer fixture
pins "php-http/guzzle7-adapter": "^1.1" which constrains PHP to ^7.3 | ^8.0 and
fails on PHP 8.5; remove that dependency from composer.json (delete the
"php-http/guzzle7-adapter": "^1.1" entry) or replace it with a version that
explicitly supports PHP 8.5 (update the package constraint to a known PHP
8.5-compatible release), or alternatively make the adapter conditional (move it
to a platform-conditional require or require-dev that is skipped for PHP >=8.5)
so CI with PHP 8.5 can install dependencies successfully.
🧹 Nitpick comments (3)
tests/logger_test.go (1)

321-370: Test lacks output verification for custom format.

The test verifies the system initializes and shuts down without errors, but doesn't validate that the custom format %time% [%level%] %message% %attrs% produces correctly formatted output. Consider capturing logs and asserting the format is applied correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/logger_test.go` around lines 321 - 370, TestLoggerCustomFormat
currently only exercises startup/shutdown; modify it to capture and assert
formatted log output by redirecting the logger output to a buffer or test hook,
emit a known log (e.g., call the logger from TestPlugin or directly via the
logger plugin's exported logger), then read the buffer and assert it matches the
configured format "%time% [%level%] %message% %attrs%" (check presence/order of
time-like token, "[LEVEL]" token, the exact message text, and attributes),
finally proceed with the existing shutdown flow; refer to
TestLoggerCustomFormat, logger.Plugin, and TestPlugin to locate where to inject
the buffer/hook and the log call.
format_handler.go (2)

221-237: Consider quoting attribute values that contain spaces.

If an attribute value contains spaces (e.g., slog.String("msg", "hello world")), the output msg=hello world could be ambiguous when parsing. Consider quoting values that contain spaces or special characters if log parsing is a requirement.

💡 Example fix for value quoting
 func formatAttrValue(v slog.Value) string {
 	if v.Kind() == slog.KindGroup {
 		var sb strings.Builder
 		for i, a := range v.Group() {
 			if i > 0 {
 				sb.WriteByte(' ')
 			}
 			sb.WriteString(a.Key)
 			sb.WriteByte('=')
 			sb.WriteString(formatAttrValue(a.Value))
 		}
 		return sb.String()
 	}
-	return v.String()
+	s := v.String()
+	if strings.ContainsAny(s, " \t\n\"") {
+		return strconv.Quote(s)
+	}
+	return s
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@format_handler.go` around lines 221 - 237, The formatAttrValue function
currently returns unquoted strings (in formatAttrValue and when recursing
through v.Group()), which makes outputs like msg=hello world ambiguous; update
formatAttrValue to detect when a value string (v.String()) contains spaces or
special characters (e.g., whitespace, '=', '"' or '\' ) and return a quoted and
escaped representation (wrap in double quotes and escape internal
quotes/backslashes) so group rendering in the v.Kind()==slog.KindGroup branch
uses the safe, quoted form; ensure recursion (formatAttrValue(a.Value))
preserves quoting/escaping for nested group values.

155-170: Consider reusing strings.Replacer for high-throughput scenarios.

Creating a new strings.NewReplacer on every Handle call has some overhead. For most use cases this is fine, but if this handler will be used in high-throughput logging scenarios, you could potentially cache the replacer and only substitute the dynamic values. This is a minor optimization consideration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@format_handler.go` around lines 155 - 170, The current code recreates
strings.NewReplacer on every call (using h.format) which is unnecessary in
high-throughput paths; instead parse/cache a template once on handler creation
(add a field like formatTmpl *template.Template to the handler struct and
initialize it from h.format), then in the Handle method execute
formatTmpl.Execute with a small data struct containing timeStr, r.Level,
r.Message, attrsStr, loggerName, sourceFile, sourceLine, sourceFunc and append
h.lineEnding before writing; keep the existing h.mu lock and io.WriteString(h.w,
...) but replace per-call NewReplacer creation with the cached template
execution to avoid repeated allocations from strings.NewReplacer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@format_handler_test.go`:
- Around line 546-548: Update the misleading comment above the test assertion:
clarify that pid is attached before the group and therefore does NOT get the
group prefix (so expected "pid=1" is correct), while http.method is inside the
group and will be prefixed; modify the comment near the variables `got :=
strings.TrimSpace(buf.String())` and `want := "req pid=1 http.method=GET"` to
state this accurate behavior.

In `@format_handler.go`:
- Around line 56-79: NewFormatHandler currently assigns opts.Level directly to
the FormatHandler.level which can be nil and lead to a panic in Enabled when
calling h.level.Level(); update NewFormatHandler to defensively default
opts.Level to slog.LevelInfo when opts.Level is nil (check opts.Level and set
f.Level = slog.LevelInfo-like default before storing into FormatHandler.level),
so FormatHandler.level is never nil and Enabled can safely call h.level.Level();
reference NewFormatHandler, FormatHandlerOptions, FormatHandler.level, and the
Enabled method/h.level.Level() when making the change.

---

Outside diff comments:
In `@tests/php_test_files/composer.json`:
- Around line 6-23: The composer fixture pins "php-http/guzzle7-adapter": "^1.1"
which constrains PHP to ^7.3 | ^8.0 and fails on PHP 8.5; remove that dependency
from composer.json (delete the "php-http/guzzle7-adapter": "^1.1" entry) or
replace it with a version that explicitly supports PHP 8.5 (update the package
constraint to a known PHP 8.5-compatible release), or alternatively make the
adapter conditional (move it to a platform-conditional require or require-dev
that is skipped for PHP >=8.5) so CI with PHP 8.5 can install dependencies
successfully.

---

Nitpick comments:
In `@format_handler.go`:
- Around line 221-237: The formatAttrValue function currently returns unquoted
strings (in formatAttrValue and when recursing through v.Group()), which makes
outputs like msg=hello world ambiguous; update formatAttrValue to detect when a
value string (v.String()) contains spaces or special characters (e.g.,
whitespace, '=', '"' or '\' ) and return a quoted and escaped representation
(wrap in double quotes and escape internal quotes/backslashes) so group
rendering in the v.Kind()==slog.KindGroup branch uses the safe, quoted form;
ensure recursion (formatAttrValue(a.Value)) preserves quoting/escaping for
nested group values.
- Around line 155-170: The current code recreates strings.NewReplacer on every
call (using h.format) which is unnecessary in high-throughput paths; instead
parse/cache a template once on handler creation (add a field like formatTmpl
*template.Template to the handler struct and initialize it from h.format), then
in the Handle method execute formatTmpl.Execute with a small data struct
containing timeStr, r.Level, r.Message, attrsStr, loggerName, sourceFile,
sourceLine, sourceFunc and append h.lineEnding before writing; keep the existing
h.mu lock and io.WriteString(h.w, ...) but replace per-call NewReplacer creation
with the cached template execution to avoid repeated allocations from
strings.NewReplacer.

In `@tests/logger_test.go`:
- Around line 321-370: TestLoggerCustomFormat currently only exercises
startup/shutdown; modify it to capture and assert formatted log output by
redirecting the logger output to a buffer or test hook, emit a known log (e.g.,
call the logger from TestPlugin or directly via the logger plugin's exported
logger), then read the buffer and assert it matches the configured format
"%time% [%level%] %message% %attrs%" (check presence/order of time-like token,
"[LEVEL]" token, the exact message text, and attributes), finally proceed with
the existing shutdown flow; refer to TestLoggerCustomFormat, logger.Plugin, and
TestPlugin to locate where to inject the buffer/hook and the log call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6eebe0b5-6002-4ef6-a28a-34797f36e277

📥 Commits

Reviewing files that changed from the base of the PR and between 83883bd and e832042.

📒 Files selected for processing (7)
  • config.go
  • format_handler.go
  • format_handler_test.go
  • schema.json
  • tests/configs/.rr-custom-format.yaml
  • tests/logger_test.go
  • tests/php_test_files/composer.json
✅ Files skipped from review due to trivial changes (1)
  • tests/configs/.rr-custom-format.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • schema.json
  • config.go

Comment thread format_handler_test.go
Comment thread format_handler.go
@rustatian rustatian mentioned this pull request Mar 20, 2026
6 tasks
@rustatian rustatian added the enhancement New feature or request label Mar 20, 2026
Signed-off-by: Valery Piashchynski <piashchynski.valery@gmail.com>
@rustatian rustatian merged commit a1c3a9d into master Mar 20, 2026
5 of 7 checks passed
@rustatian rustatian deleted the pre-release/v6-beta branch March 20, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

2 participants