From e7e598c398ffadc7ab7e5b73f545115e07aadc73 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 4 Feb 2025 14:20:35 +0100 Subject: [PATCH 01/21] Update README.md --- README.md | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a6244cf..caa70a3 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,21 @@ [![License](https://img.shields.io/github/license/samber/slog-multi)](./LICENSE) Design workflows of [slog](https://pkg.go.dev/log/slog) handlers: -- **fanout**: distribute `log.Record` to multiple `slog.Handler` in parallel -- **pipeline**: rewrite `log.Record` on the fly (eg: for privacy reason) -- **routing**: forward `log.Record` to all matching `slog.Handler` -- **failover**: forward `log.Record` to the first available `slog.Handler` -- **load balancing**: increase log bandwidth by sending `log.Record` to a pool of `slog.Handler` +- **Fanout**: distribute `log.Record` to multiple `slog.Handler` in parallel +- **Pipe**: rewrite `log.Record` on the fly (eg: for privacy reasons) +- **Router**: forward `log.Record` to all matching `slog.Handler` +- **Failover**: forward `log.Record` to the first available `slog.Handler` +- **Pool**: increase log bandwidth by sending `log.Record` to a pool of `slog.Handler` +- **RecoverHandlerError**: catch panics and errors from handlers -Here a simple workflow with both pipeline and fanout: +Here is a simple workflow with both pipeline and fanout: ![workflow example with pipeline and fanout](./images/workflow.png) +Middlewares: +- [Inline handler](#inline-handler): a shortcut to implement `slog.Handler` +- [Inline middleware](#inline-middleware): a shortcut to implement `slogmulti.Middleware` +

Sponsored by: @@ -36,18 +41,6 @@ Here a simple workflow with both pipeline and fanout:
-**Routing:** -- [Fanout](#broadcast-slogmultifanout): distributes records to multiple `slog.Handler` in parallel -- [Router](#routing-slogmultirouter): forwards records to all matching `slog.Handler` -- [Failover](#failover-slogmultifailover): forwards records to the first available `slog.Handler` -- [Load balancing](#load-balancing-slogmultipool): balances records between multiple `slog.Handler` -- [Chaining / Pipe](#chaining-slogmultipipe): builds a chain of Middleware -- [Recover handler error](#recover-errors-RecoverHandlerError): catch panics and error from handlers - -**Middlewares:** -- [Inline handler](#inline-handler): a shortcut to implement `slog.Handler` -- [Inline middleware](#inline-middleware): a shortcut to implment `slogmulti.Middleware` - **See also:** - [slog-multi](https://github.com/samber/slog-multi): `slog.Handler` chaining, fanout, routing, failover, load balancing... From dcd7c0e664a694e5411fc7c7f897bd6ebedaa368 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:57:59 +0100 Subject: [PATCH 02/21] chore(deps): bump golangci/golangci-lint-action from 6 to 7 (#28) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 7. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v7) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6d026b1..0b5036f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: stable: false - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: args: --timeout 120s --max-same-issues 50 From f8ccbbd9cb3a378dca599374c377ceb5f69aabe5 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Thu, 24 Apr 2025 00:56:10 +0200 Subject: [PATCH 03/21] chore(ci): test more go version --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f565853..8f24bc1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ jobs: go: - '1.21' - '1.22' + - '1.23' + - '1.24' - '1.x' steps: - uses: actions/checkout@v4 From 474b41418c00a79276a726f59445f495bf71f3c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:45:51 +0200 Subject: [PATCH 04/21] chore(deps): bump github.com/samber/lo from 1.49.1 to 1.50.0 (#29) Bumps [github.com/samber/lo](https://github.com/samber/lo) from 1.49.1 to 1.50.0. - [Release notes](https://github.com/samber/lo/releases) - [Commits](https://github.com/samber/lo/compare/v1.49.1...v1.50.0) --- updated-dependencies: - dependency-name: github.com/samber/lo dependency-version: 1.50.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 57f111f..1200446 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/samber/slog-multi go 1.21 require ( - github.com/samber/lo v1.49.1 + github.com/samber/lo v1.50.0 github.com/stretchr/testify v1.10.0 go.uber.org/goleak v1.3.0 ) @@ -11,6 +11,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 95f1f14..0d7667a 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,14 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= +github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= 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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/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= From 55a5ff542410ca32e446d95549304216736d65b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 22:19:57 +0200 Subject: [PATCH 05/21] chore(deps): bump golangci/golangci-lint-action from 7 to 8 (#31) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 8. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0b5036f..35f8b78 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: stable: false - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@v8 with: args: --timeout 120s --max-same-issues 50 From f4c83d9c5d4ed563809d31c85230a7bc87869bc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:40:43 +0200 Subject: [PATCH 06/21] chore(deps): bump github.com/samber/lo from 1.50.0 to 1.51.0 (#32) Bumps [github.com/samber/lo](https://github.com/samber/lo) from 1.50.0 to 1.51.0. - [Release notes](https://github.com/samber/lo/releases) - [Commits](https://github.com/samber/lo/compare/v1.50.0...v1.51.0) --- updated-dependencies: - dependency-name: github.com/samber/lo dependency-version: 1.51.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 3 ++- go.sum | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 1200446..ab276c7 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/samber/slog-multi go 1.21 require ( - github.com/samber/lo v1.50.0 + github.com/samber/lo v1.51.0 github.com/stretchr/testify v1.10.0 go.uber.org/goleak v1.3.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 0d7667a..4add69e 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,14 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= -github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= 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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -15,7 +16,7 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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/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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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= From 25912654fde70162c00d4c899332176ea4d7eab2 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Mon, 23 Jun 2025 13:08:30 +0200 Subject: [PATCH 07/21] fix(router): fix attribute+group merging closing #30 --- examples/router/example.go | 2 ++ examples/router/go.mod | 8 +++++--- examples/router/go.sum | 19 ++++++++++--------- go.mod | 1 + go.sum | 2 ++ go.work.sum | 12 +++++++++--- router.go | 18 +++++++++++++++++- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/examples/router/example.go b/examples/router/example.go index e265b73..53c76be 100644 --- a/examples/router/example.go +++ b/examples/router/example.go @@ -33,6 +33,7 @@ func main() { logger := slog.New( slogmulti.Router(). + // Add(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}), recordMatchRegion("us")). Add(slackChannelUS, recordMatchRegion("us")). Add(slackChannelEU, recordMatchRegion("eu")). Add(slackChannelAPAC, recordMatchRegion("apac")). @@ -41,6 +42,7 @@ func main() { logger. With("region", "us"). + WithGroup("group1"). With("pool", "us-west-1"). Error("Server desynchronized") } diff --git a/examples/router/go.mod b/examples/router/go.mod index 6eb87ab..ed51498 100644 --- a/examples/router/go.mod +++ b/examples/router/go.mod @@ -9,8 +9,10 @@ require ( require ( github.com/gorilla/websocket v1.4.2 // indirect - github.com/samber/lo v1.47.0 // indirect + github.com/samber/lo v1.51.0 // indirect + github.com/samber/slog-common v0.19.0 // indirect github.com/slack-go/slack v0.12.1 // indirect - golang.org/x/text v0.16.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/text v0.22.0 // indirect ) + +replace github.com/samber/slog-multi => ../../ diff --git a/examples/router/go.sum b/examples/router/go.sum index 01f7435..ab78617 100644 --- a/examples/router/go.sum +++ b/examples/router/go.sum @@ -9,20 +9,21 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= -github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= -github.com/samber/slog-multi v1.0.0 h1:snvP/P5GLQ8TQh5WSqdRaxDANW8AAA3egwEoytLsqvc= -github.com/samber/slog-multi v1.0.0/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= +github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= github.com/samber/slog-slack v1.0.0 h1:HjrlufIPwhRm8Hv4Ox8khu0+0StGEbAEY4fZzOQM5qs= github.com/samber/slog-slack v1.0.0/go.mod h1:S0UnSLr8Vpv8+gW41dwkg589dUVT+HX2p5Y6zcehSd4= github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw= github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/go.mod b/go.mod index ab276c7..4f0a849 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/samber/lo v1.51.0 + github.com/samber/slog-common v0.19.0 github.com/stretchr/testify v1.10.0 go.uber.org/goleak v1.3.0 ) diff --git a/go.sum b/go.sum index 4add69e..a1438f7 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= +github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= 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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/go.work.sum b/go.work.sum index d071d27..4bc908f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -5,8 +5,12 @@ 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/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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= +github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk= +github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= +github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 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= @@ -22,10 +26,13 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -34,4 +41,3 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/router.go b/router.go index 54921d0..67f7ecd 100644 --- a/router.go +++ b/router.go @@ -2,8 +2,11 @@ package slogmulti import ( "context" + "slices" "log/slog" + + slogcommon "github.com/samber/slog-common" ) type router struct { @@ -25,6 +28,8 @@ func (h *router) Add(handler slog.Handler, matchers ...func(ctx context.Context, &RoutableHandler{ matchers: matchers, handler: handler, + groups: []string{}, + attrs: []slog.Attr{}, }, ), } @@ -40,6 +45,8 @@ var _ slog.Handler = (*RoutableHandler)(nil) type RoutableHandler struct { matchers []func(ctx context.Context, r slog.Record) bool handler slog.Handler + groups []string + attrs []slog.Attr } // Implements slog.Handler @@ -49,8 +56,13 @@ func (h *RoutableHandler) Enabled(ctx context.Context, l slog.Level) bool { // Implements slog.Handler func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { + clone := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) + clone.AddAttrs( + slogcommon.AppendRecordAttrsToAttrs(h.attrs, h.groups, &r)..., + ) + for _, matcher := range h.matchers { - if !matcher(ctx, r) { + if !matcher(ctx, clone) { return nil } } @@ -63,6 +75,8 @@ func (h *RoutableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &RoutableHandler{ matchers: h.matchers, handler: h.handler.WithAttrs(attrs), + groups: slices.Clone(h.groups), + attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), } } @@ -76,5 +90,7 @@ func (h *RoutableHandler) WithGroup(name string) slog.Handler { return &RoutableHandler{ matchers: h.matchers, handler: h.handler.WithGroup(name), + groups: append(slices.Clone(h.groups), name), + attrs: h.attrs, } } From ae289c6413f65280d26050761ef3512a9aab7b6e Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Mon, 23 Jun 2025 21:07:15 +0200 Subject: [PATCH 08/21] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index caa70a3..7af529a 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ No breaking changes will be made to exported APIs before v2.0.0. > [!WARNING] > Use this library carefully, log processing can be very costly (!) +> +> Excessive logging —with multiple processing steps and destinations— can introduce significant overhead, which is generally undesirable in performance-critical paths. Sometimes, metrics or a sampling strategy are cheaper. ## 💡 Usage From 3be26d6f44d9a1f14d29549d10e9101a2f9e9037 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Mon, 23 Jun 2025 21:17:51 +0200 Subject: [PATCH 09/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7af529a..59ba1ce 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ No breaking changes will be made to exported APIs before v2.0.0. > [!WARNING] > Use this library carefully, log processing can be very costly (!) > -> Excessive logging —with multiple processing steps and destinations— can introduce significant overhead, which is generally undesirable in performance-critical paths. Sometimes, metrics or a sampling strategy are cheaper. +> Excessive logging —with multiple processing steps and destinations— can introduce significant overhead, which is generally undesirable in performance-critical paths. Logging is always expensive, and sometimes, metrics or a sampling strategy are cheaper. The library itself does not generate extra load. ## 💡 Usage From c8956e0023bbcd35d078e3de2b06fe6291f992f3 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 24 Jun 2025 21:24:59 +0200 Subject: [PATCH 10/21] doc: improved comments+readme --- README.md | 259 ++++++++++++++++++++++++++++++++++++-------------- failover.go | 88 +++++++++++++++-- middleware.go | 22 ++++- multi.go | 93 +++++++++++++++++- pipe.go | 56 ++++++++++- pool.go | 96 +++++++++++++++++-- recover.go | 90 ++++++++++++++++-- router.go | 103 ++++++++++++++++++-- 8 files changed, 697 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 59ba1ce..1a82676 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ - -# slog: Handler chaining, fanout, routing, failover, load balancing... +# slog-multi: Advanced Handler Composition for Go's Structured Logging (chaining, fanout, routing, failover...) [![tag](https://img.shields.io/github/tag/samber/slog-multi.svg)](https://github.com/samber/slog-multi/releases) ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c) @@ -10,21 +9,20 @@ [![Contributors](https://img.shields.io/github/contributors/samber/slog-multi)](https://github.com/samber/slog-multi/graphs/contributors) [![License](https://img.shields.io/github/license/samber/slog-multi)](./LICENSE) -Design workflows of [slog](https://pkg.go.dev/log/slog) handlers: -- **Fanout**: distribute `log.Record` to multiple `slog.Handler` in parallel -- **Pipe**: rewrite `log.Record` on the fly (eg: for privacy reasons) -- **Router**: forward `log.Record` to all matching `slog.Handler` -- **Failover**: forward `log.Record` to the first available `slog.Handler` -- **Pool**: increase log bandwidth by sending `log.Record` to a pool of `slog.Handler` -- **RecoverHandlerError**: catch panics and errors from handlers +**slog-multi** provides advanced composition patterns for Go's structured logging (`slog`). It enables you to build sophisticated logging workflows by combining multiple handlers with different strategies for distribution, routing, transformation, and error handling. -Here is a simple workflow with both pipeline and fanout: +## 🎯 Features -![workflow example with pipeline and fanout](./images/workflow.png) +- **🔄 Fanout**: Distribute logs to multiple handlers in parallel +- **🛣️ Router**: Conditionally route logs based on custom criteria +- **🔄 Failover**: High-availability logging with automatic fallback +- **⚖️ Load Balancing**: Distribute load across multiple handlers +- **🔗 Pipeline**: Transform and filter logs with middleware chains +- **🛡️ Error Recovery**: Graceful handling of logging failures Middlewares: -- [Inline handler](#inline-handler): a shortcut to implement `slog.Handler` -- [Inline middleware](#inline-middleware): a shortcut to implement `slogmulti.Middleware` +- **⚡ Inline Handlers**: Quick implementation of custom handlers +- **🔧 Inline Middleware**: Rapid development of transformation logic

@@ -84,7 +82,7 @@ Middlewares: - [slog-parquet](https://github.com/samber/slog-parquet): A `slog` handler for `Parquet` + `Object Storage` - [slog-channel](https://github.com/samber/slog-channel): A `slog` handler for Go channels -## 🚀 Install +## 🚀 Installation ```sh go get github.com/samber/slog-multi @@ -105,22 +103,30 @@ GoDoc: [https://pkg.go.dev/github.com/samber/slog-multi](https://pkg.go.dev/gith ### Broadcast: `slogmulti.Fanout()` -Distribute logs to multiple `slog.Handler` in parallel. +Distribute logs to multiple `slog.Handler` in parallel for maximum throughput and redundancy. ```go import ( + "net" slogmulti "github.com/samber/slog-multi" "log/slog" + "os" + "time" ) func main() { logstash, _ := net.Dial("tcp", "logstash.acme:4242") // use github.com/netbrain/goautosocket for auto-reconnect + datadogHandler := slogdatadog.NewDatadogHandler(slogdatadog.Option{ + APIKey: "your-api-key", + Service: "my-service", + }) stderr := os.Stderr logger := slog.New( slogmulti.Fanout( slog.NewJSONHandler(logstash, &slog.HandlerOptions{}), // pass to first handler: logstash over tcp slog.NewTextHandler(stderr, &slog.HandlerOptions{}), // then to second handler: stderr + datadogHandler, // ... ), ) @@ -162,13 +168,15 @@ Netcat output: ### Routing: `slogmulti.Router()` -Distribute logs to all matching `slog.Handler` in parallel. +Distribute logs to all matching `slog.Handler` based on custom criteria like log level, attributes, or business logic. ```go import ( + "context" slogmulti "github.com/samber/slog-multi" slogslack "github.com/samber/slog-slack" "log/slog" + "os" ) func main() { @@ -176,11 +184,14 @@ func main() { slackChannelEU := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "supervision-eu"}.NewSlackHandler() slackChannelAPAC := slogslack.Option{Level: slog.LevelError, WebhookURL: "xxx", Channel: "supervision-apac"}.NewSlackHandler() + consoleHandler := slog.NewTextHandler(os.Stderr, nil) + logger := slog.New( slogmulti.Router(). Add(slackChannelUS, recordMatchRegion("us")). Add(slackChannelEU, recordMatchRegion("eu")). Add(slackChannelAPAC, recordMatchRegion("apac")). + Add(consoleHandler, slogmulti.Level(slog.LevelInfo)). Handler(), ) @@ -208,34 +219,41 @@ func recordMatchRegion(region string) func(ctx context.Context, r slog.Record) b } ``` +**Use Cases:** +- Environment-specific logging (dev vs prod) +- Level-based routing (errors to Slack, info to console) +- Business logic routing (user actions vs system events) + ### Failover: `slogmulti.Failover()` -List multiple targets for a `slog.Record` instead of retrying on the same unavailable log management system. +Ensure logging reliability by trying multiple handlers in order until one succeeds. Perfect for high-availability scenarios. ```go import ( "net" slogmulti "github.com/samber/slog-multi" "log/slog" + "os" + "time" ) func main() { + // Create connections to multiple log servers // ncat -l 1000 -k // ncat -l 1001 -k // ncat -l 1002 -k - // list AZs - // use github.com/netbrain/goautosocket for auto-reconnect + // List AZs - use github.com/netbrain/goautosocket for auto-reconnect logstash1, _ := net.Dial("tcp", "logstash.eu-west-3a.internal:1000") logstash2, _ := net.Dial("tcp", "logstash.eu-west-3b.internal:1000") logstash3, _ := net.Dial("tcp", "logstash.eu-west-3c.internal:1000") logger := slog.New( slogmulti.Failover()( - slog.HandlerOptions{}.NewJSONHandler(logstash1, nil), // send to this instance first - slog.HandlerOptions{}.NewJSONHandler(logstash2, nil), // then this instance in case of failure - slog.HandlerOptions{}.NewJSONHandler(logstash3, nil), // and finally this instance in case of double failure + slog.HandlerOptions{}.NewJSONHandler(logstash1, nil), // Primary + slog.HandlerOptions{}.NewJSONHandler(logstash2, nil), // Secondary + slog.HandlerOptions{}.NewJSONHandler(logstash3, nil), // Tertiary ), ) @@ -252,59 +270,76 @@ func main() { } ``` +**Use Cases:** +- High-availability logging infrastructure +- Disaster recovery scenarios +- Multi-region deployments + ### Load balancing: `slogmulti.Pool()` -Increase log bandwidth by sending `log.Record` to a pool of `slog.Handler`. +Distribute logging load across multiple handlers using round-robin with randomization to increase throughput and provide redundancy. ```go import ( "net" slogmulti "github.com/samber/slog-multi" "log/slog" + "os" + "time" ) func main() { + // Create multiple log servers // ncat -l 1000 -k // ncat -l 1001 -k // ncat -l 1002 -k - // list AZs - // use github.com/netbrain/goautosocket for auto-reconnect + // List AZs - use github.com/netbrain/goautosocket for auto-reconnect logstash1, _ := net.Dial("tcp", "logstash.eu-west-3a.internal:1000") logstash2, _ := net.Dial("tcp", "logstash.eu-west-3b.internal:1000") logstash3, _ := net.Dial("tcp", "logstash.eu-west-3c.internal:1000") logger := slog.New( slogmulti.Pool()( - // a random handler will be picked + // A random handler will be picked for each log slog.HandlerOptions{}.NewJSONHandler(logstash1, nil), slog.HandlerOptions{}.NewJSONHandler(logstash2, nil), slog.HandlerOptions{}.NewJSONHandler(logstash3, nil), ), ) - logger. - With( - slog.Group("user", - slog.String("id", "user-123"), - slog.Time("created_at", time.Now()), - ), - ). - With("environment", "dev"). - With("error", fmt.Errorf("an error")). - Error("A message") + // High-volume logging + for i := 0; i < 1000; i++ { + logger. + With( + slog.Group("user", + slog.String("id", "user-123"), + slog.Time("created_at", time.Now()), + ), + ). + With("environment", "dev"). + With("error", fmt.Errorf("an error")). + Error("A message") + } } ``` -### Recover errors: `slog.RecoverHandlerError()` +**Use Cases:** +- High-throughput logging scenarios +- Distributed logging infrastructure +- Performance optimization -Returns a `slog.Handler` that recovers from panics or error of the chain of handlers. +### Recover errors: `slogmulti.RecoverHandlerError()` + +Gracefully handle logging failures without crashing the application. Catches both panics and errors from handlers. ```go import ( - slogformatter "github.com/samber/slog-formatter" - slogmulti "github.com/samber/slog-multi" - "log/slog" + "context" + slogformatter "github.com/samber/slog-formatter" + slogmulti "github.com/samber/slog-multi" + "log/slog" + "os" ) recovery := slogmulti.RecoverHandlerError( @@ -331,19 +366,48 @@ logger.Error("a message", // time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="a message" error.message="an error" error.type="*errors.errorString" user="John doe" very_private_data="********" ``` -### Chaining: `slogmulti.Pipe()` +### Pipelining: `slogmulti.Pipe()` -Rewrite `log.Record` on the fly (eg: for privacy reason). +Transform and filter logs using middleware chains. Perfect for data privacy, formatting, and cross-cutting concerns. ```go +import ( + "context" + slogmulti "github.com/samber/slog-multi" + "log/slog" + "os" + "time" +) + func main() { - // first middleware: format go `error` type into an object {error: "*myCustomErrorType", message: "could not reach https://a.b/c"} - errorFormattingMiddleware := slogmulti.NewHandleInlineMiddleware(errorFormattingMiddleware) + // First middleware: format Go `error` type into an structured object {error: "*myCustomErrorType", message: "could not reach https://a.b/c"} + errorFormattingMiddleware := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { + record.Attrs(func(attr slog.Attr) bool { + if attr.Key == "error" && attr.Value.Kind() == slog.KindAny { + if err, ok := attr.Value.Any().(error); ok { + record.AddAttrs( + slog.String("error_type", "error"), + slog.String("error_message", err.Error()), + ) + } + } + return true + }) + return next(ctx, record) + }) - // second middleware: remove PII - gdprMiddleware := NewGDPRMiddleware() + // Second middleware: remove PII + gdprMiddleware := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { + record.Attrs(func(attr slog.Attr) bool { + if attr.Key == "email" || attr.Key == "phone" || attr.Key == "created_at" { + record.AddAttrs(slog.String(attr.Key, "*********")) + } + return true + }) + return next(ctx, record) + }) - // final handler + // Final handler sink := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{}) logger := slog.New( @@ -378,8 +442,8 @@ Stderr output: "level":"ERROR", "msg":"A message", "user":{ - "id":"*******", "email":"*******", + "phone":"*******", "created_at":"*******" }, "environment":"dev", @@ -391,7 +455,15 @@ Stderr output: } ``` -#### Custom middleware +**Use Cases:** +- Data privacy and GDPR compliance +- Error formatting and standardization +- Log enrichment and transformation +- Performance monitoring and metrics + +## 🔧 Advanced Patterns + +### Custom middleware Middleware must match the following prototype: @@ -403,16 +475,17 @@ The example above uses: - a custom middleware, [see here](./examples/pipe/gdpr.go) - an inline middleware, [see here](./examples/pipe/errors.go) -Note: `WithAttrs` and `WithGroup` methods of custom middleware must return a new instance, instead of `this`. +> **Note**: `WithAttrs` and `WithGroup` methods of custom middleware must return a new instance, not `this`. #### Inline handler -An "inline handler" (aka. lambda), is a shortcut to implement `slog.Handler`, that hooks a single method and proxies others. +Inline handlers provide shortcuts to implement `slog.Handler` without creating full struct implementations. ```go mdw := slogmulti.NewHandleInlineHandler( - // simulate "Handle()" + // simulate "Handle()" method func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error { + // Custom logic here // [...] return nil }, @@ -421,13 +494,15 @@ mdw := slogmulti.NewHandleInlineHandler( ```go mdw := slogmulti.NewInlineHandler( - // simulate "Enabled()" + // simulate "Enabled()" method func(ctx context.Context, groups []string, attrs []slog.Attr, level slog.Level) bool { + // Custom logic here // [...] return true }, - // simulate "Handle()" + // simulate "Handle()" method func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error { + // Custom logic here // [...] return nil }, @@ -436,65 +511,111 @@ mdw := slogmulti.NewInlineHandler( #### Inline middleware -An "inline middleware" (aka. lambda), is a shortcut to implement middleware, that hooks a single method and proxies others. +Inline middleware provides shortcuts to implement middleware functions that hook specific methods. + +#### Hook `Enabled()` Method ```go -// hook `logger.Enabled` method -mdw := slogmulti.NewEnabledInlineMiddleware(func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{ - // [...] +middleware := slogmulti.NewEnabledInlineMiddleware(func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{ + // Custom logic before calling next + if level == slog.LevelDebug { + return false // Skip debug logs + } return next(ctx, level) }) ``` +#### Hook `Handle()` Method + ```go -// hook `logger.Handle` method -mdw := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { - // [...] +middleware := slogmulti.NewHandleInlineMiddleware(func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error { + // Add timestamp to all logs + record.AddAttrs(slog.Time("logged_at", time.Now())) return next(ctx, record) }) ``` +#### Hook `WithAttrs()` Method + ```go -// hook `logger.WithAttrs` method mdw := slogmulti.NewWithAttrsInlineMiddleware(func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler{ - // [...] + // Filter out sensitive attributes + filtered := make([]slog.Attr, 0, len(attrs)) + for _, attr := range attrs { + if attr.Key != "password" && attr.Key != "token" { + filtered = append(filtered, attr) + } + } return next(attrs) }) ``` +#### Hook `WithGroup()` Method + ```go -// hook `logger.WithGroup` method mdw := slogmulti.NewWithGroupInlineMiddleware(func(name string, next func(string) slog.Handler) slog.Handler{ - // [...] + // Add prefix to group names + prefixedName := "app." + name return next(name) }) ``` -A super inline middleware that hooks all methods. +#### Complete Inline Middleware -> Warning: you would rather implement your own middleware. +> **Warning**: You should implement your own middleware for complex scenarios. ```go mdw := slogmulti.NewInlineMiddleware( func(ctx context.Context, level slog.Level, next func(context.Context, slog.Level) bool) bool{ + // Custom logic here // [...] return next(ctx, level) }, func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error{ + // Custom logic here // [...] return next(ctx, record) }, func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler{ + // Custom logic here // [...] return next(attrs) }, func(name string, next func(string) slog.Handler) slog.Handler{ + // Custom logic here // [...] return next(name) }, ) ``` +## 💡 Best Practices + +### Performance Considerations + +- **Use Fanout sparingly**: Broadcasting to many handlers can impact performance +- **Implement sampling**: For high-volume logs, consider sampling strategies +- **Monitor handler performance**: Some handlers (like network-based ones) can be slow +- **Use buffering**: Consider buffering for network-based handlers + +### Error Handling + +- **Always use error recovery**: Wrap handlers with `RecoverHandlerError` +- **Implement fallbacks**: Use failover patterns for critical logging +- **Monitor logging failures**: Track when logging fails to identify issues + +### Security and Privacy + +- **Redact sensitive data**: Use middleware to remove PII and secrets +- **Validate log content**: Ensure logs don't contain sensitive information +- **Use secure connections**: For network-based handlers, use TLS + +### Monitoring and Observability + +- **Add correlation IDs**: Include request IDs in logs for tracing +- **Structured logging**: Use slog's structured logging features consistently +- **Log levels**: Use appropriate log levels for different types of information + ## 🤝 Contributing - Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) @@ -519,7 +640,7 @@ make watch-test ## 💫 Show your support -Give a ⭐️ if this project helped you! +If this project helped you, please give it a ⭐️ on GitHub! [![GitHub Sponsors](https://img.shields.io/github/sponsors/samber?style=for-the-badge)](https://github.com/sponsors/samber) diff --git a/failover.go b/failover.go index 54357b4..340f709 100644 --- a/failover.go +++ b/failover.go @@ -8,14 +8,36 @@ import ( "github.com/samber/lo" ) +// Ensure FailoverHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*FailoverHandler)(nil) -// @TODO: implement round robin strategy ? +// FailoverHandler implements a high-availability logging pattern. +// It attempts to forward log records to handlers in order until one succeeds. +// This is useful for scenarios where you want primary and backup logging destinations. +// +// @TODO: implement round robin strategy for load balancing across multiple handlers type FailoverHandler struct { + // handlers contains the list of slog.Handler instances in priority order + // The first handler that successfully processes a record will be used handlers []slog.Handler } -// Failover forwards records to the first available slog.Handler +// Failover creates a failover handler factory function. +// This function returns a closure that can be used to create failover handlers +// with different sets of handlers. +// +// Example usage: +// +// handler := slogmulti.Failover()( +// primaryHandler, // First choice +// secondaryHandler, // Fallback if primary fails +// backupHandler, // Last resort +// ) +// logger := slog.New(handler) +// +// Returns: +// +// A function that creates FailoverHandler instances with the provided handlers func Failover() func(...slog.Handler) slog.Handler { return func(handlers ...slog.Handler) slog.Handler { return &FailoverHandler{ @@ -24,7 +46,21 @@ func Failover() func(...slog.Handler) slog.Handler { } } -// Implements slog.Handler +// Enabled checks if any of the underlying handlers are enabled for the given log level. +// This method implements the slog.Handler interface requirement. +// +// The handler is considered enabled if at least one of its child handlers +// is enabled for the specified level. This ensures that if any handler +// can process the log, the failover handler will attempt to distribute it. +// +// Args: +// +// ctx: The context for the logging operation +// l: The log level to check +// +// Returns: +// +// true if at least one handler is enabled for the level, false otherwise func (h *FailoverHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { @@ -35,7 +71,20 @@ func (h *FailoverHandler) Enabled(ctx context.Context, l slog.Level) bool { return false } -// Implements slog.Handler +// Handle attempts to process a log record using handlers in priority order. +// This method implements the slog.Handler interface requirement. +// +// This implements a "fail-fast" strategy where the first successful handler +// prevents further attempts, making it efficient for high-availability scenarios. +// +// Args: +// +// ctx: The context for the logging operation +// r: The log record to process +// +// Returns: +// +// nil if any handler successfully processed the record, or the last error encountered func (h *FailoverHandler) Handle(ctx context.Context, r slog.Record) error { var err error @@ -53,7 +102,20 @@ func (h *FailoverHandler) Handle(ctx context.Context, r slog.Record) error { return err } -// Implements slog.Handler +// WithAttrs creates a new FailoverHandler with additional attributes added to all child handlers. +// This method implements the slog.Handler interface requirement. +// +// The method creates new handler instances for each child handler with the additional +// attributes, ensuring that the attributes are properly propagated to all handlers +// in the failover chain. +// +// Args: +// +// attrs: The attributes to add to all handlers +// +// Returns: +// +// A new FailoverHandler with the attributes added to all child handlers func (h *FailoverHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithAttrs(attrs) @@ -61,7 +123,21 @@ func (h *FailoverHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return Failover()(handers...) } -// Implements slog.Handler +// WithGroup creates a new FailoverHandler with a group name applied to all child handlers. +// This method implements the slog.Handler interface requirement. +// +// The method follows the same pattern as the standard slog implementation: +// - If the group name is empty, returns the original handler unchanged +// - Otherwise, creates new handler instances for each child handler with the group name +// +// Args: +// +// name: The group name to apply to all handlers +// +// Returns: +// +// A new FailoverHandler with the group name applied to all child handlers, +// or the original handler if the group name is empty func (h *FailoverHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { diff --git a/middleware.go b/middleware.go index f7aa51c..26eb0c4 100644 --- a/middleware.go +++ b/middleware.go @@ -4,5 +4,25 @@ import ( "log/slog" ) -// Middleware defines the handler used by slog.Handler as return value. +// Middleware is a function type that transforms one slog.Handler into another. +// It follows the standard middleware pattern where a function takes a handler +// and returns a new handler that wraps the original with additional functionality. +// +// Middleware functions can be used to: +// - Transform log records (e.g., add timestamps, modify levels) +// - Filter records based on conditions +// - Add context or attributes to records +// - Implement cross-cutting concerns like error recovery or sampling +// +// Example usage: +// +// gdprMiddleware := NewGDPRMiddleware() +// sink := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{}) +// +// logger := slog.New( +// slogmulti. +// Pipe(gdprMiddleware). +// // ... +// Handler(sink), +// ) type Middleware func(slog.Handler) slog.Handler diff --git a/multi.go b/multi.go index 697e828..05c5d9e 100644 --- a/multi.go +++ b/multi.go @@ -9,20 +9,56 @@ import ( "github.com/samber/lo" ) +// Ensure FanoutHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*FanoutHandler)(nil) +// FanoutHandler distributes log records to multiple slog.Handler instances in parallel. +// It implements the slog.Handler interface and forwards all logging operations to all +// registered handlers that are enabled for the given log level. type FanoutHandler struct { + // handlers contains the list of slog.Handler instances to which log records will be distributed handlers []slog.Handler } -// Fanout distributes records to multiple slog.Handler in parallel +// Fanout creates a new FanoutHandler that distributes records to multiple slog.Handler instances. +// This function is the primary entry point for creating a multi-handler setup. +// +// Example usage: +// +// handler := slogmulti.Fanout( +// slog.NewJSONHandler(os.Stdout, nil), +// slogdatadog.NewDatadogHandler(...), +// ) +// logger := slog.New(handler) +// +// Args: +// +// handlers: Variable number of slog.Handler instances to distribute logs to +// +// Returns: +// +// A slog.Handler that forwards all operations to the provided handlers func Fanout(handlers ...slog.Handler) slog.Handler { return &FanoutHandler{ handlers: handlers, } } -// Implements slog.Handler +// Enabled checks if any of the underlying handlers are enabled for the given log level. +// This method implements the slog.Handler interface requirement. +// +// The handler is considered enabled if at least one of its child handlers +// is enabled for the specified level. This ensures that if any handler +// can process the log, the fanout handler will attempt to distribute it. +// +// Args: +// +// ctx: The context for the logging operation +// l: The log level to check +// +// Returns: +// +// true if at least one handler is enabled for the level, false otherwise func (h *FanoutHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { @@ -33,7 +69,27 @@ func (h *FanoutHandler) Enabled(ctx context.Context, l slog.Level) bool { return false } -// Implements slog.Handler +// Handle distributes a log record to all enabled handlers. +// This method implements the slog.Handler interface requirement. +// +// The method: +// 1. Iterates through all registered handlers +// 2. Checks if each handler is enabled for the record's level +// 3. For enabled handlers, calls their Handle method with a cloned record +// 4. Collects any errors that occur during handling +// 5. Returns a combined error if any handlers failed +// +// Note: Each handler receives a cloned record to prevent interference between handlers. +// This ensures that one handler cannot modify the record for other handlers. +// +// Args: +// +// ctx: The context for the logging operation +// r: The log record to distribute +// +// Returns: +// +// An error if any handler failed to process the record, nil otherwise func (h *FanoutHandler) Handle(ctx context.Context, r slog.Record) error { var errs []error for i := range h.handlers { @@ -51,7 +107,20 @@ func (h *FanoutHandler) Handle(ctx context.Context, r slog.Record) error { return errors.Join(errs...) } -// Implements slog.Handler +// WithAttrs creates a new FanoutHandler with additional attributes added to all child handlers. +// This method implements the slog.Handler interface requirement. +// +// The method creates new handler instances for each child handler with the additional +// attributes, ensuring that the attributes are properly propagated to all handlers +// in the fanout chain. +// +// Args: +// +// attrs: The attributes to add to all handlers +// +// Returns: +// +// A new FanoutHandler with the attributes added to all child handlers func (h *FanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handlers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithAttrs(slices.Clone(attrs)) @@ -59,7 +128,21 @@ func (h *FanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return Fanout(handlers...) } -// Implements slog.Handler +// WithGroup creates a new FanoutHandler with a group name applied to all child handlers. +// This method implements the slog.Handler interface requirement. +// +// The method follows the same pattern as the standard slog implementation: +// - If the group name is empty, returns the original handler unchanged +// - Otherwise, creates new handler instances for each child handler with the group name +// +// Args: +// +// name: The group name to apply to all handlers +// +// Returns: +// +// A new FanoutHandler with the group name applied to all child handlers, +// or the original handler if the group name is empty func (h *FanoutHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { diff --git a/pipe.go b/pipe.go index fa0a1f8..f7d395b 100644 --- a/pipe.go +++ b/pipe.go @@ -4,24 +4,70 @@ import ( "log/slog" ) -// Pipe defines a chain of Middleware. +// PipeBuilder provides a fluent API for building middleware chains. +// It allows you to compose multiple middleware functions that will be applied +// to log records in the order they are added (last-in, first-out). type PipeBuilder struct { + // middlewares contains the list of middleware functions to be applied + // The middlewares are applied in reverse order (LIFO) when building the final handler middlewares []Middleware } -// Pipe builds a chain of Middleware. -// Eg: rewrite log.Record on the fly for privacy reason. +// Pipe creates a new PipeBuilder with the provided middleware functions. +// This function is the entry point for building middleware chains. +// +// Middleware functions are applied in reverse order (last-in, first-out), +// which means the last middleware added will be the first one applied to incoming records. +// This allows for intuitive composition where you can think of the chain as +// "transform A, then transform B, then send to handler". +// +// Example usage: +// +// handler := slogmulti.Pipe( +// RewriteLevel(slog.LevelWarn, slog.LevelInfo), +// RewriteMessage("prefix: %s"), +// RedactPII(), +// ).Handler(finalHandler) +// +// Args: +// +// middlewares: Variable number of middleware functions to chain together +// +// Returns: +// +// A new PipeBuilder instance ready for further configuration func Pipe(middlewares ...Middleware) *PipeBuilder { return &PipeBuilder{middlewares: middlewares} } -// Implements slog.Handler +// Pipe adds an additional middleware to the chain. +// This method provides a fluent API for building middleware chains incrementally. +// +// Args: +// +// middleware: The middleware function to add to the chain +// +// Returns: +// +// The PipeBuilder instance for method chaining func (h *PipeBuilder) Pipe(middleware Middleware) *PipeBuilder { h.middlewares = append(h.middlewares, middleware) return h } -// Implements slog.Handler +// Handler creates a slog.Handler by applying all middleware to the provided handler. +// This method finalizes the middleware chain and returns a handler that can be used with slog.New(). +// +// This LIFO approach ensures that the middleware chain is applied in the intuitive order: +// the first middleware in the chain is applied first to incoming records. +// +// Args: +// +// handler: The final slog.Handler that will receive the transformed records +// +// Returns: +// +// A slog.Handler that applies all middleware transformations before forwarding to the final handler func (h *PipeBuilder) Handler(handler slog.Handler) slog.Handler { for len(h.middlewares) > 0 { middleware := h.middlewares[len(h.middlewares)-1] diff --git a/pool.go b/pool.go index dc40de1..c41fccb 100644 --- a/pool.go +++ b/pool.go @@ -10,15 +10,41 @@ import ( "github.com/samber/lo" ) +// Ensure PoolHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*PoolHandler)(nil) +// PoolHandler implements a load balancing strategy for logging handlers. +// It distributes log records across multiple handlers using a round-robin approach +// with randomization to ensure even distribution and prevent hot-spotting. type PoolHandler struct { + // randSource provides a thread-safe random number generator for load balancing randSource rand.Source - handlers []slog.Handler + // handlers contains the list of slog.Handler instances to distribute records across + handlers []slog.Handler } -// Pool balances records between multiple slog.Handler in order to increase bandwidth. -// Uses a round robin strategy. +// Pool creates a load balancing handler factory function. +// This function returns a closure that can be used to create pool handlers +// with different sets of handlers for load balancing. +// +// The pool uses a round-robin strategy with randomization to distribute +// log records evenly across all available handlers. This is useful for: +// - Increasing logging throughput by parallelizing handler operations +// - Providing redundancy by having multiple handlers process the same records +// - Load balancing across multiple logging destinations +// +// Example usage: +// +// handler := slogmulti.Pool()( +// handler1, // Will receive ~33% of records +// handler2, // Will receive ~33% of records +// handler3, // Will receive ~33% of records +// ) +// logger := slog.New(handler) +// +// Returns: +// +// A function that creates PoolHandler instances with the provided handlers func Pool() func(...slog.Handler) slog.Handler { return func(handlers ...slog.Handler) slog.Handler { return &PoolHandler{ @@ -28,7 +54,21 @@ func Pool() func(...slog.Handler) slog.Handler { } } -// Implements slog.Handler +// Enabled checks if any of the underlying handlers are enabled for the given log level. +// This method implements the slog.Handler interface requirement. +// +// The handler is considered enabled if at least one of its child handlers +// is enabled for the specified level. This ensures that if any handler +// can process the log, the pool handler will attempt to distribute it. +// +// Args: +// +// ctx: The context for the logging operation +// l: The log level to check +// +// Returns: +// +// true if at least one handler is enabled for the level, false otherwise func (h *PoolHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h.handlers { if h.handlers[i].Enabled(ctx, l) { @@ -39,13 +79,26 @@ func (h *PoolHandler) Enabled(ctx context.Context, l slog.Level) bool { return false } -// Implements slog.Handler +// Handle distributes a log record to a handler selected using round-robin with randomization. +// This method implements the slog.Handler interface requirement. +// +// This approach ensures even distribution of load while providing fault tolerance +// through the failover behavior when a handler is unavailable. +// +// Args: +// +// ctx: The context for the logging operation +// r: The log record to distribute +// +// Returns: +// +// nil if any handler successfully processed the record, or the last error encountered func (h *PoolHandler) Handle(ctx context.Context, r slog.Record) error { if len(h.handlers) == 0 { return nil } - // round robin + // round robin with randomization rand := h.randSource.Int63() % int64(len(h.handlers)) handlers := append(h.handlers[rand:], h.handlers[:rand]...) @@ -65,7 +118,20 @@ func (h *PoolHandler) Handle(ctx context.Context, r slog.Record) error { return err } -// Implements slog.Handler +// WithAttrs creates a new PoolHandler with additional attributes added to all child handlers. +// This method implements the slog.Handler interface requirement. +// +// The method creates new handler instances for each child handler with the additional +// attributes, ensuring that the attributes are properly propagated to all handlers +// in the pool. +// +// Args: +// +// attrs: The attributes to add to all handlers +// +// Returns: +// +// A new PoolHandler with the attributes added to all child handlers func (h *PoolHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler { return h.WithAttrs(attrs) @@ -73,7 +139,21 @@ func (h *PoolHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return Pool()(handers...) } -// Implements slog.Handler +// WithGroup creates a new PoolHandler with a group name applied to all child handlers. +// This method implements the slog.Handler interface requirement. +// +// The method follows the same pattern as the standard slog implementation: +// - If the group name is empty, returns the original handler unchanged +// - Otherwise, creates new handler instances for each child handler with the group name +// +// Args: +// +// name: The group name to apply to all handlers +// +// Returns: +// +// A new PoolHandler with the group name applied to all child handlers, +// or the original handler if the group name is empty func (h *PoolHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { diff --git a/recover.go b/recover.go index 25442f5..b76e7fe 100644 --- a/recover.go +++ b/recover.go @@ -6,16 +6,49 @@ import ( "log/slog" ) +// RecoveryFunc is a callback function that handles errors and panics from logging handlers. +// It receives the context, the log record that caused the error, and the error itself. +// This function can be used to log the error, send alerts, or perform any other +// error handling logic without affecting the main application flow. type RecoveryFunc func(ctx context.Context, record slog.Record, err error) +// Ensure HandlerErrorRecovery implements the slog.Handler interface at compile time var _ slog.Handler = (*HandlerErrorRecovery)(nil) +// HandlerErrorRecovery wraps a slog.Handler to provide panic and error recovery. +// It catches both panics and errors from the underlying handler and calls +// a recovery function to handle them gracefully. type HandlerErrorRecovery struct { + // recovery is the function called when an error or panic occurs recovery RecoveryFunc - handler slog.Handler + // handler is the underlying slog.Handler that this recovery wrapper protects + handler slog.Handler } -// RecoverHandlerError returns a slog.Handler that recovers from panics or error of the chain of handlers. +// RecoverHandlerError creates a middleware that adds error recovery to a slog.Handler. +// This function returns a closure that can be used to wrap handlers with recovery logic. +// +// The recovery handler provides fault tolerance by: +// 1. Catching panics from the underlying handler +// 2. Catching errors returned by the underlying handler +// 3. Calling the recovery function with the error details +// 4. Propagating the original error to maintain logging semantics +// +// Example usage: +// +// recovery := slogmulti.RecoverHandlerError(func(ctx context.Context, record slog.Record, err error) { +// fmt.Printf("Logging error: %v\n", err) +// }) +// safeHandler := recovery(riskyHandler) +// logger := slog.New(safeHandler) +// +// Args: +// +// recovery: The function to call when an error or panic occurs +// +// Returns: +// +// A function that wraps handlers with recovery logic func RecoverHandlerError(recovery RecoveryFunc) func(slog.Handler) slog.Handler { return func(handler slog.Handler) slog.Handler { return &HandlerErrorRecovery{ @@ -25,12 +58,35 @@ func RecoverHandlerError(recovery RecoveryFunc) func(slog.Handler) slog.Handler } } -// Enabled implements slog.Handler. +// Enabled checks if the underlying handler is enabled for the given log level. +// This method implements the slog.Handler interface requirement. +// +// Args: +// +// ctx: The context for the logging operation +// l: The log level to check +// +// Returns: +// +// true if the underlying handler is enabled for the level, false otherwise func (h *HandlerErrorRecovery) Enabled(ctx context.Context, l slog.Level) bool { return h.handler.Enabled(ctx, l) } -// Handle implements slog.Handler. +// Handle processes a log record with error recovery. +// This method implements the slog.Handler interface requirement. +// +// This ensures that logging errors don't crash the application while still +// allowing the error to be handled appropriately by the calling code. +// +// Args: +// +// ctx: The context for the logging operation +// record: The log record to process +// +// Returns: +// +// The error from the underlying handler (never nil if an error occurred) func (h *HandlerErrorRecovery) Handle(ctx context.Context, record slog.Record) error { defer func() { if r := recover(); r != nil { @@ -51,7 +107,16 @@ func (h *HandlerErrorRecovery) Handle(ctx context.Context, record slog.Record) e return err } -// WithAttrs implements slog.Handler. +// WithAttrs creates a new HandlerErrorRecovery with additional attributes. +// This method implements the slog.Handler interface requirement. +// +// Args: +// +// attrs: The attributes to add to the underlying handler +// +// Returns: +// +// A new HandlerErrorRecovery with the additional attributes func (h *HandlerErrorRecovery) WithAttrs(attrs []slog.Attr) slog.Handler { return &HandlerErrorRecovery{ recovery: h.recovery, @@ -59,7 +124,20 @@ func (h *HandlerErrorRecovery) WithAttrs(attrs []slog.Attr) slog.Handler { } } -// WithGroup implements slog.Handler. +// WithGroup creates a new HandlerErrorRecovery with a group name. +// This method implements the slog.Handler interface requirement. +// +// The method follows the same pattern as the standard slog implementation: +// - If the group name is empty, returns the original handler unchanged +// - Otherwise, creates a new handler with the group name applied to the underlying handler +// +// Args: +// +// name: The group name to apply to the underlying handler +// +// Returns: +// +// A new HandlerErrorRecovery with the group name, or the original handler if the name is empty func (h *HandlerErrorRecovery) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { diff --git a/router.go b/router.go index 67f7ecd..e2a1d0b 100644 --- a/router.go +++ b/router.go @@ -13,14 +13,36 @@ type router struct { handlers []slog.Handler } -// Router forwards records to all matching slog.Handler. +// Router creates a new router instance for building conditional log routing. +// This function is the entry point for creating a routing configuration. +// +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.Level(slog.LevelInfo)). +// Add(fileHandler, slogmulti.Level(slog.LevelError)). +// Handler() +// +// Returns: +// +// A new router instance ready for configuration func Router() *router { return &router{ handlers: []slog.Handler{}, } } -// Add a new handler to the router. The handler will be called if all matchers return true. +// Add registers a new handler with optional matchers to the router. +// The handler will only process records if all provided matchers return true. +// +// Args: +// +// handler: The slog.Handler to register +// matchers: Optional functions that determine if a record should be routed to this handler +// +// Returns: +// +// The router instance for method chaining func (h *router) Add(handler slog.Handler, matchers ...func(ctx context.Context, r slog.Record) bool) *router { return &router{ handlers: append( @@ -35,26 +57,62 @@ func (h *router) Add(handler slog.Handler, matchers ...func(ctx context.Context, } } +// Handler creates a slog.Handler from the configured router. +// This method finalizes the routing configuration and returns a handler +// that can be used with slog.New(). +// +// Returns: +// +// A slog.Handler that implements the routing logic func (h *router) Handler() slog.Handler { return Fanout(h.handlers...) } +// Ensure RoutableHandler implements the slog.Handler interface at compile time var _ slog.Handler = (*RoutableHandler)(nil) -// @TODO: implement round robin strategy ? +// RoutableHandler wraps a slog.Handler with conditional matching logic. +// It only forwards records to the underlying handler if all matchers return true. +// This enables sophisticated routing scenarios like level-based or attribute-based routing. +// +// @TODO: implement round robin strategy for load balancing across multiple handlers type RoutableHandler struct { + // matchers contains functions that determine if a record should be processed matchers []func(ctx context.Context, r slog.Record) bool - handler slog.Handler - groups []string - attrs []slog.Attr + // handler is the underlying slog.Handler that processes matching records + handler slog.Handler + // groups tracks the current group hierarchy for proper attribute handling + groups []string + // attrs contains accumulated attributes that should be added to records + attrs []slog.Attr } -// Implements slog.Handler +// Enabled checks if the underlying handler is enabled for the given log level. +// This method implements the slog.Handler interface requirement. +// +// Args: +// +// ctx: The context for the logging operation +// l: The log level to check +// +// Returns: +// +// true if the underlying handler is enabled for the level, false otherwise func (h *RoutableHandler) Enabled(ctx context.Context, l slog.Level) bool { return h.handler.Enabled(ctx, l) } -// Implements slog.Handler +// Handle processes a log record if all matchers return true. +// This method implements the slog.Handler interface requirement. +// +// Args: +// +// ctx: The context for the logging operation +// r: The log record to process +// +// Returns: +// +// An error if the underlying handler failed to process the record, nil otherwise func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { clone := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) clone.AddAttrs( @@ -70,7 +128,19 @@ func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { return h.handler.Handle(ctx, r) } -// Implements slog.Handler +// WithAttrs creates a new RoutableHandler with additional attributes. +// This method implements the slog.Handler interface requirement. +// +// The method properly handles attribute accumulation within the current group context, +// ensuring that attributes are correctly applied to records when they are processed. +// +// Args: +// +// attrs: The attributes to add to the handler +// +// Returns: +// +// A new RoutableHandler with the additional attributes func (h *RoutableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &RoutableHandler{ matchers: h.matchers, @@ -80,7 +150,20 @@ func (h *RoutableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { } } -// Implements slog.Handler +// WithGroup creates a new RoutableHandler with a group name. +// This method implements the slog.Handler interface requirement. +// +// The method follows the same pattern as the standard slog implementation: +// - If the group name is empty, returns the original handler unchanged +// - Otherwise, creates a new handler with the group name added to the group hierarchy +// +// Args: +// +// name: The group name to apply to the handler +// +// Returns: +// +// A new RoutableHandler with the group name, or the original handler if the name is empty func (h *RoutableHandler) WithGroup(name string) slog.Handler { // https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247 if name == "" { From 7e1ca96dfa53aec05c18d7c491ac28d9c4d23639 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 24 Jun 2025 21:25:43 +0200 Subject: [PATCH 11/21] doc: improved comments+readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a82676..40849d8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# slog-multi: Advanced Handler Composition for Go's Structured Logging (chaining, fanout, routing, failover...) +# slog-multi: Advanced Handler Composition for Go's Structured Logging (pipelining, fanout, routing, failover...) [![tag](https://img.shields.io/github/tag/samber/slog-multi.svg)](https://github.com/samber/slog-multi/releases) ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c) From b6228510ea3253fdae8a329665bcb7ff7f45ec6c Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 29 Jul 2025 13:58:58 +0200 Subject: [PATCH 12/21] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 40849d8..58eb56e 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ Middlewares:
Sponsored by:
- +
- Quickwit + Dash0
- Cloud-native search engine for observability - An OSS alternative to Splunk, Elasticsearch, Loki, and Tempo. + 100% OpenTelemetry-native observability platform—simple to use, built on open standards, and designed for full cost control.

From 9636bea534eadfd289bc9def39095e005445f031 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 29 Jul 2025 16:02:54 +0200 Subject: [PATCH 13/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58eb56e..ba9b04b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Middlewares:
Sponsored by:
- +
Dash0
From fcd19d76792a59b77f32934827647f1f8e5e4edf Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 29 Jul 2025 16:40:37 +0200 Subject: [PATCH 14/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba9b04b..4c6b6ae 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Middlewares: Dash0
- 100% OpenTelemetry-native observability platform—simple to use, built on open standards, and designed for full cost control. + 100% OpenTelemetry-native observability platform—simple to use
Built on open standards, and designed for full cost control

From a7efdb3293df6ca1513b74b8de4da315f047a7ac Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 29 Jul 2025 16:42:51 +0200 Subject: [PATCH 15/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c6b6ae..d6e1579 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Middlewares: Dash0
- 100% OpenTelemetry-native observability platform—simple to use
Built on open standards, and designed for full cost control + 100% OpenTelemetry-native observability platform
Simple to use, built on open standards, and designed for full cost control

From 5b4415719e284ce96a6248f7f673ac767bd5ba14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:51:45 +0200 Subject: [PATCH 16/21] chore(deps): bump actions/checkout from 4 to 5 (#34) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 35f8b78..110301d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: with: go-version: 1.21 stable: false - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e2122a..c4875e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: if: github.triggering_actor == 'samber' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f24bc1..a511ad5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - '1.24' - '1.x' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 From f89e77a81a5a0f612c901efd19e74f024208e4b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:06:36 +0200 Subject: [PATCH 17/21] chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.0 (#35) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.10.0 to 1.11.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-version: 1.11.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4f0a849..1f131c4 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/samber/lo v1.51.0 github.com/samber/slog-common v0.19.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.0 go.uber.org/goleak v1.3.0 ) diff --git a/go.sum b/go.sum index a1438f7..207ac9e 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= From f4d3d75b402e5497b14e6110a83a6731ebd8c6f5 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Tue, 2 Sep 2025 16:22:33 +0200 Subject: [PATCH 18/21] Update dependabot.yml --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 632e8eb..61b1f35 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,8 @@ updates: - package-ecosystem: github-actions directory: / schedule: - interval: weekly + interval: monthly - package-ecosystem: gomod directory: / schedule: - interval: weekly + interval: monthly From 27220630419966c44a2fefdb39b15b49b62284ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:08:42 +0200 Subject: [PATCH 19/21] chore(deps): bump github.com/stretchr/testify from 1.11.0 to 1.11.1 (#38) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.0 to 1.11.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.11.0...v1.11.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-version: 1.11.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f131c4..8d6deb6 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/samber/lo v1.51.0 github.com/samber/slog-common v0.19.0 - github.com/stretchr/testify v1.11.0 + github.com/stretchr/testify v1.11.1 go.uber.org/goleak v1.3.0 ) diff --git a/go.sum b/go.sum index 207ac9e..52342bb 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= From 5ec57db0c5418d2fc0a6ac13b56492e4fbae9a20 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Thu, 4 Sep 2025 10:39:41 +0200 Subject: [PATCH 20/21] feat: adding some router predicates (#37) * feat: adding some router predicates * Update router.go --- README.md | 2 +- router.go | 48 +++++++-------- router_predicate.go | 143 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 router_predicate.go diff --git a/README.md b/README.md index d6e1579..72a446c 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ func main() { Add(slackChannelUS, recordMatchRegion("us")). Add(slackChannelEU, recordMatchRegion("eu")). Add(slackChannelAPAC, recordMatchRegion("apac")). - Add(consoleHandler, slogmulti.Level(slog.LevelInfo)). + Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo, slog.LevelDebug)). Handler(), ) diff --git a/router.go b/router.go index e2a1d0b..d655164 100644 --- a/router.go +++ b/router.go @@ -19,8 +19,8 @@ type router struct { // Example usage: // // r := slogmulti.Router(). -// Add(consoleHandler, slogmulti.Level(slog.LevelInfo)). -// Add(fileHandler, slogmulti.Level(slog.LevelError)). +// Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo)). +// Add(fileHandler, slogmulti.LevelIs(slog.LevelError)). // Handler() // // Returns: @@ -32,26 +32,26 @@ func Router() *router { } } -// Add registers a new handler with optional matchers to the router. -// The handler will only process records if all provided matchers return true. +// Add registers a new handler with optional predicates to the router. +// The handler will only process records if all provided predicates return true. // // Args: // // handler: The slog.Handler to register -// matchers: Optional functions that determine if a record should be routed to this handler +// predicates: Optional functions that determine if a record should be routed to this handler // // Returns: // // The router instance for method chaining -func (h *router) Add(handler slog.Handler, matchers ...func(ctx context.Context, r slog.Record) bool) *router { +func (h *router) Add(handler slog.Handler, predicates ...func(ctx context.Context, r slog.Record) bool) *router { return &router{ handlers: append( h.handlers, &RoutableHandler{ - matchers: matchers, - handler: handler, - groups: []string{}, - attrs: []slog.Attr{}, + predicates: predicates, + handler: handler, + groups: []string{}, + attrs: []slog.Attr{}, }, ), } @@ -72,13 +72,13 @@ func (h *router) Handler() slog.Handler { var _ slog.Handler = (*RoutableHandler)(nil) // RoutableHandler wraps a slog.Handler with conditional matching logic. -// It only forwards records to the underlying handler if all matchers return true. +// It only forwards records to the underlying handler if all predicates return true. // This enables sophisticated routing scenarios like level-based or attribute-based routing. // // @TODO: implement round robin strategy for load balancing across multiple handlers type RoutableHandler struct { - // matchers contains functions that determine if a record should be processed - matchers []func(ctx context.Context, r slog.Record) bool + // predicates contains functions that determine if a record should be processed + predicates []func(ctx context.Context, r slog.Record) bool // handler is the underlying slog.Handler that processes matching records handler slog.Handler // groups tracks the current group hierarchy for proper attribute handling @@ -102,7 +102,7 @@ func (h *RoutableHandler) Enabled(ctx context.Context, l slog.Level) bool { return h.handler.Enabled(ctx, l) } -// Handle processes a log record if all matchers return true. +// Handle processes a log record if all predicates return true. // This method implements the slog.Handler interface requirement. // // Args: @@ -119,8 +119,8 @@ func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { slogcommon.AppendRecordAttrsToAttrs(h.attrs, h.groups, &r)..., ) - for _, matcher := range h.matchers { - if !matcher(ctx, clone) { + for _, predicate := range h.predicates { + if !predicate(ctx, clone) { return nil } } @@ -143,10 +143,10 @@ func (h *RoutableHandler) Handle(ctx context.Context, r slog.Record) error { // A new RoutableHandler with the additional attributes func (h *RoutableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &RoutableHandler{ - matchers: h.matchers, - handler: h.handler.WithAttrs(attrs), - groups: slices.Clone(h.groups), - attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), + predicates: h.predicates, + handler: h.handler.WithAttrs(attrs), + groups: slices.Clone(h.groups), + attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), } } @@ -171,9 +171,9 @@ func (h *RoutableHandler) WithGroup(name string) slog.Handler { } return &RoutableHandler{ - matchers: h.matchers, - handler: h.handler.WithGroup(name), - groups: append(slices.Clone(h.groups), name), - attrs: h.attrs, + predicates: h.predicates, + handler: h.handler.WithGroup(name), + groups: append(slices.Clone(h.groups), name), + attrs: h.attrs, } } diff --git a/router_predicate.go b/router_predicate.go new file mode 100644 index 0000000..93df86d --- /dev/null +++ b/router_predicate.go @@ -0,0 +1,143 @@ +package slogmulti + +import ( + "context" + "log/slog" + "strings" +) + +// LevelIs returns a function that checks if the record level is in the given levels. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.LevelIs(slog.LevelInfo)). +// Add(fileHandler, slogmulti.LevelIs(slog.LevelError)). +// Handler() +// +// Args: +// +// levels: The levels to match +// +// Returns: +// +// A function that checks if the record level is in the given levels +func LevelIs(levels ...slog.Level) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + for _, level := range levels { + if r.Level == level { + return true + } + } + return false + } +} + +// LevelIsNot returns a function that checks if the record level is not in the given levels. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.LevelIsNot(slog.LevelInfo)). +// Add(fileHandler, slogmulti.LevelIsNot(slog.LevelError)). +// Handler() +// +// Args: +// +// levels: The levels to check +// +// Returns: +// +// A function that checks if the record level is not in the given levels +func LevelIsNot(levels ...slog.Level) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + for _, level := range levels { + if r.Level == level { + return false + } + } + return true + } +} + +// MessageIs returns a function that checks if the record message is equal to the given message. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageIs("database error")). +// Add(fileHandler, slogmulti.MessageIs("database error")). +// Handler() +// +// Args: +// +// msg: The message to check +// +// Returns: +// +// A function that checks if the record message is equal to the given message +func MessageIs(msg string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return r.Message == msg + } +} + +// MessageIsNot returns a function that checks if the record message is not equal to the given message. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageIsNot("database error")). +// Add(fileHandler, slogmulti.MessageIsNot("database error")). +// Handler() +// +// Args: +// +// msg: The message to check +// +// Returns: +// +// A function that checks if the record message is not equal to the given message +func MessageIsNot(msg string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return r.Message != msg + } +} + +// MessageContains returns a function that checks if the record message contains the given part. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageContains("database error")). +// Add(fileHandler, slogmulti.MessageContains("database error")). +// Handler() +// +// Args: +// +// part: The part to check +// +// Returns: +// +// A function that checks if the record message contains the given part +func MessageContains(part string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return strings.Contains(r.Message, part) + } +} + +// MessageNotContains returns a function that checks if the record message does not contain the given part. +// Example usage: +// +// r := slogmulti.Router(). +// Add(consoleHandler, slogmulti.MessageNotContains("database error")). +// Add(fileHandler, slogmulti.MessageNotContains("database error")). +// Handler() +// +// Args: +// +// part: The part to check +// +// Returns: +// +// A function that checks if the record message does not contain the given part +func MessageNotContains(part string) func(ctx context.Context, r slog.Record) bool { + return func(ctx context.Context, r slog.Record) bool { + return !strings.Contains(r.Message, part) + } +} From 84c6c3c6289cbec4a0f0bec413c23eaac6f5a744 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:13:12 +0200 Subject: [PATCH 21/21] chore(deps): bump actions/setup-go from 5 to 6 (#39) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 110301d..d8d8c28 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: 1.21 stable: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4875e2..526b0f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.21 stable: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a511ad5..f234686 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} stable: false