Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger_test

import (
"log/slog"
"os"
"testing"

Expand Down Expand Up @@ -101,6 +102,90 @@ func BenchmarkLogger_JsonCtx(b *testing.B) {
})
}

func BenchmarkHandler_Logfmt(b *testing.B) {
h := logger.NewHandler(discard{}, logger.LogfmtFormat(), slog.LevelDebug)
log := slog.New(h)

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.Error("some message")
}
})
}

func BenchmarkHandler_Json(b *testing.B) {
h := logger.NewHandler(discard{}, logger.JSONFormat(), slog.LevelDebug)
log := slog.New(h)

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.Error("some message")
}
})
}

func BenchmarkHandler_LogfmtWithGroup(b *testing.B) {
h := logger.NewHandler(discard{}, logger.LogfmtFormat(), slog.LevelDebug)
log := slog.New(h.WithGroup("service"))

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.Error("some message")
}
})
}

func BenchmarkHandler_JsonWithGroup(b *testing.B) {
h := logger.NewHandler(discard{}, logger.JSONFormat(), slog.LevelDebug)
log := slog.New(h.WithGroup("service"))

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.Error("some message")
}
})
}

func BenchmarkHandler_LogfmtWithGroupAndAttrs(b *testing.B) {
h := logger.NewHandler(discard{}, logger.LogfmtFormat(), slog.LevelDebug)
log := slog.New(h.WithGroup("db").WithAttrs([]slog.Attr{
slog.String("host", "localhost"),
slog.Int("port", 5432),
}))

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.Error("some message", slog.String("driver", "pgx"))
}
})
}

func BenchmarkHandler_JsonWithGroupAndAttrs(b *testing.B) {
h := logger.NewHandler(discard{}, logger.JSONFormat(), slog.LevelDebug)
log := slog.New(h.WithGroup("db").WithAttrs([]slog.Attr{
slog.String("host", "localhost"),
slog.Int("port", 5432),
}))

b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
log.Error("some message", slog.String("driver", "pgx"))
}
})
}

func BenchmarkLevelLogger_Logfmt(b *testing.B) {
log := logger.New(discard{}, logger.LogfmtFormat(), logger.Debug).With(ctx.Str("_n", "bench"), ctx.Int("_p", os.Getpid()))

Expand Down
17 changes: 17 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger_test

import (
"log/slog"
"os"

"github.com/hamba/logger/v2"
Expand All @@ -18,3 +19,19 @@ func ExampleSyncWriter() {

log.Info("redis connection", ctx.Str("redis", "some redis name"), ctx.Int("timeout", 10))
}

func ExampleNewHandler() {
h := logger.NewHandler(os.Stdout, logger.JSONFormat(), slog.LevelInfo)

log := slog.New(h).With(slog.String("env", "prod")).WithGroup("db")

log.Info("connected", slog.String("driver", "pgx"))
}

func ExampleNewHandler_logfmt() {
h := logger.NewHandler(os.Stdout, logger.LogfmtFormat(), slog.LevelInfo)

log := slog.New(h).With(slog.String("env", "prod")).WithGroup("db")

log.Info("connected", slog.String("driver", "pgx"))
}
189 changes: 189 additions & 0 deletions slog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package logger

import (
"context"
"io"
"log/slog"
)

// Handler is a slog.Handler backed by hamba/logger. It does not synchronize writes;
// wrap the writer with NewSyncWriter for concurrent use in non-posix environments.
type Handler struct {
Comment thread
nrwiersma marked this conversation as resolved.
w io.Writer
isDiscard bool
fmtr Formatter
lvl slog.Level
Comment thread
nrwiersma marked this conversation as resolved.

ctx []byte
prefix []byte

openGroups int
}
Comment thread
nrwiersma marked this conversation as resolved.

// NewHandler returns a new Handler.
func NewHandler(w io.Writer, fmtr Formatter, lvl slog.Level) *Handler {
return &Handler{
w: w,
isDiscard: w == io.Discard,
fmtr: fmtr,
lvl: lvl,
}
}

// Enabled returns false when lvl is below the configured minimum or the
// underlying writer is io.Discard.
func (h *Handler) Enabled(_ context.Context, lvl slog.Level) bool {
return !h.isDiscard && lvl >= h.lvl
}

// Handle writes the record to the underlying writer.
func (h *Handler) Handle(_ context.Context, r slog.Record) error {
e := newEvent(h.fmtr)
e.prefix = append(e.prefix, h.prefix...)

e.fmtr.AppendBeginMarker(e.buf)
e.fmtr.WriteMessage(e.buf, r.Time, mapSlogLevel(r.Level), r.Message)
e.buf.Write(h.ctx)

r.Attrs(func(a slog.Attr) bool {
appendAttr(e, a)
return true
})

for range h.openGroups {
e.prefix = e.fmtr.AppendGroupEnd(e.buf, e.prefix)
}

e.fmtr.AppendEndMarker(e.buf)
e.fmtr.AppendLineBreak(e.buf)

_, err := h.w.Write(e.buf.Bytes())
putEvent(e)
return err
}

// WithAttrs returns a new Handler with attrs pre-serialised into the context.
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return h
}

e := newEvent(h.fmtr)
e.buf.Write(h.ctx)
e.prefix = append(e.prefix, h.prefix...)

for _, a := range attrs {
appendAttr(e, a)
}

newCtx := make([]byte, e.buf.Len())
copy(newCtx, e.buf.Bytes())

putEvent(e)

return &Handler{
fmtr: h.fmtr,
w: h.w,
isDiscard: h.isDiscard,
lvl: h.lvl,
ctx: newCtx,
prefix: h.prefix,
openGroups: h.openGroups,
}
}

// WithGroup returns a new Handler with name appended to the group stack.
// An empty name is a no-op per the slog spec.
func (h *Handler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}

e := newEvent(h.fmtr)
e.buf.Write(h.ctx)
e.prefix = append(e.prefix, h.prefix...)

e.prefix = h.fmtr.AppendGroupStart(e.buf, e.prefix, name)

newCtx := make([]byte, e.buf.Len())
copy(newCtx, e.buf.Bytes())
newPrefix := make([]byte, len(e.prefix))
copy(newPrefix, e.prefix)

putEvent(e)

return &Handler{
fmtr: h.fmtr,
w: h.w,
isDiscard: h.isDiscard,
lvl: h.lvl,
ctx: newCtx,
prefix: newPrefix,
openGroups: h.openGroups + 1,
}
}

func appendAttr(e *Event, a slog.Attr) {
a.Value = a.Value.Resolve()
if a.Equal(slog.Attr{}) {
return
}

switch a.Value.Kind() {
case slog.KindGroup:
appendGroup(e, a.Key, a.Value)
case slog.KindString:
e.AppendString(a.Key, a.Value.String())
case slog.KindInt64:
e.AppendInt(a.Key, a.Value.Int64())
case slog.KindUint64:
e.AppendUint(a.Key, a.Value.Uint64())
case slog.KindFloat64:
e.AppendFloat(a.Key, a.Value.Float64())
case slog.KindBool:
e.AppendBool(a.Key, a.Value.Bool())
case slog.KindTime:
e.AppendTime(a.Key, a.Value.Time())
case slog.KindDuration:
e.AppendDuration(a.Key, a.Value.Duration())
default:
e.AppendInterface(a.Key, a.Value.Any())
}
}

func appendGroup(e *Event, name string, val slog.Value) {
subs := val.Group()
if len(subs) == 0 {
return
}

if name == "" {
// Per the slog spec, an anonymous group is flattened into the
// enclosing scope.
for _, a := range subs {
appendAttr(e, a)
}
return
}

e.OpenGroup(name)
for _, a := range subs {
appendAttr(e, a)
}
e.CloseGroup()
}

// mapSlogLevel maps to logger.Level. Custom levels below Debug clamp to
// Debug; above Error clamp to Error. Trace and Crit are never produced.
func mapSlogLevel(lvl slog.Level) Level {
switch {
case lvl >= slog.LevelError:
return Error
case lvl >= slog.LevelWarn:
return Warn
case lvl >= slog.LevelInfo:
return Info
default:
return Debug
}
}
Loading
Loading