github.com/phsym/zeroslog@v0.1.1-0.20240224183259-0b7a5ea94339/zerolog.go (about)

     1  package zeroslog
     2  
     3  import (
     4  	"context"
     5  	"encoding"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"log/slog"
    10  	"net"
    11  	"runtime"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/rs/zerolog"
    16  )
    17  
    18  // HandlerOptions are options for a ZerologHandler.
    19  // A zero HandlerOptions consists entirely of default values.
    20  type HandlerOptions struct {
    21  	// AddSource causes the handler to compute the source code position
    22  	// of the log statement and add a SourceKey attribute to the output.
    23  	AddSource bool
    24  
    25  	// Level reports the minimum record level that will be logged.
    26  	// The handler discards records with lower levels.
    27  	// If Level is nil, the handler assumes the level set in the logger.
    28  	// The handler calls Level.Level if it's not nil for each record processed;
    29  	// to adjust the minimum level dynamically, use a LevelVar.
    30  	Level slog.Leveler
    31  }
    32  
    33  // zerologHandler is an internal interface used to expose additional methods
    34  // between handlers.
    35  type zerologHandler interface {
    36  	slog.Handler
    37  	// handleGroup handles records comming from the child group.
    38  	handleGroup(group string, rec *slog.Record, e *zerolog.Event)
    39  }
    40  
    41  // Handler is an slog.Handler implementation that uses zerolog to process slog.Record.
    42  type Handler struct {
    43  	opts   *HandlerOptions
    44  	logger zerolog.Logger
    45  }
    46  
    47  var _ zerologHandler = (*Handler)(nil)
    48  
    49  // NewHandler creates a *ZerologHandler implementing slog.Handler.
    50  // It wraps a zerolog.Logger to which log records will be sent.
    51  //
    52  // Unlesse opts.Level is not nil, the logger level is used to filter out records, otherwise
    53  // opts.Level is used.
    54  //
    55  // The provided logger instance must be configured to not send timestamps or caller information.
    56  //
    57  // If opts is nil, it assumes default options values.
    58  func NewHandler(logger zerolog.Logger, opts *HandlerOptions) *Handler {
    59  	if opts == nil {
    60  		opts = new(HandlerOptions)
    61  	}
    62  	opt := *opts // Copy
    63  	return &Handler{
    64  		opts:   &opt,
    65  		logger: logger,
    66  	}
    67  }
    68  
    69  // NewJsonHandler is a shortcut to calling
    70  //
    71  //	NewHandler(zerolog.New(out).Level(zerolog.InfoLevel), opts)
    72  func NewJsonHandler(out io.Writer, opts *HandlerOptions) *Handler {
    73  	return NewHandler(zerolog.New(out).Level(zerolog.InfoLevel), opts)
    74  }
    75  
    76  // NewConsoleHandler creates a new zerolog handler, wrapping out into a zerolog.ConsoleWriter.
    77  // It's a shortcut to calling
    78  //
    79  //	NewHandler(zerolog.New(&zerolog.ConsoleWriter{Out: out, TimeFormat: time.DateTime}).Level(zerolog.InfoLevel), opts)
    80  func NewConsoleHandler(out io.Writer, opts *HandlerOptions) *Handler {
    81  	return NewJsonHandler(&zerolog.ConsoleWriter{Out: out, TimeFormat: time.DateTime}, opts)
    82  }
    83  
    84  // Enabled implements slog.Handler.
    85  func (h *Handler) Enabled(_ context.Context, lvl slog.Level) bool {
    86  	if h.opts.Level != nil {
    87  		return lvl >= h.opts.Level.Level()
    88  	}
    89  	return zerologLevel(lvl) >= h.logger.GetLevel()
    90  }
    91  
    92  // startLog creates a new logging event at the given level.
    93  func (h *Handler) startLog(lvl slog.Level) *zerolog.Event {
    94  	logger := h.logger
    95  	if h.opts.Level != nil {
    96  		logger = h.logger.Level(zerologLevel(h.opts.Level.Level()))
    97  	}
    98  	return logger.WithLevel(zerologLevel(lvl))
    99  }
   100  
   101  // endLog finalize the log event by appending record source, timestamp and message before sending it.
   102  func (h *Handler) endLog(rec *slog.Record, evt *zerolog.Event) {
   103  	if h.opts.AddSource && rec.PC > 0 {
   104  		frame, _ := runtime.CallersFrames([]uintptr{rec.PC}).Next()
   105  		evt.Str(zerolog.CallerFieldName, fmt.Sprintf("%s:%d", frame.File, frame.Line))
   106  	}
   107  
   108  	evt.Time(zerolog.TimestampFieldName, rec.Time)
   109  	evt.Msg(rec.Message)
   110  }
   111  
   112  // handleGroup handles records comming from a child group.
   113  func (h *Handler) handleGroup(group string, rec *slog.Record, dict *zerolog.Event) {
   114  	evt := h.startLog(rec.Level)
   115  	evt.Dict(group, dict)
   116  	h.endLog(rec, evt)
   117  }
   118  
   119  // Handle implements slog.Handler.
   120  func (h *Handler) Handle(_ context.Context, rec slog.Record) error {
   121  	evt := h.startLog(rec.Level)
   122  	rec.Attrs(func(a slog.Attr) bool {
   123  		mapAttr(evt, a)
   124  		return true
   125  	})
   126  	h.endLog(&rec, evt)
   127  	return nil
   128  }
   129  
   130  // WithAttrs implements slog.Handler.
   131  func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
   132  	return &Handler{
   133  		opts:   h.opts,
   134  		logger: mapAttrs(h.logger.With(), attrs...).Logger(),
   135  	}
   136  }
   137  
   138  // WithGroup implements slog.Handler.
   139  func (h *Handler) WithGroup(name string) slog.Handler {
   140  	return &groupHandler{
   141  		parent: h,
   142  		ctx:    zerolog.Context{},
   143  		name:   strings.TrimSpace(name),
   144  	}
   145  }
   146  
   147  // groupHandler handles groups and subgroups.
   148  type groupHandler struct {
   149  	parent zerologHandler
   150  	ctx    zerolog.Context
   151  	name   string
   152  }
   153  
   154  var _ zerologHandler = (*groupHandler)(nil)
   155  
   156  // Enabled implements slog.Handler.
   157  func (h *groupHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
   158  	return h.parent.Enabled(ctx, lvl)
   159  }
   160  
   161  // handleGroup handles records comming from a child group.
   162  func (h *groupHandler) handleGroup(group string, rec *slog.Record, dict *zerolog.Event) {
   163  	l := h.ctx.Logger()
   164  	evt := l.Log()
   165  	evt.Dict(group, dict)
   166  	h.parent.handleGroup(h.name, rec, evt)
   167  }
   168  
   169  // Handle implements slog.Handler.
   170  func (h *groupHandler) Handle(ctx context.Context, rec slog.Record) error {
   171  	l := h.ctx.Logger()
   172  	evt := l.Log()
   173  	rec.Attrs(func(a slog.Attr) bool {
   174  		mapAttr(evt, a)
   175  		return true
   176  	})
   177  	h.parent.handleGroup(h.name, &rec, evt)
   178  	return nil
   179  }
   180  
   181  // WithAttrs implements slog.Handler.
   182  func (h *groupHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
   183  	return &groupHandler{
   184  		parent: h.parent,
   185  		ctx:    mapAttrs(h.ctx.Logger().With(), attrs...),
   186  		name:   h.name,
   187  	}
   188  }
   189  
   190  // WithGroup implements slog.Handler.
   191  func (h *groupHandler) WithGroup(name string) slog.Handler {
   192  	return &groupHandler{
   193  		parent: h,
   194  		ctx:    zerolog.Context{},
   195  		name:   name,
   196  	}
   197  }
   198  
   199  // zlogWriter is an interface with methods common between
   200  // zerolog.Context and *zerolog.Event. This interface is
   201  // implemented by both zerolog.Context and *zerolog.Event.
   202  type zlogWriter[E any] interface {
   203  	Bool(string, bool) E
   204  	Dur(string, time.Duration) E
   205  	Float64(string, float64) E
   206  	Int64(string, int64) E
   207  	Str(string, string) E
   208  	Time(string, time.Time) E
   209  	Uint64(string, uint64) E
   210  	Dict(string, *zerolog.Event) E
   211  	Interface(string, any) E
   212  	AnErr(string, error) E
   213  	Stringer(string, fmt.Stringer) E
   214  	IPAddr(string, net.IP) E
   215  	IPPrefix(string, net.IPNet) E
   216  	MACAddr(string, net.HardwareAddr) E
   217  	RawJSON(string, []byte) E
   218  }
   219  
   220  var (
   221  	_ zlogWriter[*zerolog.Event]  = (*zerolog.Event)(nil)
   222  	_ zlogWriter[zerolog.Context] = zerolog.Context{}
   223  )
   224  
   225  // mapAttrs writes multiple slog.Attr into the target which is either a zerolog.Context
   226  // or a *zerolog.Event.
   227  func mapAttrs[T zlogWriter[T]](target T, a ...slog.Attr) T {
   228  	for _, attr := range a {
   229  		target = mapAttr(target, attr)
   230  	}
   231  	return target
   232  }
   233  
   234  // mapAttr writes slog.Attr into the target which is either a zerolog.Context
   235  // or a *zerolog.Event.
   236  func mapAttr[T zlogWriter[T]](target T, a slog.Attr) T {
   237  	value := a.Value.Resolve()
   238  	switch value.Kind() {
   239  	case slog.KindGroup:
   240  		return target.Dict(a.Key, mapAttrs(zerolog.Dict(), value.Group()...))
   241  	case slog.KindBool:
   242  		return target.Bool(a.Key, value.Bool())
   243  	case slog.KindDuration:
   244  		return target.Dur(a.Key, value.Duration())
   245  	case slog.KindFloat64:
   246  		return target.Float64(a.Key, value.Float64())
   247  	case slog.KindInt64:
   248  		return target.Int64(a.Key, value.Int64())
   249  	case slog.KindString:
   250  		return target.Str(a.Key, value.String())
   251  	case slog.KindTime:
   252  		return target.Time(a.Key, value.Time())
   253  	case slog.KindUint64:
   254  		return target.Uint64(a.Key, value.Uint64())
   255  	case slog.KindAny:
   256  		fallthrough
   257  	default:
   258  		return mapAttrAny(target, a.Key, value.Any())
   259  	}
   260  }
   261  
   262  func mapAttrAny[T zlogWriter[T]](target T, key string, value any) T {
   263  	switch v := value.(type) {
   264  	case net.IP:
   265  		return target.IPAddr(key, v)
   266  	case net.IPNet:
   267  		return target.IPPrefix(key, v)
   268  	case net.HardwareAddr:
   269  		return target.MACAddr(key, v)
   270  	case error:
   271  		return target.AnErr(key, v)
   272  	case fmt.Stringer:
   273  		return target.Stringer(key, v)
   274  	case json.Marshaler:
   275  		txt, err := v.MarshalJSON()
   276  		if err == nil {
   277  			return target.RawJSON(key, txt)
   278  		}
   279  		return target.Str(key, "!ERROR:"+err.Error())
   280  	case encoding.TextMarshaler:
   281  		txt, err := v.MarshalText()
   282  		if err == nil {
   283  			return target.Str(key, string(txt))
   284  		}
   285  		return target.Str(key, "!ERROR:"+err.Error())
   286  	default:
   287  		return target.Interface(key, value)
   288  	}
   289  }
   290  
   291  // zerologLevel maps slog.Level into zerolog.Level.
   292  func zerologLevel(lvl slog.Level) zerolog.Level {
   293  	switch {
   294  	case lvl < slog.LevelDebug:
   295  		return zerolog.TraceLevel
   296  	case lvl < slog.LevelInfo:
   297  		return zerolog.DebugLevel
   298  	case lvl < slog.LevelWarn:
   299  		return zerolog.InfoLevel
   300  	case lvl < slog.LevelError:
   301  		return zerolog.WarnLevel
   302  	default:
   303  		return zerolog.ErrorLevel
   304  	}
   305  }