mirror of
https://github.com/chainguard-dev/clog
synced 2026-07-06 22:12:46 +00:00
Add context logger.
This is heavily inspired by https://pkg.go.dev/knative.dev/pkg/logging Also adds top level logging methods for convinience.
This commit is contained in:
parent
81392732b8
commit
5565dbc976
8 changed files with 389 additions and 30 deletions
33
README.md
33
README.md
|
|
@ -4,6 +4,37 @@ Context aware slog
|
|||
|
||||
## Usage
|
||||
|
||||
### Context Logger
|
||||
|
||||
The context Logger can be used to use Loggers from the context. This is
|
||||
sometimes preferred over the [Context Handler](#context-handler), since this can
|
||||
make it easier to use different loggers in different contexts (e.g. testing).
|
||||
|
||||
This approach is heavily inspired by
|
||||
[knative.dev/pkg/logging](https://pkg.go.dev/knative.dev/pkg/logging)
|
||||
|
||||
```go
|
||||
func main() {
|
||||
log := slogctx.New(slog.Default).With("a", "b")
|
||||
ctx := slogctx.WithLogger(log)
|
||||
|
||||
// Grab logger from context and use
|
||||
slogctx.FromContext(ctx).With("foo", "bar").Infof("hello world")
|
||||
|
||||
// Package level context loggers are also aware
|
||||
slogctx.ErrorContext(ctx, "asdf")
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
2023/12/12 18:27:27 INFO hello world a=b foo=bar
|
||||
2023/12/12 18:27:27 ERROR asdf a=b
|
||||
```
|
||||
|
||||
### Context Handler
|
||||
|
||||
The context Handler can be used to insert values from the context.
|
||||
|
||||
```go
|
||||
func init() {
|
||||
slog.SetDefault(slog.New(slogctx.NewHandler(slog.NewTextHandler(os.Stdout, nil))))
|
||||
|
|
@ -11,7 +42,7 @@ func init() {
|
|||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
ctx = slogctx.With(ctx, "foo", "bar")
|
||||
ctx = slogctx.WithValue(ctx, "foo", "bar")
|
||||
|
||||
// Use slog package directly
|
||||
slog.InfoContext(ctx, "hello world", slog.Bool("baz", true))
|
||||
|
|
|
|||
|
|
@ -14,11 +14,15 @@ func init() {
|
|||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
ctx = slogctx.With(ctx, "foo", "bar")
|
||||
ctx = slogctx.WithValues(ctx, "foo", "bar")
|
||||
|
||||
// Use slog package directly
|
||||
slog.InfoContext(ctx, "hello world", slog.Bool("baz", true))
|
||||
|
||||
// glog / zap style (note: can't pass additional attributes)
|
||||
slogctx.Errorf(ctx, "hello %s", "world")
|
||||
slogctx.Errorf("hello %s", "world")
|
||||
|
||||
log := slogctx.DefaultLogger()
|
||||
log.With("foo", "bar").Infof("hello %s", "world")
|
||||
log.Info("hello world", slog.Bool("baz", true))
|
||||
}
|
||||
19
examples/logger/main.go
Normal file
19
examples/logger/main.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/wlynch/slogctx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log := slogctx.NewLogger(slog.Default()).With("a", "b")
|
||||
ctx := slogctx.WithLogger(context.Background(), log)
|
||||
|
||||
// Grab logger from context and use
|
||||
slogctx.FromContext(ctx).With("foo", "bar").Infof("hello world")
|
||||
|
||||
// Package level context loggers are also aware
|
||||
slogctx.ErrorContext(ctx, "asdf")
|
||||
}
|
||||
|
|
@ -13,10 +13,10 @@ type ctxVal map[string]any
|
|||
|
||||
// With returns a new context with the given values.
|
||||
// Values are expected to be key-value pairs, where the key is a string.
|
||||
// e.g. With(ctx, "foo", "bar", "baz", 1)
|
||||
// e.g. WithValues(ctx, "foo", "bar", "baz", 1)
|
||||
// If a value already exists, it is overwritten.
|
||||
// If an odd number of arguments are provided, With panics.
|
||||
func With(ctx context.Context, args ...any) context.Context {
|
||||
func WithValues(ctx context.Context, args ...any) context.Context {
|
||||
if len(args)%2 != 0 {
|
||||
panic("non-even number of arguments")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,30 +3,51 @@ package slogctx
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContextHandler(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = With(ctx, "foo", "bar")
|
||||
ctx2 := With(ctx,
|
||||
ctx = WithValues(ctx, "foo", "bar")
|
||||
ctx2 := WithValues(ctx,
|
||||
"a", "b",
|
||||
"c", "d",
|
||||
)
|
||||
ctx = With(ctx, "b", 1)
|
||||
ctx = WithValues(ctx, "b", 1)
|
||||
|
||||
for _, tc := range []struct {
|
||||
ctx context.Context
|
||||
want string
|
||||
want map[string]any
|
||||
}{
|
||||
{ctx, "foo=bar b=1"},
|
||||
{ctx2, "foo=bar a=b c=d"},
|
||||
{ctx, map[string]any{
|
||||
"b": float64(1),
|
||||
"foo": "bar",
|
||||
}},
|
||||
{ctx2, map[string]any{
|
||||
"foo": "bar",
|
||||
"a": "b",
|
||||
"c": "d",
|
||||
}},
|
||||
} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
log := slog.New(NewHandler(slog.NewTextHandler(b, nil)))
|
||||
log.InfoContext(tc.ctx, "hello world")
|
||||
log := slog.New(NewHandler(slog.NewJSONHandler(b, testopts)))
|
||||
log.InfoContext(tc.ctx, "")
|
||||
|
||||
tc.want["level"] = "INFO"
|
||||
tc.want["msg"] = ""
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(b.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.want, got) {
|
||||
t.Errorf("want %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
86
log.go
86
log.go
|
|
@ -2,38 +2,90 @@ package slogctx
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func Info(ctx context.Context, args ...any) {
|
||||
slog.InfoContext(ctx, fmt.Sprint(args...))
|
||||
// Info calls Info on the default logger.
|
||||
func Info(msg string, args ...any) {
|
||||
wrap(context.Background(), DefaultLogger(), slog.LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
func Infof(ctx context.Context, msg string, args ...any) {
|
||||
slog.InfoContext(ctx, fmt.Sprintf(msg, args...))
|
||||
// InfoContext calls InfoContext on the context logger.
|
||||
// If a Logger is found in the context, it will be used.
|
||||
func InfoContext(ctx context.Context, msg string, args ...any) {
|
||||
wrap(ctx, FromContext(ctx), slog.LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
func Warn(ctx context.Context, args ...any) {
|
||||
slog.WarnContext(ctx, fmt.Sprint(args...))
|
||||
// Infof calls Infof on the default logger.
|
||||
func Infof(format string, args ...any) {
|
||||
wrapf(context.Background(), DefaultLogger(), slog.LevelInfo, format, args...)
|
||||
}
|
||||
|
||||
func Warnf(ctx context.Context, msg string, args ...any) {
|
||||
slog.WarnContext(ctx, fmt.Sprintf(msg, args...))
|
||||
// InfoContextf calls InfoContextf on the context logger.
|
||||
// If a Logger is found in the context, it will be used.
|
||||
func InfoContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, FromContext(ctx), slog.LevelInfo, format, args...)
|
||||
}
|
||||
|
||||
func Error(ctx context.Context, args ...any) {
|
||||
slog.ErrorContext(ctx, fmt.Sprint(args...))
|
||||
// Warn calls Warn on the default logger.
|
||||
func Warn(msg string, args ...any) {
|
||||
wrap(context.Background(), DefaultLogger(), slog.LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
func Errorf(ctx context.Context, msg string, args ...any) {
|
||||
slog.ErrorContext(ctx, fmt.Sprintf(msg, args...))
|
||||
// WarnContext calls WarnContext on the context logger.
|
||||
// If a Logger is found in the context, it will be used.
|
||||
func WarnContext(ctx context.Context, msg string, args ...any) {
|
||||
wrap(ctx, FromContext(ctx), slog.LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
func Debug(ctx context.Context, args ...any) {
|
||||
slog.DebugContext(ctx, fmt.Sprint(args...))
|
||||
// Warnf calls Warnf on the default logger.
|
||||
func Warnf(format string, args ...any) {
|
||||
wrapf(context.Background(), DefaultLogger(), slog.LevelWarn, format, args...)
|
||||
}
|
||||
|
||||
func Debugf(ctx context.Context, msg string, args ...any) {
|
||||
slog.DebugContext(ctx, fmt.Sprintf(msg, args...))
|
||||
// WarnContextf calls WarnContextf on the context logger.
|
||||
// If a Logger is found in the context, it will be used.
|
||||
func WarnContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, FromContext(ctx), slog.LevelWarn, format, args...)
|
||||
}
|
||||
|
||||
// Error calls Error on the default logger.
|
||||
func Error(msg string, args ...any) {
|
||||
wrap(context.Background(), DefaultLogger(), slog.LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// ErrorContext calls ErrorContext on the context logger.
|
||||
func ErrorContext(ctx context.Context, msg string, args ...any) {
|
||||
wrap(ctx, FromContext(ctx), slog.LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// Errorf calls Errorf on the default logger.
|
||||
func Errorf(format string, args ...any) {
|
||||
wrapf(context.Background(), DefaultLogger(), slog.LevelError, format, args...)
|
||||
}
|
||||
|
||||
// ErrorContextf calls ErrorContextf on the context logger.
|
||||
func ErrorContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, FromContext(ctx), slog.LevelError, format, args...)
|
||||
}
|
||||
|
||||
// Debug calls Debug on the default logger.
|
||||
func Debug(msg string, args ...any) {
|
||||
wrap(context.Background(), DefaultLogger(), slog.LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// DebugContext calls DebugContext on the context logger.
|
||||
func DebugContext(ctx context.Context, msg string, args ...any) {
|
||||
wrap(ctx, FromContext(ctx), slog.LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// Debugf calls Debugf on the default logger.
|
||||
func Debugf(format string, args ...any) {
|
||||
wrapf(context.Background(), DefaultLogger(), slog.LevelDebug, format, args...)
|
||||
}
|
||||
|
||||
// DebugContextf calls DebugContextf on the context logger.
|
||||
// If a Logger is found in the context, it will be used.
|
||||
func DebugContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, FromContext(ctx), slog.LevelDebug, format, args...)
|
||||
}
|
||||
|
|
|
|||
117
logger.go
Normal file
117
logger.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package slogctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger implements a wrapper around [slog.Logger] that adds formatter functions (e.g. Infof, Errorf)
|
||||
type Logger struct {
|
||||
slog.Logger
|
||||
}
|
||||
|
||||
// DefaultLogger returns a new logger that uses the default [slog.Logger].
|
||||
func DefaultLogger() *Logger {
|
||||
return NewLogger(slog.Default())
|
||||
}
|
||||
|
||||
// NewLogger returns a new logger that wraps the given [slog.Logger].
|
||||
func NewLogger(l *slog.Logger) *Logger {
|
||||
if l == nil {
|
||||
l = slog.Default()
|
||||
}
|
||||
return &Logger{Logger: *l}
|
||||
}
|
||||
|
||||
// New returns a new logger that wraps the given [slog.Handler].
|
||||
func New(h slog.Handler) *Logger {
|
||||
return NewLogger(slog.New(h))
|
||||
}
|
||||
|
||||
// With calls [Logger.With] on the default logger.
|
||||
func With(args ...any) *Logger {
|
||||
return DefaultLogger().With(args...)
|
||||
}
|
||||
|
||||
// With calls [Logger.With] on the logger.
|
||||
func (l *Logger) With(args ...any) *Logger {
|
||||
return NewLogger(l.Logger.With(args...))
|
||||
}
|
||||
|
||||
// Infof logs at LevelInfo with the given format and arguments.
|
||||
func (l *Logger) Infof(format string, args ...any) {
|
||||
wrapf(context.Background(), l, slog.LevelInfo, format, args...)
|
||||
}
|
||||
|
||||
// InfoContextf logs at LevelInfo with the given context, format and arguments.
|
||||
func (l *Logger) InfoContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, l, slog.LevelInfo, format, args...)
|
||||
}
|
||||
|
||||
// Warnf logs at LevelWarn with the given format and arguments.
|
||||
func (l *Logger) Warnf(format string, args ...any) {
|
||||
wrapf(context.Background(), l, slog.LevelWarn, format, args...)
|
||||
}
|
||||
|
||||
// WarnContextf logs at LevelWarn with the given context, format and arguments.
|
||||
func (l *Logger) WarnContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, l, slog.LevelWarn, format, args...)
|
||||
}
|
||||
|
||||
// Errorf logs at LevelError with the given format and arguments.
|
||||
func (l *Logger) Errorf(format string, args ...any) {
|
||||
wrapf(context.Background(), l, slog.LevelError, format, args...)
|
||||
}
|
||||
|
||||
// ErrorContextf logs at LevelError with the given context, format and arguments.
|
||||
func (l *Logger) ErrorContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, l, slog.LevelError, format, args...)
|
||||
}
|
||||
|
||||
// Debugf logs at LevelDebug with the given format and arguments.
|
||||
func (l *Logger) Debugf(format string, args ...any) {
|
||||
wrapf(context.Background(), l, slog.LevelDebug, format, args...)
|
||||
}
|
||||
|
||||
// DebugContextf logs at LevelDebug with the given context, format and arguments.
|
||||
func (l *Logger) DebugContextf(ctx context.Context, format string, args ...any) {
|
||||
wrapf(ctx, l, slog.LevelDebug, format, args...)
|
||||
}
|
||||
|
||||
// Base returns the underlying [slog.Logger].
|
||||
func (l *Logger) Base() *slog.Logger {
|
||||
return &l.Logger
|
||||
}
|
||||
|
||||
func wrap(ctx context.Context, logger *Logger, level slog.Level, msg string, args ...any) {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(3, pcs[:]) // skip [Callers, Infof, wrapf]
|
||||
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
||||
r.Add(args...)
|
||||
_ = logger.Handler().Handle(ctx, r)
|
||||
}
|
||||
|
||||
// wrapf is like wrap, but uses fmt.Sprintf to format the message.
|
||||
// NOTE: args are passed to fmt.Sprintf, not as [slog.Attr].
|
||||
func wrapf(ctx context.Context, logger *Logger, level slog.Level, format string, args ...any) {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(3, pcs[:]) // skip [Callers, Infof, wrapf]
|
||||
r := slog.NewRecord(time.Now(), level, fmt.Sprintf(format, args...), pcs[0])
|
||||
_ = logger.Handler().Handle(ctx, r)
|
||||
}
|
||||
|
||||
type loggerKey struct{}
|
||||
|
||||
func WithLogger(ctx context.Context, logger *Logger) context.Context {
|
||||
return context.WithValue(ctx, loggerKey{}, logger)
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) *Logger {
|
||||
if logger, ok := ctx.Value(loggerKey{}).(*Logger); ok {
|
||||
return logger
|
||||
}
|
||||
return DefaultLogger()
|
||||
}
|
||||
115
logger_test.go
Normal file
115
logger_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package slogctx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
testopts = &slog.HandlerOptions{
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Ignore time to make testing easier.
|
||||
if a.Key == "time" {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
b := new(bytes.Buffer)
|
||||
base := slog.New(NewHandler(slog.NewJSONHandler(b, testopts)))
|
||||
log := NewLogger(base).With("a", "b")
|
||||
log.InfoContext(ctx, "")
|
||||
t.Log(b.String())
|
||||
|
||||
want := map[string]any{
|
||||
"level": "INFO",
|
||||
"msg": "",
|
||||
"a": "b",
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(b.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerNilBase(t *testing.T) {
|
||||
log := NewLogger(nil)
|
||||
log.Info("")
|
||||
}
|
||||
|
||||
func TestLoggerFromContext(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
base := slog.New(NewHandler(slog.NewJSONHandler(b, testopts)))
|
||||
log := NewLogger(base).With("a", "b")
|
||||
|
||||
ctx := WithLogger(context.Background(), log)
|
||||
FromContext(ctx).Info("")
|
||||
|
||||
want := map[string]any{
|
||||
"level": "INFO",
|
||||
"msg": "",
|
||||
"a": "b",
|
||||
}
|
||||
|
||||
t.Run("FromContext.Info", func(t *testing.T) {
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(b.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("want %v, got %v", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
b.Reset()
|
||||
|
||||
t.Run("slogctx.Info", func(t *testing.T) {
|
||||
InfoContext(ctx, "")
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(b.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("want %v, got %v", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoggerPC(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
log := NewLogger(slog.New(NewHandler(slog.NewJSONHandler(b, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
ReplaceAttr: testopts.ReplaceAttr,
|
||||
}))))
|
||||
|
||||
log.Info("")
|
||||
t.Log(b.String())
|
||||
|
||||
var got struct {
|
||||
Source struct {
|
||||
File string `json:"file"`
|
||||
Function string `json:"function"`
|
||||
} `json:"source"`
|
||||
}
|
||||
if err := json.Unmarshal(b.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Knowing that the PC is from this test is good enough.
|
||||
want := fmt.Sprintf("github.com/wlynch/slogctx.%s", t.Name())
|
||||
if got.Source.Function != want {
|
||||
t.Errorf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue