Skip to content

Commit cb93271

Browse files
authored
feat: Add Zap logger formatter with an example. (#97)
Signed-off-by: Murat Mirgün Ercan <[email protected]>
1 parent 2abf8f2 commit cb93271

File tree

9 files changed

+362
-0
lines changed

9 files changed

+362
-0
lines changed

examples/zap/example.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"time"
7+
8+
"github.com/samber/oops"
9+
oopszap "github.com/samber/oops/loggers/zap"
10+
"go.uber.org/zap"
11+
"go.uber.org/zap/zapcore"
12+
)
13+
14+
// go run examples/zap/example.go
15+
16+
func d() error {
17+
req, _ := http.NewRequest("POST", "http://localhost:1337/foobar", strings.NewReader("hello world"))
18+
19+
return oops.
20+
Code("iam_authz_missing_permission").
21+
In("authz").
22+
Time(time.Now()).
23+
With("user_id", 1234).
24+
With("permission", "post.create").
25+
Hint("Runbook: https://doc.acme.org/doc/abcd.md").
26+
User("user-123", "firstname", "john", "lastname", "doe").
27+
Request(req, true).
28+
Errorf("permission denied")
29+
}
30+
31+
func c() error {
32+
return d()
33+
}
34+
35+
func b() error {
36+
return oops.
37+
In("iam").
38+
Trace("6710668a-2b2a-4de6-b8cf-3272a476a1c9").
39+
With("hello", "world").
40+
Wrapf(c(), "something failed")
41+
}
42+
43+
func a() error {
44+
return b()
45+
}
46+
47+
func main() {
48+
config := zap.NewProductionConfig()
49+
config.EncoderConfig.TimeKey = "timestamp"
50+
config.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder
51+
logger, _ := config.Build()
52+
defer logger.Sync()
53+
54+
err := a()
55+
56+
logger.Error(err.Error(),
57+
zap.Object("error", oopszap.OopsMarshalFunc(err)),
58+
zap.String("stacktrace", oopszap.OopsStackMarshaller(err)),
59+
)
60+
}

examples/zap/go.mod

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module github.com/samber/oops/examples/zap
2+
3+
go 1.21
4+
5+
replace (
6+
github.com/samber/oops => ../..
7+
github.com/samber/oops/loggers/zap => ../../loggers/zap
8+
)
9+
10+
require (
11+
github.com/samber/oops v0.0.0
12+
github.com/samber/oops/loggers/zap v0.0.0
13+
go.uber.org/zap v1.26.0
14+
)
15+
16+
require (
17+
github.com/oklog/ulid/v2 v2.1.1 // indirect
18+
github.com/samber/lo v1.52.0 // indirect
19+
go.opentelemetry.io/otel v1.29.0 // indirect
20+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
21+
go.uber.org/multierr v1.10.0 // indirect
22+
golang.org/x/text v0.22.0 // indirect
23+
)

examples/zap/go.sum

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
4+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5+
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
6+
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
7+
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
8+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
9+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10+
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
11+
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
12+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
13+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
14+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
15+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
16+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
17+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
18+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
19+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
20+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
21+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
22+
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
23+
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
24+
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
25+
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
26+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
27+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

