Skip to content

Commit

Permalink
feat(logger): implement slog.Handler (#13)
Browse files Browse the repository at this point in the history
* feat(logger): implement slog.Handler

This implements slog.Handler interface. You can use Log as an slog
handler.

* feat: compile for go1.20 and go1.21

* refactor: match log/slog level values

* feat: add slog tests

* chore: downgrade the minimum go version to go1.19

* doc: add slog support

* refactor: rename files

* fix: add emoji test

* chore!: change default timestamp and level key names

* fix: lint
  • Loading branch information
aymanbagabas authored Nov 7, 2023
1 parent d96bea2 commit de79c17
Show file tree
Hide file tree
Showing 21 changed files with 454 additions and 108 deletions.
3 changes: 1 addition & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ linters:
- goimports
- goprintffuncname
- gosec
- ifshort
- misspell
- nolintlint
- prealloc
Expand All @@ -32,4 +31,4 @@ linters:
- sqlclosecheck
- unconvert
- unparam
- whitespace
- whitespace
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ readable logging with batteries included.
- Leveled logging.
- Text, JSON, and Logfmt formatters.
- Store and retrieve logger in and from context.
- Slog handler.
- Standard log adapter.

## Usage
Expand Down Expand Up @@ -306,6 +307,17 @@ startOven(400) // INFO <cookies/oven.go:123> Starting oven degree=400
This will use the _caller_ function (`startOven`) line number instead of the
logging function (`log.Info`) to report the source location.

### Slog Handler

You can use Log as an [`log/slog`](https://pkg.go.dev/log/slog) handler. Just
pass a logger instance to Slog and you're good to go.

```go
handler := log.New(os.Stderr)
logger := slog.New(handler)
logger.Error("meow?")
```

### Standard Log Adapter

Some Go libraries, especially the ones in the standard library, will only accept
Expand Down
4 changes: 2 additions & 2 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package log
import (
"bytes"
"context"
"io/ioutil"
"io"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -14,7 +14,7 @@ func TestLogContext_empty(t *testing.T) {
}

func TestLogContext_simple(t *testing.T) {
l := New(ioutil.Discard)
l := New(io.Discard)
ctx := WithContext(context.Background(), l)
require.Equal(t, l, FromContext(ctx))
}
Expand Down
4 changes: 2 additions & 2 deletions formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ const (

var (
// TimestampKey is the key for the timestamp.
TimestampKey = "ts"
TimestampKey = "time"
// MessageKey is the key for the message.
MessageKey = "msg"
// LevelKey is the key for the level.
LevelKey = "lvl"
LevelKey = "level"
// CallerKey is the key for the caller.
CallerKey = "caller"
// PrefixKey is the key for the prefix.
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
module github.com/charmbracelet/log

go 1.17
go 1.19

require (
github.com/charmbracelet/lipgloss v0.9.1
github.com/go-logfmt/logfmt v0.6.0
github.com/muesli/termenv v0.15.2
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
)

require (
Expand All @@ -18,6 +19,6 @@ require (
github.com/muesli/reflow v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
15 changes: 4 additions & 11 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
Expand All @@ -12,7 +11,6 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
Expand All @@ -24,19 +22,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
30 changes: 15 additions & 15 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestJson(t *testing.T) {
}{
{
name: "default logger info with timestamp",
expected: "{\"lvl\":\"info\",\"msg\":\"info\"}\n",
expected: "{\"level\":\"info\",\"msg\":\"info\"}\n",
msg: "info",
kvs: nil,
f: l.Info,
Expand All @@ -38,77 +38,77 @@ func TestJson(t *testing.T) {
},
{
name: "default logger error with timestamp",
expected: "{\"lvl\":\"error\",\"msg\":\"info\"}\n",
expected: "{\"level\":\"error\",\"msg\":\"info\"}\n",
msg: "info",
kvs: nil,
f: l.Error,
},
{
name: "multiline message",
expected: "{\"lvl\":\"error\",\"msg\":\"info\\ninfo\"}\n",
expected: "{\"level\":\"error\",\"msg\":\"info\\ninfo\"}\n",
msg: "info\ninfo",
kvs: nil,
f: l.Error,
},
{
name: "multiline kvs",
expected: "{\"lvl\":\"error\",\"msg\":\"info\",\"multiline\":\"info\\ninfo\"}\n",
expected: "{\"level\":\"error\",\"msg\":\"info\",\"multiline\":\"info\\ninfo\"}\n",
msg: "info",
kvs: []interface{}{"multiline", "info\ninfo"},
f: l.Error,
},
{
name: "odd number of kvs",
expected: "{\"baz\":\"missing value\",\"foo\":\"bar\",\"lvl\":\"error\",\"msg\":\"info\"}\n",
expected: "{\"baz\":\"missing value\",\"foo\":\"bar\",\"level\":\"error\",\"msg\":\"info\"}\n",
msg: "info",
kvs: []interface{}{"foo", "bar", "baz"},
f: l.Error,
},
{
name: "error field",
expected: "{\"error\":\"error message\",\"lvl\":\"error\",\"msg\":\"info\"}\n",
expected: "{\"error\":\"error message\",\"level\":\"error\",\"msg\":\"info\"}\n",
msg: "info",
kvs: []interface{}{"error", errors.New("error message")},
f: l.Error,
},
{
name: "struct field",
expected: "{\"lvl\":\"info\",\"msg\":\"info\",\"struct\":{}}\n",
expected: "{\"level\":\"info\",\"msg\":\"info\",\"struct\":{}}\n",
msg: "info",
kvs: []interface{}{"struct", struct{ foo string }{foo: "bar"}},
f: l.Info,
},
{
name: "slice field",
expected: "{\"lvl\":\"info\",\"msg\":\"info\",\"slice\":[1,2,3]}\n",
expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[1,2,3]}\n",
msg: "info",
kvs: []interface{}{"slice", []int{1, 2, 3}},
f: l.Info,
},
{
name: "slice of structs",
expected: "{\"lvl\":\"info\",\"msg\":\"info\",\"slice\":[{},{}]}\n",
expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[{},{}]}\n",
msg: "info",
kvs: []interface{}{"slice", []struct{ foo string }{{foo: "bar"}, {foo: "baz"}}},
f: l.Info,
},
{
name: "slice of strings",
expected: "{\"lvl\":\"info\",\"msg\":\"info\",\"slice\":[\"foo\",\"bar\"]}\n",
expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[\"foo\",\"bar\"]}\n",
msg: "info",
kvs: []interface{}{"slice", []string{"foo", "bar"}},
f: l.Info,
},
{
name: "slice of errors",
expected: "{\"lvl\":\"info\",\"msg\":\"info\",\"slice\":[{},{}]}\n",
expected: "{\"level\":\"info\",\"msg\":\"info\",\"slice\":[{},{}]}\n",
msg: "info",
kvs: []interface{}{"slice", []error{errors.New("error message1"), errors.New("error message2")}},
f: l.Info,
},
{
name: "map of strings",
expected: "{\"lvl\":\"info\",\"map\":{\"a\":\"b\",\"foo\":\"bar\"},\"msg\":\"info\"}\n",
expected: "{\"level\":\"info\",\"map\":{\"a\":\"b\",\"foo\":\"bar\"},\"msg\":\"info\"}\n",
msg: "info",
kvs: []interface{}{"map", map[string]string{"a": "b", "foo": "bar"}},
f: l.Info,
Expand Down Expand Up @@ -140,14 +140,14 @@ func TestJsonCaller(t *testing.T) {
}{
{
name: "simple caller",
expected: fmt.Sprintf("{\"caller\":\"log/%s:%d\",\"lvl\":\"info\",\"msg\":\"info\"}\n", filepath.Base(file), line+30),
expected: fmt.Sprintf("{\"caller\":\"log/%s:%d\",\"level\":\"info\",\"msg\":\"info\"}\n", filepath.Base(file), line+30),
msg: "info",
kvs: nil,
f: l.Info,
},
{
name: "nested caller",
expected: fmt.Sprintf("{\"caller\":\"log/%s:%d\",\"lvl\":\"info\",\"msg\":\"info\"}\n", filepath.Base(file), line+30),
expected: fmt.Sprintf("{\"caller\":\"log/%s:%d\",\"level\":\"info\",\"msg\":\"info\"}\n", filepath.Base(file), line+30),
msg: "info",
kvs: nil,
f: func(msg interface{}, kvs ...interface{}) {
Expand Down Expand Up @@ -177,5 +177,5 @@ func TestJsonCustomKey(t *testing.T) {
logger.SetFormatter(JSONFormatter)
logger.SetReportTimestamp(true)
logger.Info("info")
require.Equal(t, "{\"lvl\":\"info\",\"msg\":\"info\",\"time\":\"0001/01/01 00:00:00\"}\n", buf.String())
require.Equal(t, "{\"level\":\"info\",\"msg\":\"info\",\"time\":\"0002/01/01 00:00:00\"}\n", buf.String())
}
13 changes: 7 additions & 6 deletions level.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package log
import (
"errors"
"fmt"
"math"
"strings"
)

Expand All @@ -11,17 +12,17 @@ type Level int32

const (
// DebugLevel is the debug level.
DebugLevel Level = iota - 1
DebugLevel Level = -4
// InfoLevel is the info level.
InfoLevel
InfoLevel Level = 0
// WarnLevel is the warn level.
WarnLevel
WarnLevel Level = 4
// ErrorLevel is the error level.
ErrorLevel
ErrorLevel Level = 8
// FatalLevel is the fatal level.
FatalLevel
FatalLevel Level = 12
// noLevel is used with log.Print.
noLevel
noLevel Level = math.MaxInt32
)

// String returns the string representation of the level.
Expand Down
15 changes: 15 additions & 0 deletions level_121.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build go1.21
// +build go1.21

package log

import "log/slog"

// fromSlogLevel converts slog.Level to log.Level.
var fromSlogLevel = map[slog.Level]Level{
slog.LevelDebug: DebugLevel,
slog.LevelInfo: InfoLevel,
slog.LevelWarn: WarnLevel,
slog.LevelError: ErrorLevel,
slog.Level(12): FatalLevel,
}
15 changes: 15 additions & 0 deletions level_no121.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build !go1.21
// +build !go1.21

package log

import "golang.org/x/exp/slog"

// fromSlogLevel converts slog.Level to log.Level.
var fromSlogLevel = map[slog.Level]Level{
slog.LevelDebug: DebugLevel,
slog.LevelInfo: InfoLevel,
slog.LevelWarn: WarnLevel,
slog.LevelError: ErrorLevel,
slog.Level(12): FatalLevel,
}
Loading

0 comments on commit de79c17

Please sign in to comment.