Skip to content

Commit 78b8af5

Browse files
authored
Merge pull request #60 from pohly/slog
support slog
2 parents 6684601 + ae27dfc commit 78b8af5

File tree

9 files changed

+729
-31
lines changed

9 files changed

+729
-31
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ Zapr :zap:
22
==========
33

44
A [logr](https://github.com/go-logr/logr) implementation using
5-
[Zap](https://github.com/uber-go/zap).
5+
[Zap](https://github.com/uber-go/zap). Can also be used as
6+
[slog](https://pkg.go.dev/log/slog) handler.
67

78
Usage
89
-----
910

11+
Via logr:
12+
1013
```go
14+
package main
15+
1116
import (
1217
"fmt"
1318

@@ -29,6 +34,33 @@ func main() {
2934
}
3035
```
3136

37+
Via slog:
38+
39+
```
40+
package main
41+
42+
import (
43+
"fmt"
44+
"log/slog"
45+
46+
"github.com/go-logr/logr/slogr"
47+
"github.com/go-logr/zapr"
48+
"go.uber.org/zap"
49+
)
50+
51+
func main() {
52+
var log *slog.Logger
53+
54+
zapLog, err := zap.NewDevelopment()
55+
if err != nil {
56+
panic(fmt.Sprintf("who watches the watchmen (%v)?", err))
57+
}
58+
log = slog.New(slogr.NewSlogHandler(zapr.NewLogger(zapLog)))
59+
60+
log.Info("Logr in action!", "the answer", 42)
61+
}
62+
```
63+
3264
Increasing Verbosity
3365
--------------------
3466

@@ -68,3 +100,8 @@ For the most part, concepts in Zap correspond directly with those in logr.
68100
Unlike Zap, all fields *must* be in the form of sugared fields --
69101
it's illegal to pass a strongly-typed Zap field in a key position to any
70102
of the logging methods (`Log`, `Error`).
103+
104+
The zapr `logr.LogSink` implementation also implements `logr.SlogHandler`. That
105+
enables `slogr.NewSlogHandler` to provide a `slog.Handler` which just passes
106+
parameters through to zapr. zapr handles special slog values (Group,
107+
LogValuer), regardless of which front-end API is used.

slog_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
/*
5+
Copyright 2023 The logr Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package zapr_test
21+
22+
import (
23+
"bytes"
24+
"context"
25+
"encoding/json"
26+
"log/slog"
27+
"strings"
28+
"testing"
29+
"testing/slogtest"
30+
31+
"github.com/go-logr/logr/slogr"
32+
"github.com/go-logr/zapr"
33+
"github.com/stretchr/testify/require"
34+
"go.uber.org/zap"
35+
"go.uber.org/zap/zapcore"
36+
)
37+
38+
func TestSlogHandler(t *testing.T) {
39+
var buffer bytes.Buffer
40+
encoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
41+
MessageKey: slog.MessageKey,
42+
TimeKey: slog.TimeKey,
43+
LevelKey: slog.LevelKey,
44+
EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
45+
encoder.AppendInt(int(level))
46+
},
47+
})
48+
core := zapcore.NewCore(encoder, zapcore.AddSync(&buffer), zapcore.Level(0))
49+
zl := zap.New(core)
50+
logger := zapr.NewLogger(zl)
51+
handler := slogr.NewSlogHandler(logger)
52+
53+
err := slogtest.TestHandler(handler, func() []map[string]any {
54+
_ = zl.Sync()
55+
return parseOutput(t, buffer.Bytes())
56+
})
57+
t.Logf("Log output:\n%s\nAs JSON:\n%v\n", buffer.String(), parseOutput(t, buffer.Bytes()))
58+
// Correlating failures with individual test cases is hard with the current API.
59+
// See https://github.com/golang/go/issues/61758
60+
if err != nil {
61+
if err, ok := err.(interface {
62+
Unwrap() []error
63+
}); ok {
64+
for _, err := range err.Unwrap() {
65+
if !containsOne(err.Error(),
66+
"a Handler should ignore a zero Record.Time", // zapr always writes a time field.
67+
"a Handler should not output groups for an empty Record", // Relies on WithGroup and that always opens a group. Text may change, see https://go.dev/cl/516155
68+
) {
69+
t.Errorf("Unexpected error: %v", err)
70+
}
71+
}
72+
return
73+
}
74+
// Shouldn't be reached, errors from errors.Join can be split up.
75+
t.Errorf("Unexpected errors:\n%v", err)
76+
}
77+
}
78+
79+
func containsOne(hay string, needles ...string) bool {
80+
for _, needle := range needles {
81+
if strings.Contains(hay, needle) {
82+
return true
83+
}
84+
}
85+
return false
86+
}
87+
88+
// TestSlogCases covers some gaps in the coverage we get from
89+
// slogtest.TestHandler (empty and invalud PC, see
90+
// https://github.com/golang/go/issues/62280) and verbosity handling in
91+
// combination with V().
92+
func TestSlogCases(t *testing.T) {
93+
for name, tc := range map[string]struct {
94+
record slog.Record
95+
v int
96+
expected string
97+
}{
98+
"empty": {
99+
expected: `{"msg":"", "level":"info", "v":0}`,
100+
},
101+
"invalid-pc": {
102+
record: slog.Record{PC: 1},
103+
expected: `{"msg":"", "level":"info", "v":0}`,
104+
},
105+
"debug": {
106+
record: slog.Record{Level: slog.LevelDebug},
107+
expected: `{"msg":"", "level":"Level(-4)", "v":4}`,
108+
},
109+
"warn": {
110+
record: slog.Record{Level: slog.LevelWarn},
111+
expected: `{"msg":"", "level":"warn", "v":0}`,
112+
},
113+
"error": {
114+
record: slog.Record{Level: slog.LevelError},
115+
expected: `{"msg":"", "level":"error"}`,
116+
},
117+
"debug-v1": {
118+
v: 1,
119+
record: slog.Record{Level: slog.LevelDebug},
120+
expected: `{"msg":"", "level":"Level(-5)", "v":5}`,
121+
},
122+
"warn-v1": {
123+
v: 1,
124+
record: slog.Record{Level: slog.LevelWarn},
125+
expected: `{"msg":"", "level":"info", "v":0}`,
126+
},
127+
"error-v1": {
128+
v: 1,
129+
record: slog.Record{Level: slog.LevelError},
130+
expected: `{"msg":"", "level":"error"}`,
131+
},
132+
"debug-v4": {
133+
v: 4,
134+
record: slog.Record{Level: slog.LevelDebug},
135+
expected: `{"msg":"", "level":"Level(-8)", "v":8}`,
136+
},
137+
"warn-v4": {
138+
v: 4,
139+
record: slog.Record{Level: slog.LevelWarn},
140+
expected: `{"msg":"", "level":"info", "v":0}`,
141+
},
142+
"error-v4": {
143+
v: 4,
144+
record: slog.Record{Level: slog.LevelError},
145+
expected: `{"msg":"", "level":"error"}`,
146+
},
147+
} {
148+
t.Run(name, func(t *testing.T) {
149+
var buffer bytes.Buffer
150+
encoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
151+
MessageKey: slog.MessageKey,
152+
LevelKey: slog.LevelKey,
153+
EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
154+
encoder.AppendString(level.String())
155+
},
156+
})
157+
core := zapcore.NewCore(encoder, zapcore.AddSync(&buffer), zapcore.Level(-10))
158+
zl := zap.New(core)
159+
logger := zapr.NewLoggerWithOptions(zl, zapr.LogInfoLevel("v"))
160+
handler := slogr.NewSlogHandler(logger.V(tc.v))
161+
require.NoError(t, handler.Handle(context.Background(), tc.record))
162+
_ = zl.Sync()
163+
require.JSONEq(t, tc.expected, buffer.String())
164+
})
165+
}
166+
}
167+
168+
func parseOutput(t *testing.T, output []byte) []map[string]any {
169+
var ms []map[string]any
170+
for _, line := range bytes.Split(output, []byte{'\n'}) {
171+
if len(line) == 0 {
172+
continue
173+
}
174+
var m map[string]any
175+
if err := json.Unmarshal(line, &m); err != nil {
176+
t.Fatal(err)
177+
}
178+
ms = append(ms, m)
179+
}
180+
return ms
181+
}

0 commit comments

Comments
 (0)