feat(logger): slog 结构化日志封装(text/json + 级别解析)
This commit is contained in:
1 parent
62c7a2aa12
commit
0236010136
2 files changed
+163
No files matched your search
@@ -0,0 +1,70 @@
|
|||||||
|
// Package logger provides the project-wide structured logging facade built on
|
||||||
|
// log/slog. All modules must use this package instead of creating their own
|
||||||
|
// logger instances. Development builds render human-friendly TextHandler output;
|
||||||
|
// production builds render JSON for machine parsing.
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format selects the structured log output encoding.
|
||||||
|
type Format string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FormatText is the human-readable development format.
|
||||||
|
FormatText Format = "text"
|
||||||
|
// FormatJSON is the machine-parseable production format.
|
||||||
|
FormatJSON Format = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options configures a Logger.
|
||||||
|
type Options struct {
|
||||||
|
// Level is the minimum level to record, one of debug, info, warn, error.
|
||||||
|
// Unknown values fall back to info.
|
||||||
|
Level string
|
||||||
|
// Format is the output encoding, one of text, json. Unknown values fall
|
||||||
|
// back to text.
|
||||||
|
Format string
|
||||||
|
// Out is the destination writer. Defaults to os.Stderr when nil.
|
||||||
|
Out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builds a slog.Logger from opts.
|
||||||
|
func New(opts Options) *slog.Logger {
|
||||||
|
level := ParseLevel(opts.Level)
|
||||||
|
out := opts.Out
|
||||||
|
if out == nil {
|
||||||
|
out = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerOpts := &slog.HandlerOptions{Level: level}
|
||||||
|
var handler slog.Handler
|
||||||
|
if Format(opts.Format) == FormatJSON {
|
||||||
|
handler = slog.NewJSONHandler(out, handlerOpts)
|
||||||
|
} else {
|
||||||
|
handler = slog.NewTextHandler(out, handlerOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slog.New(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLevel maps a string level name to a slog.Level. Unknown or empty values
|
||||||
|
// produce slog.LevelInfo.
|
||||||
|
func ParseLevel(s string) slog.Level {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||||
|
case "debug":
|
||||||
|
return slog.LevelDebug
|
||||||
|
case "info":
|
||||||
|
return slog.LevelInfo
|
||||||
|
case "warn", "warning":
|
||||||
|
return slog.LevelWarn
|
||||||
|
case "error":
|
||||||
|
return slog.LevelError
|
||||||
|
default:
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseLevel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
expected slog.Level
|
||||||
|
}{
|
||||||
|
{name: "debug", in: "debug", expected: slog.LevelDebug},
|
||||||
|
{name: "debug upper", in: "DEBUG", expected: slog.LevelDebug},
|
||||||
|
{name: "info", in: "info", expected: slog.LevelInfo},
|
||||||
|
{name: "warn", in: "warn", expected: slog.LevelWarn},
|
||||||
|
{name: "warning alias", in: "warning", expected: slog.LevelWarn},
|
||||||
|
{name: "error", in: "error", expected: slog.LevelError},
|
||||||
|
{name: "empty defaults info", in: "", expected: slog.LevelInfo},
|
||||||
|
{name: "unknown defaults info", in: "trace", expected: slog.LevelInfo},
|
||||||
|
{name: "whitespace trimmed", in: " debug ", expected: slog.LevelDebug},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := ParseLevel(tt.in); got != tt.expected {
|
||||||
|
t.Fatalf("ParseLevel(%q) = %v, want %v", tt.in, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_TextFormat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
log := New(Options{Level: "debug", Format: "text", Out: &buf})
|
||||||
|
log.Info("hello")
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
if !strings.Contains(got, "level=INFO") || !strings.Contains(got, "msg=hello") {
|
||||||
|
t.Fatalf("text output missing level/msg, got: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_JSONFormat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
log := New(Options{Level: "debug", Format: "json", Out: &buf})
|
||||||
|
log.Warn("hello", "k", "v")
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
if !strings.Contains(got, `"level":"WARN"`) || !strings.Contains(got, `"msg":"hello"`) || !strings.Contains(got, `"k":"v"`) {
|
||||||
|
t.Fatalf("json output missing fields, got: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_LevelFiltersBelowConfigured(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
// Configured at error level: info/warn must not be emitted.
|
||||||
|
log := New(Options{Level: "error", Format: "json", Out: &buf})
|
||||||
|
log.Info("low", "k", "v")
|
||||||
|
log.Warn("mid", "k", "v")
|
||||||
|
log.Error("high", "k", "v")
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
if strings.Contains(got, "low") || strings.Contains(got, "mid") {
|
||||||
|
t.Fatalf("lower-than-configured levels leaked, got: %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "high") {
|
||||||
|
t.Fatalf("configured level not emitted, got: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_DefaultsToErrAndText(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// No options should not panic and should produce a working logger.
|
||||||
|
log := New(Options{})
|
||||||
|
if log == nil {
|
||||||
|
t.Fatal("New(Options{}) returned nil logger")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user