feat: add message replacement for log records

Add Logger.AddReplacer to mask or normalize message text before records reach writers.
Document replacement behavior in README and package GoDoc, and update the example.

Also fix JSON writer edge cases:
- avoid panic on empty message lists
- preserve newline semantics when the last message already ends with n
- keep original message argument types for custom writers when no replacers are configured

Update dependencies and add release notes for the next release.
This commit is contained in:
2026-04-20 18:00:11 +03:00
parent 5d96c6fbae
commit 9268e222ab
11 changed files with 232 additions and 23 deletions
+57 -1
View File
@@ -2,6 +2,7 @@ package slog
import (
"bytes"
"encoding/json"
"errors"
"os"
"strings"
@@ -13,6 +14,7 @@ type stubLoggerWriter struct {
closeErr error
printCalls int
closeCalls int
messages []any
}
func (w *stubLoggerWriter) Close() error {
@@ -24,8 +26,9 @@ func (w *stubLoggerWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func (w *stubLoggerWriter) Print(_ LogLevel, _ string, _ []*MethodTraceback, _ ...any) error {
func (w *stubLoggerWriter) Print(_ LogLevel, _ string, _ []*MethodTraceback, messages ...any) error {
w.printCalls++
w.messages = append([]any(nil), messages...)
return w.printErr
}
@@ -80,6 +83,20 @@ func TestLoggerPrintDoesNotRecurseOnWriterError(t *testing.T) {
}
}
func TestLoggerPreservesMessageTypesWithoutReplacers(t *testing.T) {
writer := &stubLoggerWriter{}
logger := CreateLogger().AddWriter(writer)
logger.Error("status", 500)
if len(writer.messages) != 2 {
t.Fatalf("writer should receive two messages, got %d", len(writer.messages))
}
if _, ok := writer.messages[1].(int); !ok {
t.Fatalf("writer should receive original int message type, got %T", writer.messages[1])
}
}
func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) {
writer := CreateTextWriter(&bytes.Buffer{}, false, false)
if err := writer.Close(); err != nil {
@@ -130,6 +147,45 @@ func TestCreateJsonWriterDoesNotCloseExternalCloser(t *testing.T) {
}
}
func TestJsonWriterPrintAllowsEmptyMessages(t *testing.T) {
var buf bytes.Buffer
writer := CreateJsonWriter(&buf, false)
if err := writer.Print(INFO, "TEST", nil); err != nil {
t.Fatalf("Print() error = %v", err)
}
var message LoggerJsonMessage
if err := json.Unmarshal(buf.Bytes(), &message); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if message.Message != "" {
t.Fatalf("message should be empty, got %q", message.Message)
}
}
func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
var buf bytes.Buffer
writer := CreateJsonWriter(&buf, false)
if err := writer.Print(INFO, "TEST", nil, "hello\n"); err != nil {
t.Fatalf("Print() error = %v", err)
}
data := buf.Bytes()
if !bytes.HasSuffix(data, []byte("\n")) {
t.Fatalf("JSON output should end with newline, got %q", data)
}
var message LoggerJsonMessage
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if message.Message != "hello" {
t.Fatalf("message should not include trailing newline, got %q", message.Message)
}
}
func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) {
stdoutFile := swapStdout(t)