Repository: samber/oops
Pull request #50: Add Zerolog formatter
Author: login: zzzLobster
Created: 2025-03-03T20:25:15Z | Merged: 2025-03-04T19:38:09Z

Add Zerolog formatter

Log entry looks like this:
```json
{
  "level": "error",
  "stack": "Oops: permission denied\n  --- at ... main()",
  "error": {
    "code": "iam_authz_missing_permission",
    "domain": "authz",
    "context": {
      "hello": "world",
      "user_id": 1234,
      "permission": "post.create"
    },
    "user": {
      "firstname": "john",
      "id": "user-123",
      "lastname": "doe"
    },
    "error": "something failed: permission denied",
    "trace": "6710668a-2b2a-4de6-b8cf-3272a476a1c9",
    "hint": "Runbook: https://doc.acme.org/doc/abcd.md",
    "time": "2025-03-03T19:53:26.338249Z"
  },
  "time": "2025-03-03T22:53:26+03:00",
  "message": "something failed: permission denied"
}
```



Diff:
diff --git a/Makefile b/Makefile
index accd04f..4926b21 100644
--- a/Makefile
+++ b/Makefile
@@ -1,19 +1,21 @@
+MODULES=$(shell go list -m)
+MODULE_DIRS=$(shell go list -m -f '{{.Dir}}')
 
 build:
 	go build -v ./...
 
 test:
-	go test -race -v ./...
+	go test -race -v ${MODULES} ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ${MODULES} ./...'
 
 bench:
-	go test -benchmem -count 3 -bench ./...
+	go test -benchmem -count 3 -bench ${MODULES} ./...
 watch-bench:
-	reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
+	reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ${MODULES} ./...'
 
 coverage:
-	go test -v -coverprofile=cover.out -covermode=atomic ./...
+	go test -v -coverprofile=cover.out -covermode=atomic ${MODULES} ./...
 	go tool cover -html=cover.out -o cover.html
 
 tools:
@@ -27,9 +29,9 @@ tools:
 	go mod tidy
 
 lint:
-	golangci-lint run --timeout 60s --max-same-issues 50 ./...
+	golangci-lint run --timeout 60s --max-same-issues 50 ${MODULE_DIRS}
 lint-fix:
-	golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
+	golangci-lint run --timeout 60s --max-same-issues 50 --fix ${MODULE_DIRS}
 
 audit:
 	go list -json -m all | nancy sleuth
