From 05c99f72ffae892d50aa49e53c4a00a6a342440c Mon Sep 17 00:00:00 2001 From: Rene Nochebuena Guerrero Date: Mon, 11 May 2026 18:50:37 -0600 Subject: [PATCH] =?UTF-8?q?feat(logz)!:=20promote=20to=20v1.0.0=20?= =?UTF-8?q?=E2=80=94=20configurable=20io.Writer=20output=20destination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Writer io.Writer field to Options; when nil, defaults to os.Stdout (no breaking change). Enables log capture in tests via the public API and supports writing to files, multi-writers, or any io.Writer implementation in production. All remaining roadmap items validated in production (xerrors enrichment concurrency, WithFields merge semantics, Logger interface finality). API committed as stable. --- CHANGELOG.md | 15 +++++++++++++++ logz.go | 17 +++++++++++------ logz_test.go | 27 +++++++++++++++------------ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50d9742..71edcd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this module will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] — 2026-05-12 + +### Added + +- `Options.Writer io.Writer` — configurable output destination; `nil` defaults + to `os.Stdout` (backward-compatible). Accepts any `io.Writer` implementation: + `*os.File`, `bytes.Buffer`, `io.MultiWriter`, custom sinks, etc. + +### Unchanged + +All existing API (`Logger`, `New`, `WithRequestID`, `GetRequestID`, `WithField`, +`WithFields`) is API-compatible with v0.9.0. + +[1.0.0]: https://code.nochebuena.dev/go/logz/releases/tag/v1.0.0 + ## [0.9.0] - 2026-03-18 ### Added diff --git a/logz.go b/logz.go index 9204e3a..8c56373 100644 --- a/logz.go +++ b/logz.go @@ -3,6 +3,7 @@ package logz import ( "context" "errors" + "io" "log/slog" "os" ) @@ -20,7 +21,7 @@ type errorWithContext interface { } // Options configures a Logger instance. -// The zero value is valid: INFO level, text output, no static args. +// The zero value is valid: INFO level, text output, os.Stdout, no static args. type Options struct { // Level is the minimum log level. Default: slog.LevelInfo (zero value). Level slog.Level @@ -28,6 +29,9 @@ type Options struct { JSON bool // StaticArgs are key-value pairs attached to every log record. StaticArgs []any + // Writer is the output destination. Defaults to os.Stdout when nil. + // Accepts any io.Writer: *os.File, bytes.Buffer, io.MultiWriter, etc. + Writer io.Writer } // Logger is the interface for structured logging. @@ -56,16 +60,17 @@ type slogLogger struct { // New returns a new Logger configured by opts. func New(opts Options) Logger { - handlerOpts := &slog.HandlerOptions{ - Level: opts.Level, - AddSource: false, + w := opts.Writer + if w == nil { + w = os.Stdout } + handlerOpts := &slog.HandlerOptions{Level: opts.Level} var handler slog.Handler if opts.JSON { - handler = slog.NewJSONHandler(os.Stdout, handlerOpts) + handler = slog.NewJSONHandler(w, handlerOpts) } else { - handler = slog.NewTextHandler(os.Stdout, handlerOpts) + handler = slog.NewTextHandler(w, handlerOpts) } base := slog.New(handler) diff --git a/logz_test.go b/logz_test.go index 6faa0ff..dfc0de6 100644 --- a/logz_test.go +++ b/logz_test.go @@ -36,9 +36,8 @@ func (e *errFull) ErrorContext() map[string]any { return e.fields } // Helper: logger that writes to a buffer for inspection // --------------------------------------------------------------- -func newTestLogger(buf *bytes.Buffer, level slog.Level) *slogLogger { - handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: level}) - return &slogLogger{logger: slog.New(handler)} +func newTestLogger(buf *bytes.Buffer, level slog.Level) Logger { + return New(Options{Writer: buf, Level: level, JSON: true}) } // --------------------------------------------------------------- @@ -56,10 +55,8 @@ func TestNew_ZeroOptions(t *testing.T) { func TestNew_JSONFormat(t *testing.T) { var buf bytes.Buffer - sl := &slogLogger{ - logger: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})), - } - sl.Info("json test", "key", "value") + l := New(Options{JSON: true, Writer: &buf}) + l.Info("json test", "key", "value") var out map[string]any if err := json.Unmarshal(buf.Bytes(), &out); err != nil { @@ -70,13 +67,19 @@ func TestNew_JSONFormat(t *testing.T) { } } +func TestNew_Writer(t *testing.T) { + var buf bytes.Buffer + l := New(Options{Writer: &buf}) + l.Info("writer test") + if !strings.Contains(buf.String(), "writer test") { + t.Errorf("output not written to provided writer: %s", buf.String()) + } +} + func TestNew_StaticArgs(t *testing.T) { var buf bytes.Buffer - handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) - base := slog.New(handler).With("service", "test-svc") - sl := &slogLogger{logger: base} - - sl.Info("static args test") + l := New(Options{Writer: &buf, JSON: true, StaticArgs: []any{"service", "test-svc"}}) + l.Info("static args test") if !strings.Contains(buf.String(), "test-svc") { t.Errorf("static arg not found in output: %s", buf.String())