go.work

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ use (
1010
./examples/segfault
1111
./examples/slog
1212
./examples/sources
13+
./examples/zap
1314
./examples/zerolog
1415

1516
// logger formatters
1617
./loggers/logrus
18+
./loggers/zap
1719
./loggers/zerolog
1820

1921
// recovery middlewares

loggers/zap/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Zap formatter for Oops
2+
3+
```go
4+
import "go.uber.org/zap"
5+
import oopszap "github.com/samber/oops/loggers/zap"
6+
7+
func main() {
8+
logger, _ := zap.NewProduction()
9+
defer logger.Sync()
10+
11+
err := oops.
12+
With("driver", "postgresql").
13+
With("query", "SELECT * FROM users").
14+
Errorf("could not fetch user")
15+
16+
if err != nil {
17+
logger.Error(err.Error(),
18+
zap.Object("error", oopszap.OopsMarshalFunc(err)),
19+
zap.String("stacktrace", oopszap.OopsStackMarshaller(err)),
20+
)
21+
}
22+
}
23+
```

loggers/zap/formatter.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package oopszap
2+
3+
import (
4+
"github.com/samber/oops"
5+
"go.uber.org/zap/zapcore"
6+
)
7+
8+
// OopsStackMarshaller returns the stack trace string for use in zap.
9+
// Usage: zap.String("stacktrace", oopszap.OopsStackMarshaller(err))
10+
func OopsStackMarshaller(err error) string {
11+
if typedErr, ok := oops.AsOops(err); ok {
12+
return typedErr.Stacktrace()
13+
}
14+
// For normal errors, we might not want to return anything or just empty string,
15+
// but to be safe/useful let's return nothing or handle it at call site.
16+
// Actually, matching zerolog implementation logic:
17+
return ""
18+
}
19+
20+
// OopsMarshalFunc returns a zapcore.ObjectMarshaler that logs the error details.
21+
// Usage: zap.Object("error", oopszap.OopsMarshalFunc(err))
22+
func OopsMarshalFunc(err error) zapcore.ObjectMarshaler {
23+
if typedErr, ok := oops.AsOops(err); ok {
24+
return &zapErrorMarshaller{err: typedErr}
25+
}
26+
return &simpleErrorMarshaller{err: err}
27+
}
28+
29+
type simpleErrorMarshaller struct {
30+
err error
31+
}
32+
33+
func (m *simpleErrorMarshaller) MarshalLogObject(enc zapcore.ObjectEncoder) error {
34+
if m.err != nil {
35+
enc.AddString("message", m.err.Error())
36+
}
37+
return nil
38+
}
39+
40+
type zapErrorMarshaller struct {
41+
err oops.OopsError
42+
}
43+
44+
func (m *zapErrorMarshaller) MarshalLogObject(enc zapcore.ObjectEncoder) error {
45+
payload := m.err.ToMap()
46+
47+
for k, v := range payload {
48+
switch k {
49+
case "stacktrace":
50+
// Skip stacktrace in the main object - handled separately if desired
51+
case "context":
52+
if contextMap, ok := v.(map[string]any); ok && len(contextMap) > 0 {
53+
enc.AddObject(k, zapcore.ObjectMarshalerFunc(func(innerEnc zapcore.ObjectEncoder) error {
54+
for ctxK, ctxV := range contextMap {
55+
if errVal, ok := ctxV.(error); ok {
56+
innerEnc.AddString(ctxK, errVal.Error())
57+
} else {
58+
innerEnc.AddReflected(ctxK, ctxV)
59+
}
60+
}
61+
return nil
62+
}))
63+
}
64+
default:
65+
enc.AddReflected(k, v)
66+
}
67+
}
68+
return nil
69+
}

loggers/zap/formatter_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package oopszap
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/samber/oops"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"go.uber.org/zap"
12+
"go.uber.org/zap/zapcore"
13+
)
14+
15+
type jsonLogEntryError struct {
16+
Error string `json:"error"`
17+
Time string `json:"time"`
18+
Domain string `json:"domain"`
19+
Trace string `json:"trace"`
20+
Context map[string]any `json:"context"`
21+
}
22+
23+
type jsonLogEntry struct {
24+
Level string `json:"level"`
25+
Stacktrace string `json:"stacktrace"`
26+
Message string `json:"msg"`
27+
Error jsonLogEntryError `json:"error"`
28+
}
29+
30+
func TestZapFormatter(t *testing.T) {
31+
is := assert.New(t)
32+
33+
// Setup Zap logger buffer
34+
buffer := &bytes.Buffer{}
35+
encoderConfig := zap.NewProductionEncoderConfig()
36+
encoderConfig.TimeKey = "" // disable timestamp for easier testing or keep it? zerolog test kept it but cleared it.
37+
// We want to test the payload structure.
38+
39+
core := zapcore.NewCore(
40+
zapcore.NewJSONEncoder(encoderConfig),
41+
zapcore.AddSync(buffer),
42+
zap.ErrorLevel,
43+
)
44+
logger := zap.New(core)
45+
46+
err := oops.
47+
In("test").
48+
With("driver", "postgresql").
49+
Errorf("could not fetch user")
50+
51+
logger.Error("something went wrong",
52+
zap.Object("error", OopsMarshalFunc(err)),
53+
zap.String("stacktrace", OopsStackMarshaller(err)),
54+
)
55+
56+
var loggedError jsonLogEntry
57+
decErr := json.Unmarshal(buffer.Bytes(), &loggedError)
58+
require.NoError(t, decErr)
59+
60+
// Assertions
61+
is.Contains(loggedError.Stacktrace, "Oops: could not fetch user\n --- at ")
62+
is.NotEmpty(loggedError.Error.Time)
63+
is.NotEmpty(loggedError.Error.Trace)
64+
65+
// Clear dynamic fields for equality check
66+
loggedError.Stacktrace = ""
67+
loggedError.Error.Time = ""
68+
loggedError.Error.Trace = ""
69+
70+
expected := jsonLogEntry{
71+
Level: "error",
72+
Message: "something went wrong",
73+
Error: jsonLogEntryError{
74+
Error: "could not fetch user",
75+
Domain: "test",
76+
Context: map[string]any{
77+
"driver": "postgresql",
78+
},
79+
},
80+
}
81+
is.Equal(expected, loggedError)
82+
}
83+
84+
func BenchmarkZapFormatter(b *testing.B) {
85+
err := oops.
86+
In("test").
87+
With("driver", "postgresql").
88+
Errorf("could not fetch user")
89+
90+
// Discard output
91+
core := zapcore.NewCore(
92+
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
93+
zapcore.AddSync(bytes.NewBuffer(nil)),
94+
zap.ErrorLevel,
95+
)
96+
logger := zap.New(core)
97+
98+
b.ResetTimer()
99+
b.ReportAllocs()
100+
101+
for i := 0; i < b.N; i++ {
102+
logger.Error("something went wrong",
103+
zap.Object("error", OopsMarshalFunc(err)),
104+
)
105+
}
106+
}

loggers/zap/go.mod

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module github.com/samber/oops/loggers/zap
2+
3+
go 1.21
4+
5+
replace github.com/samber/oops => ../..
6+
7+
require (
8+
github.com/samber/oops v0.0.0
9+
github.com/stretchr/testify v1.11.1
10+
go.uber.org/zap v1.26.0
11+
)
12+
13+
require (
14+
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/oklog/ulid/v2 v2.1.1 // indirect
16+
github.com/pmezard/go-difflib v1.0.0 // indirect
17+
github.com/samber/lo v1.52.0 // indirect
18+
go.opentelemetry.io/otel v1.29.0 // indirect
19+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
20+
go.uber.org/multierr v1.10.0 // indirect
21+
golang.org/x/text v0.22.0 // indirect
22+
gopkg.in/yaml.v3 v3.0.1 // indirect
23+
)

loggers/zap/go.sum

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
4+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5+
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
6+
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
7+
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
8+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
9+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10+
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
11+
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
12+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
13+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
14+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
15+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
16+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
17+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
18+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
19+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
20+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
21+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
22+
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
23+
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
24+
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
25+
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
26+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
27+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
28+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
29+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)