diff --git a/README.md b/README.md
index 04c6f0f..5103567 100644
--- a/README.md
+++ b/README.md
@@ -558,10 +558,10 @@ Available loggers:
 - log: [playground](https://go.dev/play/p/uNx3CcT-X40) - [example](https://github.com/samber/oops/tree/master/examples/log)
 - slog: [playground](https://go.dev/play/p/-X2ZnqjyDLu) - [example](https://github.com/samber/oops/tree/master/examples/slog)
 - logrus: [formatter](https://github.com/samber/oops/tree/master/loggers/logrus) - [playground](https://go.dev/play/p/-_7EBnceJ_A) - [example](https://github.com/samber/oops/tree/master/examples/logrus)
+- zerolog: [formatter](https://github.com/samber/oops/tree/master/loggers/zerolog) - [playground](https://go.dev/play/p/aalqQ6wEDyx) - [example](https://github.com/samber/oops/tree/master/examples/zerolog)
 
 We are looking for contributions and examples for:
 - zap
-- zerolog
 - go-sentry
 - otel
 - other?
diff --git a/examples/slog/example.go b/examples/slog/example.go
index c820572..cfbcf5b 100644
--- a/examples/slog/example.go
+++ b/examples/slog/example.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"fmt"
 	"log/slog"
 	"net/http"
 	"os"
@@ -45,36 +44,6 @@ func a() error {
 	return b()
 }
 
-type myError1 struct {
-	err error
-}
-
-func (e myError1) Error() string {
-	return fmt.Errorf("fuck %w", e.err).Error()
-}
-
-func (c myError1) Unwrap() error {
-	if c.err != nil {
-		return c.err
-	}
-	return nil
-}
-
-type myError2 struct {
-	err error
-}
-
-func (e myError2) Error() string {
-	return fmt.Errorf("fuck %w", e.err).Error()
-}
-
-func (c myError2) Unwrap() error {
-	if c.err != nil {
-		return c.err
-	}
-	return nil
-}
-
 func main() {
 	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
 
diff --git a/examples/zerolog/README.md b/examples/zerolog/README.md
index 17b50c4..6a018e5 100644
--- a/examples/zerolog/README.md
+++ b/examples/zerolog/README.md
@@ -1,9 +1,9 @@
 
 # Example of Zerolog logger
 
-Playground: https://go.dev/play/p/DaHzR4Zc-jj
+Playground: https://go.dev/play/p/aalqQ6wEDyx
 
 ```sh
 go run examples/zerolog/example.go 2>&1 | jq
-go run examples/zerolog/example.go 2>&1 | jq .stacktrace -r
+go run examples/zerolog/example.go 2>&1 | jq .stack -r
 ```
diff --git a/examples/zerolog/example.go b/examples/zerolog/example.go
index af32eeb..4eb3442 100644
--- a/examples/zerolog/example.go
+++ b/examples/zerolog/example.go
@@ -6,8 +6,12 @@ import (
 
 	"github.com/rs/zerolog"
 	"github.com/samber/oops"
+	oopszerolog "github.com/samber/oops/loggers/zerolog"
 )
 
+// go run examples/zerolog/example.go 2>&1 | jq
+// go run examples/zerolog/example.go 2>&1 | jq .stack -r
+
 func d() error {
 	return oops.
 		Code("iam_authz_missing_permission").
@@ -36,22 +40,9 @@ func a() error {
 	return b()
 }
 
-func WithOops(l zerolog.Logger, err error) *zerolog.Logger {
-	if oopsErr, ok := oops.AsOops(err); ok {
-		ctx := l.With()
-		for k, v := range oopsErr.ToMap() {
-			ctx = ctx.Interface(k, v)
-		}
-
-		logger := ctx.Err(oopsErr).Logger()
-		return &logger
-	}
-
-	// Recursively call into ourself so we can at least get a stack trace for any error
-	return WithOops(l, oops.Wrap(err))
-}
-
 func main() {
+	zerolog.ErrorStackMarshaler = oopszerolog.OopsStackMarshaller
+	zerolog.ErrorMarshalFunc = oopszerolog.OopsMarshalFunc
 	logger := zerolog.
 		New(os.Stderr).
 		With().
@@ -60,5 +51,5 @@ func main() {
 
 	err := a()
 
-	WithOops(logger, err).Error().Send()
+	logger.Error().Stack().Err(err).Msg(err.Error())
 }
diff --git a/examples/zerolog/go.mod b/examples/zerolog/go.mod
index 6c3f552..c71d320 100644
--- a/examples/zerolog/go.mod
+++ b/examples/zerolog/go.mod
@@ -2,16 +2,24 @@ module github.com/samber/oops/examples/zerolog
 
 go 1.21
 
+replace (
+	github.com/samber/oops => ../..
+	github.com/samber/oops/loggers/zerolog => ../../loggers/zerolog
+)
+
 require (
 	github.com/rs/zerolog v1.31.0
-	github.com/samber/oops v1.9.2
+	github.com/samber/oops v0.0.0
+	github.com/samber/oops/loggers/zerolog v0.0.0
 )
 
 require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.19 // indirect
 	github.com/oklog/ulid/v2 v2.1.0 // indirect
-	github.com/samber/lo v1.38.1 // indirect
-	golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
+	github.com/samber/lo v1.49.1 // indirect
+	go.opentelemetry.io/otel v1.29.0 // indirect
+	go.opentelemetry.io/otel/trace v1.29.0 // indirect
 	golang.org/x/sys v0.12.0 // indirect
+	golang.org/x/text v0.22.0 // indirect
 )
diff --git a/examples/zerolog/go.sum b/examples/zerolog/go.sum
index 582376f..7302ff1 100644
--- a/examples/zerolog/go.sum
+++ b/examples/zerolog/go.sum
@@ -1,5 +1,9 @@
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -9,16 +13,26 @@ github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
 github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
 github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
 github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
-github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
-github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
-github.com/samber/oops v1.9.2 h1:ph8WaOk5pEMetbtp+UTcdrdaggk8E8gKhpunShAKyIM=
-github.com/samber/oops v1.9.2/go.mod h1:xEXk4BLyqajkvCxzpxBnqfzHzRLFk3+g4E+tOJtOPZY=
-golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
-golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.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/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go.work b/go.work
index e9cb6b7..82abefb 100644
--- a/go.work
+++ b/go.work
@@ -14,6 +14,7 @@ use (
 
 	// logger formatters
 	./loggers/logrus
+	./loggers/zerolog
 
 	// recovery middlewares
 	./recovery/gin
diff --git a/go.work.sum b/go.work.sum
index 919e519..05dbaf0 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -1,15 +1,43 @@
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
+github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
 go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
 go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
@@ -18,29 +46,40 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
 golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
 golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
 golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
 golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
 golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
@@ -49,4 +88,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
diff --git a/loggers/zerolog/README.md b/loggers/zerolog/README.md
new file mode 100644
index 0000000..531db7c
--- /dev/null
+++ b/loggers/zerolog/README.md
@@ -0,0 +1,22 @@
+# Zerolog formatter for Oops
+
+```go
+import oopszerolog "github.com/samber/oops/loggers/zerolog"
+
+func init() {
+	zerolog.ErrorStackMarshaler = oopszerolog.OopsStackMarshaller
+	zerolog.ErrorMarshalFunc = oopszerolog.OopsMarshalFunc
+}
+
+func main() {
+	err := oops.
+		With("driver", "postgresql").
+		With("query", query).
+		With("query.duration", queryDuration).
+		Errorf("could not fetch user")
+
+	if err != nil {
+		zerolog.New(os.Stderr).Error().Stack().Err(err).Msg(err.Error())
+	}
+}
+```
diff --git a/loggers/zerolog/formatter.go b/loggers/zerolog/formatter.go
new file mode 100644
index 0000000..9f3e5f0
--- /dev/null
+++ b/loggers/zerolog/formatter.go
@@ -0,0 +1,49 @@
+package oopszerolog
+
+import (
+	"github.com/rs/zerolog"
+	"github.com/samber/oops"
+)
+
+func OopsStackMarshaller(err error) interface{} {
+	if typedErr, ok := oops.AsOops(err); ok {
+		return typedErr.Stacktrace()
+	}
+	return err
+}
+
+func OopsMarshalFunc(err error) interface{} {
+	if typedErr, ok := oops.AsOops(err); ok {
+		return zerologErrorMarshaller{err: typedErr}
+	}
+	return err
+}
+
+type zerologErrorMarshaller struct {
+	err oops.OopsError
+}
+
+func (m zerologErrorMarshaller) MarshalZerologObject(e *zerolog.Event) {
+	payload := m.err.ToMap()
+	for k, v := range payload {
+		switch k {
+		case "stacktrace":
+		case "context":
+			if context, ok := v.(map[string]any); ok && len(context) > 0 {
+				dict := zerolog.Dict()
+				for k, v := range context {
+					switch vTyped := v.(type) {
+					case nil:
+					case error:
+						dict = dict.Str(k, vTyped.Error())
+					default:
+						dict = dict.Interface(k, vTyped)
+					}
+				}
+				e.Dict(k, dict)
+			}
+		default:
+			e.Any(k, v)
+		}
+	}
+}
diff --git a/loggers/zerolog/formatter_test.go b/loggers/zerolog/formatter_test.go
new file mode 100644
index 0000000..eaf2ef0
--- /dev/null
+++ b/loggers/zerolog/formatter_test.go
@@ -0,0 +1,61 @@
+package oopszerolog
+
+import (
+	"bytes"
+	"encoding/json"
+	"github.com/rs/zerolog"
+	"github.com/samber/oops"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+type jsonLogEntryError struct {
+	Error   string
+	Time    string
+	Domain  string
+	Trace   string
+	Context map[string]any
+}
+
+type jsonLogEntry struct {
+	Level   string
+	Stack   string
+	Message string
+	Error   jsonLogEntryError
+}
+
+func TestZerologFormatter(t *testing.T) {
+	zerolog.ErrorStackMarshaler = OopsStackMarshaller
+	zerolog.ErrorMarshalFunc = OopsMarshalFunc
+
+	buffer := bytes.NewBuffer(make([]byte, 0, 1024))
+	logger := zerolog.New(buffer).Level(zerolog.ErrorLevel)
+	err := oops.
+		In("test").
+		With("driver", "postgresql").
+		Errorf("could not fetch user")
+	logger.Error().Stack().Err(err).Msg("something went wrong")
+
+	loggedError := jsonLogEntry{}
+	err = json.Unmarshal(buffer.Bytes(), &loggedError)
+	require.NoError(t, err)
+
+	assert.Contains(t, loggedError.Stack, "Oops: could not fetch user\n  --- at ")
+	assert.NotEmpty(t, loggedError.Error.Time)
+	assert.NotEmpty(t, loggedError.Error.Trace)
+	loggedError.Stack = ""
+	loggedError.Error.Time = ""
+	loggedError.Error.Trace = ""
+	assert.EqualValues(t, jsonLogEntry{
+		Level:   "error",
+		Message: "something went wrong",
+		Error: jsonLogEntryError{
+			Error:  "could not fetch user",
+			Domain: "test",
+			Context: map[string]any{
+				"driver": "postgresql",
+			},
+		},
+	}, loggedError)
+}
diff --git a/loggers/zerolog/go.mod b/loggers/zerolog/go.mod
new file mode 100644
index 0000000..1252469
--- /dev/null
+++ b/loggers/zerolog/go.mod
@@ -0,0 +1,25 @@
+module github.com/samber/oops/loggers/zerolog
+
+go 1.21
+
+replace github.com/samber/oops => ../..
+
+require (
+	github.com/rs/zerolog v1.31.0
+	github.com/samber/oops v0.0.0
+	github.com/stretchr/testify v1.10.0
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/oklog/ulid/v2 v2.1.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/samber/lo v1.49.1 // indirect
+	go.opentelemetry.io/otel v1.29.0 // indirect
+	go.opentelemetry.io/otel/trace v1.29.0 // indirect
+	golang.org/x/sys v0.12.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/loggers/zerolog/go.sum b/loggers/zerolog/go.sum
new file mode 100644
index 0000000..de34631
--- /dev/null
+++ b/loggers/zerolog/go.sum
@@ -0,0 +1,45 @@
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
+github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
+github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
+github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/recovery/gin/middleware.go b/recovery/gin/middleware.go
index 9ae44f7..4671f8f 100644
--- a/recovery/gin/middleware.go
+++ b/recovery/gin/middleware.go
@@ -11,7 +11,7 @@ func GinOopsRecovery() gin.HandlerFunc {
 			c.Next()
 		}, "gin: panic recovered")
 		if err != nil {
-			c.Error(err)
+			_ = c.Error(err)
 			c.AbortWithStatus(500)
 		}
 	}


Reviews:
user: login: samber
body: Please add it to readme as well. ✌️

(ctrl-f logrus)
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-04T13:44:27Z
user: login: zzzLobster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T13:57:42Z
user: login: samber
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T14:09:30Z
user: login: samber
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T14:10:38Z
user: login: samber
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-04T14:13:22Z
user: login: zzzLobster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T14:45:55Z
user: login: samber
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T15:34:29Z
user: login: zzzLobster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T17:42:04Z
user: login: samber
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-04T19:37:47Z

Review comments:
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: samber
body: i'm pretty sure it is not necessary, since you declared this submodule in go.work
created_at: 2025-03-04T13:41:47Z
author_association: OWNER
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: zzzLobster
body: Unfortunately, there is no a straight way to do that right now.
There is a proposal for that: https://github.com/golang/go/issues/71294 (accepted, but it's not a part of `cmd/go` so far)

```sh
go test -race -v ./...
=== RUN   TestErrorsIs
--- PASS: TestErrorsIs (0.00s)
=== RUN   TestErrorsAs
--- PASS: TestErrorsAs (0.00s)
=== RUN   TestDereferencePointers
--- PASS: TestDereferencePointers (0.00s)
=== RUN   TestOopsWrap
--- PASS: TestOopsWrap (0.00s)
=== RUN   TestOopsWrap_wrapped
--- PASS: TestOopsWrap_wrapped (0.10s)
=== RUN   TestOopsWrapf
--- PASS: TestOopsWrapf (0.00s)
=== RUN   TestOopsFromContext
--- PASS: TestOopsFromContext (0.00s)
=== RUN   TestOopsNew
--- PASS: TestOopsNew (0.00s)
=== RUN   TestOopsErrorf
--- PASS: TestOopsErrorf (0.00s)
=== RUN   TestOopsCode
--- PASS: TestOopsCode (0.00s)
=== RUN   TestOopsTime
--- PASS: TestOopsTime (0.00s)
=== RUN   TestOopsSince
--- PASS: TestOopsSince (0.01s)
=== RUN   TestOopsDuration
--- PASS: TestOopsDuration (0.01s)
=== RUN   TestOopsIn
--- PASS: TestOopsIn (0.00s)
=== RUN   TestOopsTags
--- PASS: TestOopsTags (0.00s)
=== RUN   TestOopsTx
--- PASS: TestOopsTx (0.00s)
=== RUN   TestOopsTxSpanFromOtel
--- PASS: TestOopsTxSpanFromOtel (0.00s)
=== RUN   TestOopsWith
--- PASS: TestOopsWith (0.00s)
=== RUN   TestOopsWithContext
--- PASS: TestOopsWithContext (0.00s)
=== RUN   TestOopsWithLazyEvaluation
--- PASS: TestOopsWithLazyEvaluation (0.00s)
=== RUN   TestOopsHint
--- PASS: TestOopsHint (0.00s)
=== RUN   TestOopsPublic
--- PASS: TestOopsPublic (0.00s)
=== RUN   TestOopsOwner
--- PASS: TestOopsOwner (0.00s)
=== RUN   TestOopsUser
--- PASS: TestOopsUser (0.00s)
=== RUN   TestOopsTenant
--- PASS: TestOopsTenant (0.00s)
=== RUN   TestOopsRequest
--- PASS: TestOopsRequest (0.00s)
=== RUN   TestOopsMixed
--- PASS: TestOopsMixed (0.00s)
=== RUN   TestOopsMixedWithGetters
--- PASS: TestOopsMixedWithGetters (0.00s)
=== RUN   TestOopsLogValuer
--- PASS: TestOopsLogValuer (0.00s)
=== RUN   TestOopsFormatSummary
--- PASS: TestOopsFormatSummary (0.00s)
=== RUN   TestOopsFormatVerbose
--- PASS: TestOopsFormatVerbose (0.00s)
=== RUN   TestOopsMarshalJSON
--- PASS: TestOopsMarshalJSON (0.00s)
=== RUN   TestOopsGetPublic
--- PASS: TestOopsGetPublic (0.00s)
=== RUN   TestRemoveGoPath
--- PASS: TestRemoveGoPath (0.00s)
=== RUN   TestStacktrace
--- PASS: TestStacktrace (0.00s)
PASS
ok      github.com/samber/oops  (cached)
```
(it does not run the new test)

```go
go -C loggers/zerolog test -race -v ./...
=== RUN   TestZerologFormatter
--- PASS: TestZerologFormatter (0.00s)
PASS
ok      github.com/samber/oops/loggers/zerolog  (cached)
```

Possible one-liner (I'm not sure if that's gonna work in CI):
```sh
go list -f '{{.Dir}}' -m | xargs go test -race -v
?       github.com/samber/oops/examples/log     [no test files]
?       github.com/samber/oops/examples/panic   [no test files]
?       github.com/samber/oops/examples/logrus  [no test files]
?       github.com/samber/oops/examples/segfault        [no test files]
?       github.com/samber/oops/examples/slog    [no test files]
?       github.com/samber/oops/examples/sources [no test files]
?       github.com/samber/oops/examples/zerolog [no test files]
=== RUN   TestErrorsIs
--- PASS: TestErrorsIs (0.00s)
=== RUN   TestErrorsAs
--- PASS: TestErrorsAs (0.00s)
=== RUN   TestDereferencePointers
--- PASS: TestDereferencePointers (0.00s)
=== RUN   TestOopsWrap
--- PASS: TestOopsWrap (0.00s)
=== RUN   TestOopsWrap_wrapped
--- PASS: TestOopsWrap_wrapped (0.10s)
=== RUN   TestOopsWrapf
--- PASS: TestOopsWrapf (0.00s)
=== RUN   TestOopsFromContext
--- PASS: TestOopsFromContext (0.00s)
=== RUN   TestOopsNew
--- PASS: TestOopsNew (0.00s)
=== RUN   TestOopsErrorf
--- PASS: TestOopsErrorf (0.00s)
=== RUN   TestOopsCode
--- PASS: TestOopsCode (0.00s)
=== RUN   TestOopsTime
--- PASS: TestOopsTime (0.00s)
=== RUN   TestOopsSince
--- PASS: TestOopsSince (0.01s)
=== RUN   TestOopsDuration
--- PASS: TestOopsDuration (0.01s)
=== RUN   TestOopsIn
--- PASS: TestOopsIn (0.00s)
=== RUN   TestOopsTags
--- PASS: TestOopsTags (0.00s)
=== RUN   TestOopsTx
--- PASS: TestOopsTx (0.00s)
=== RUN   TestOopsTxSpanFromOtel
--- PASS: TestOopsTxSpanFromOtel (0.00s)
=== RUN   TestOopsWith
--- PASS: TestOopsWith (0.00s)
=== RUN   TestOopsWithContext
--- PASS: TestOopsWithContext (0.00s)
=== RUN   TestOopsWithLazyEvaluation
--- PASS: TestOopsWithLazyEvaluation (0.00s)
=== RUN   TestOopsHint
--- PASS: TestOopsHint (0.00s)
=== RUN   TestOopsPublic
--- PASS: TestOopsPublic (0.00s)
=== RUN   TestOopsOwner
--- PASS: TestOopsOwner (0.00s)
=== RUN   TestOopsUser
--- PASS: TestOopsUser (0.00s)
=== RUN   TestOopsTenant
--- PASS: TestOopsTenant (0.00s)
=== RUN   TestOopsRequest
--- PASS: TestOopsRequest (0.00s)
=== RUN   TestOopsMixed
--- PASS: TestOopsMixed (0.00s)
=== RUN   TestOopsMixedWithGetters
--- PASS: TestOopsMixedWithGetters (0.00s)
=== RUN   TestOopsLogValuer
--- PASS: TestOopsLogValuer (0.00s)
=== RUN   TestOopsFormatSummary
--- PASS: TestOopsFormatSummary (0.00s)
=== RUN   TestOopsFormatVerbose
--- PASS: TestOopsFormatVerbose (0.00s)
=== RUN   TestOopsMarshalJSON
--- PASS: TestOopsMarshalJSON (0.00s)
=== RUN   TestOopsGetPublic
--- PASS: TestOopsGetPublic (0.00s)
=== RUN   TestRemoveGoPath
--- PASS: TestRemoveGoPath (0.00s)
=== RUN   TestStacktrace
--- PASS: TestStacktrace (0.00s)
PASS
ok      github.com/samber/oops  (cached)
testing: warning: no tests to run
PASS
ok      github.com/samber/oops/loggers/logrus   (cached) [no tests to run]
=== RUN   TestZerologFormatter
--- PASS: TestZerologFormatter (0.00s)
PASS
ok      github.com/samber/oops/loggers/zerolog  (cached)
?       github.com/samber/oops/recovery/gin     [no test files]
```
created_at: 2025-03-04T13:57:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: samber
body: Oh yes, sorry. I did this in another project:

```makefile
MODULES=$(shell go list -m)

test:
	go test -race -v ${MODULES} ./...
```
created_at: 2025-03-04T14:09:30Z
author_association: OWNER
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: samber
body: feel free to change the readme and add logrus submodule

the limit of both methods is that go.work version must be compatible with submodule
created_at: 2025-03-04T14:10:37Z
author_association: OWNER
diff_hunk: @@ -4,14 +4,16 @@ go 1.21
 
 require (
 	github.com/rs/zerolog v1.31.0
-	github.com/samber/oops v1.9.2
+	github.com/samber/oops v1.16.1
path: examples/zerolog/go.mod
user: login: samber
body: ```
require github.com/samber/oops v0.0.0
replace github.com/samber/oops => ../..
```
created_at: 2025-03-04T14:12:28Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,23 @@
+module github.com/samber/oops/loggers/zerolog
+
+go 1.21
+
+require (
+	github.com/rs/zerolog v1.31.0
+	github.com/samber/oops v1.16.1
path: loggers/zerolog/go.mod
user: login: samber
body: ```
require github.com/samber/oops v0.0.0
replace github.com/samber/oops => ../..
```
created_at: 2025-03-04T14:13:15Z
author_association: OWNER
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: zzzLobster
body: I'm not sure I precisely understand what you mean.
What readme do you mean in this case?

I propose to use your approach from that another project:
```sh
MODULES=$(shell go list -m)

build:
	go build -v ./...

test:
	go test -race -v ${MODULES} ./...
watch-test:
	reflex -t 50ms -s -- sh -c 'gotest -race -v ${MODULES} ./...'
```

and update other goals same way
created_at: 2025-03-04T14:45:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: samber
body: sorry, I mean the Makefile :-( 
created_at: 2025-03-04T15:34:29Z
author_association: OWNER
diff_hunk: @@ -4,8 +4,9 @@ build:
 
 test:
 	go test -race -v ./...
+	go -C loggers/zerolog test -race -v ./...
 watch-test:
-	reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
+	reflex -t 50ms -s -- sh -c 'gotest -race -v ./... && go -C loggers/zerolog test -race -v ./...'
path: Makefile
user: login: zzzLobster
body: Please take a look at the latest commit with the changes in `Makefile`.
Same issue is applicable to `golangci-lint`, the difference is that it wants directories, not packages (updated it as well and it forced me to fix a couple of files)
created_at: 2025-03-04T17:42:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: zzzLobster
created_at: 2025-03-03T20:25:41Z
body: @samber , Should I raise an issue before submitting a PR?
author_association: CONTRIBUTOR
user: login: zzzLobster
created_at: 2025-03-04T14:11:44Z
body: > Please add it to readme as well. ✌️
> 
> (ctrl-f logrus)

I've updated the root `README`. I hope I've not missed anything
author_association: CONTRIBUTOR
user: login: samber
created_at: 2025-03-04T19:38:05Z
body: Thanks!

Let's merge ;)

[edit] i'm going to make a release as soon as the other pr is done
author_association: OWNER