feat(logz)!: promote to v1.0.0 — configurable io.Writer output destination

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.
This commit is contained in:
2026-05-11 18:50:37 -06:00
parent 3667b92fab
commit 05c99f72ff
3 changed files with 41 additions and 18 deletions

View File

@@ -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/), 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). 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 ## [0.9.0] - 2026-03-18
### Added ### Added

17
logz.go
View File

@@ -3,6 +3,7 @@ package logz
import ( import (
"context" "context"
"errors" "errors"
"io"
"log/slog" "log/slog"
"os" "os"
) )
@@ -20,7 +21,7 @@ type errorWithContext interface {
} }
// Options configures a Logger instance. // 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 { type Options struct {
// Level is the minimum log level. Default: slog.LevelInfo (zero value). // Level is the minimum log level. Default: slog.LevelInfo (zero value).
Level slog.Level Level slog.Level
@@ -28,6 +29,9 @@ type Options struct {
JSON bool JSON bool
// StaticArgs are key-value pairs attached to every log record. // StaticArgs are key-value pairs attached to every log record.
StaticArgs []any 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. // Logger is the interface for structured logging.
@@ -56,16 +60,17 @@ type slogLogger struct {
// New returns a new Logger configured by opts. // New returns a new Logger configured by opts.
func New(opts Options) Logger { func New(opts Options) Logger {
handlerOpts := &slog.HandlerOptions{ w := opts.Writer
Level: opts.Level, if w == nil {
AddSource: false, w = os.Stdout
} }
handlerOpts := &slog.HandlerOptions{Level: opts.Level}
var handler slog.Handler var handler slog.Handler
if opts.JSON { if opts.JSON {
handler = slog.NewJSONHandler(os.Stdout, handlerOpts) handler = slog.NewJSONHandler(w, handlerOpts)
} else { } else {
handler = slog.NewTextHandler(os.Stdout, handlerOpts) handler = slog.NewTextHandler(w, handlerOpts)
} }
base := slog.New(handler) base := slog.New(handler)

View File

@@ -36,9 +36,8 @@ func (e *errFull) ErrorContext() map[string]any { return e.fields }
// Helper: logger that writes to a buffer for inspection // Helper: logger that writes to a buffer for inspection
// --------------------------------------------------------------- // ---------------------------------------------------------------
func newTestLogger(buf *bytes.Buffer, level slog.Level) *slogLogger { func newTestLogger(buf *bytes.Buffer, level slog.Level) Logger {
handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: level}) return New(Options{Writer: buf, Level: level, JSON: true})
return &slogLogger{logger: slog.New(handler)}
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -56,10 +55,8 @@ func TestNew_ZeroOptions(t *testing.T) {
func TestNew_JSONFormat(t *testing.T) { func TestNew_JSONFormat(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
sl := &slogLogger{ l := New(Options{JSON: true, Writer: &buf})
logger: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})), l.Info("json test", "key", "value")
}
sl.Info("json test", "key", "value")
var out map[string]any var out map[string]any
if err := json.Unmarshal(buf.Bytes(), &out); err != nil { 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) { func TestNew_StaticArgs(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) l := New(Options{Writer: &buf, JSON: true, StaticArgs: []any{"service", "test-svc"}})
base := slog.New(handler).With("service", "test-svc") l.Info("static args test")
sl := &slogLogger{logger: base}
sl.Info("static args test")
if !strings.Contains(buf.String(), "test-svc") { if !strings.Contains(buf.String(), "test-svc") {
t.Errorf("static arg not found in output: %s", buf.String()) t.Errorf("static arg not found in output: %s", buf.String())