restructured logger from budigt

This commit is contained in:
juancwu 2026-04-25 20:35:03 +00:00
commit b2fd12b1c8
11 changed files with 1227 additions and 1 deletions

124
logger.go Normal file
View file

@ -0,0 +1,124 @@
package splinter
import (
"context"
"fmt"
"maps"
"os"
"time"
)
// Logger fans out log records to one or more Streams. Streams are fixed at
// construction time; child loggers from With share the same streams.
type Logger struct {
streams []Stream
baseAttrs map[string]any
onError func(stream string, rec Record, err error)
}
// Option configures a Logger.
type Option func(*Logger)
// WithStream adds a Stream to the logger.
func WithStream(s Stream) Option {
return func(l *Logger) { l.streams = append(l.streams, s) }
}
// WithAttrs pre-populates attributes on every record produced by this logger.
func WithAttrs(attrs map[string]any) Option {
return func(l *Logger) { maps.Copy(l.baseAttrs, attrs) }
}
// WithErrorHandler sets a callback invoked when a stream's Write fails.
// The default handler prints to stderr.
func WithErrorHandler(fn func(stream string, rec Record, err error)) Option {
return func(l *Logger) { l.onError = fn }
}
// New creates a Logger. With no streams supplied, a JSON ConsoleStream at
// LevelInfo writing to stderr is used.
func New(opts ...Option) *Logger {
l := &Logger{
baseAttrs: make(map[string]any),
onError: defaultOnError,
}
for _, o := range opts {
o(l)
}
if len(l.streams) == 0 {
l.streams = append(l.streams, NewConsoleStream(ConsoleJSON, LevelInfo))
}
return l
}
// With returns a child Logger with additional attributes merged onto the
// base. The child shares streams and the error handler with the parent.
func (l *Logger) With(attrs map[string]any) *Logger {
merged := make(map[string]any, len(l.baseAttrs)+len(attrs))
maps.Copy(merged, l.baseAttrs)
maps.Copy(merged, attrs)
return &Logger{
streams: l.streams,
baseAttrs: merged,
onError: l.onError,
}
}
// Debug logs at LevelDebug.
func (l *Logger) Debug(msg string, args ...any) { l.log(LevelDebug, msg, args...) }
// Info logs at LevelInfo.
func (l *Logger) Info(msg string, args ...any) { l.log(LevelInfo, msg, args...) }
// Warn logs at LevelWarn.
func (l *Logger) Warn(msg string, args ...any) { l.log(LevelWarn, msg, args...) }
// Error logs at LevelError.
func (l *Logger) Error(msg string, args ...any) { l.log(LevelError, msg, args...) }
// Close shuts down all streams. Returns the first error encountered, but
// always attempts to close every stream.
func (l *Logger) Close() error {
var firstErr error
for _, s := range l.streams {
if err := s.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (l *Logger) log(level Level, msg string, args ...any) {
rec := Record{
Time: time.Now(),
Level: level,
Message: msg,
Attrs: l.buildAttrs(args),
}
ctx := context.Background()
for _, s := range l.streams {
if !s.Enabled(level) {
continue
}
if err := s.Write(ctx, rec); err != nil {
l.onError(s.Name(), rec, err)
}
}
}
func (l *Logger) buildAttrs(args []any) map[string]any {
attrs := make(map[string]any, len(l.baseAttrs)+len(args)/2)
maps.Copy(attrs, l.baseAttrs)
for i := 0; i+1 < len(args); i += 2 {
key, ok := args[i].(string)
if !ok {
continue
}
attrs[key] = args[i+1]
}
return attrs
}
func defaultOnError(stream string, _ Record, err error) {
fmt.Fprintf(os.Stderr, "splinter: stream %q write failed: %v\n", stream, err)
}