Skip to content

Commit

Permalink
Basic slog hook
Browse files Browse the repository at this point in the history
  • Loading branch information
flimzy committed Nov 9, 2023
1 parent dd1b4c2 commit e3f17e5
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 0 deletions.
70 changes: 70 additions & 0 deletions hooks/slog/slog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//go:build go1.21
// +build go1.21

package slog

import (
"log/slog"

"github.com/sirupsen/logrus"
)

// SlogHook sends logs to slog.
type SlogHook struct {
logger *slog.Logger
}

var _ logrus.Hook = (*SlogHook)(nil)

// NewSlogHook creates a hook that sends logs to an existing slog Logger.
// This hook is intended to be used during transition from Logrus to slog,
// or as a shim between different parts of your application or different
// libraries that depend on different loggers.
//
// Example usage:
//
// logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
// hook := NewSlogHook(logger)
func NewSlogHook(logger *slog.Logger) *SlogHook {
return &SlogHook{
logger: logger,
}
}

func (*SlogHook) toSlogLevel(level logrus.Level) slog.Level {
switch level {
case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel:
return slog.LevelError
case logrus.WarnLevel:
return slog.LevelWarn
case logrus.InfoLevel:
return slog.LevelInfo
case logrus.DebugLevel, logrus.TraceLevel:
return slog.LevelDebug
default:
// Treat all unknown levels as errors
return slog.LevelError
}
}

// Levels always returns all levels, since slog allows controlling level
// enabling based on context.
func (h *SlogHook) Levels() []logrus.Level {
return logrus.AllLevels
}

// Fire sends entry to the underlying slog logger. The Time and Caller fields
// of entry are ignored.
func (h *SlogHook) Fire(entry *logrus.Entry) error {
attrs := make([]interface{}, 0, len(entry.Data))
for k, v := range entry.Data {
attrs = append(attrs, slog.Any(k, v))
}
h.logger.Log(
entry.Context,
h.toSlogLevel(entry.Level),
entry.Message,
attrs...,
)
return nil
}
72 changes: 72 additions & 0 deletions hooks/slog/slog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//go:build go1.21
// +build go1.21

package slog

import (
"bytes"
"io"
"log/slog"
"strings"
"testing"

"github.com/sirupsen/logrus"
)

func TestSlogHook(t *testing.T) {
tests := []struct {
name string
fn func(*logrus.Logger)
want []string
}{
{
name: "defaults",
fn: func(log *logrus.Logger) {
log.Info("info")
},
want: []string{
"level=INFO msg=info",
},
},
{
name: "with fields",
fn: func(log *logrus.Logger) {
log.WithFields(logrus.Fields{
"chicken": "cluck",
}).Error("error")
},
want: []string{
"level=ERROR msg=error chicken=cluck",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
slogLogger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{
// Remove timestamps from logs, for easier comparison
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
log := logrus.New()
log.Out = io.Discard
log.AddHook(NewSlogHook(slogLogger))
tt.fn(log)
got := strings.Split(strings.TrimSpace(buf.String()), "\n")
if len(got) != len(tt.want) {
t.Errorf("Got %d log lines, expected %d", len(got), len(tt.want))
return
}
for i, line := range got {
if line != tt.want[i] {
t.Errorf("line %d differs from expectation.\n Got: %s\nWant: %s", i, line, tt.want[i])
}
}
})
}
}

0 comments on commit e3f17e5

Please sign in to comment.