├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── lint.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── example
└── example.go
├── filters.go
├── go.mod
├── go.sum
├── main_test.go
└── middleware.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [samber]
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: weekly
7 | - package-ecosystem: gomod
8 | directory: /
9 | schedule:
10 | interval: weekly
11 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | golangci:
9 | name: lint
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/setup-go@v5
13 | with:
14 | go-version: 1.21
15 | stable: false
16 | - uses: actions/checkout@v4
17 | - name: golangci-lint
18 | uses: golangci/golangci-lint-action@v8
19 | with:
20 | args: --timeout 120s --max-same-issues 50
21 |
22 | - name: Bearer
23 | uses: bearer/bearer-action@v2
24 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | semver:
7 | type: string
8 | description: 'Semver (eg: v1.2.3)'
9 | required: true
10 |
11 | jobs:
12 | release:
13 | if: github.triggering_actor == 'samber'
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | - name: Set up Go
19 | uses: actions/setup-go@v5
20 | with:
21 | go-version: 1.21
22 | stable: false
23 |
24 | - name: Test
25 | run: make test
26 |
27 | # remove tests in order to clean dependencies
28 | - name: Remove xxx_test.go files
29 | run: rm -rf *_test.go ./examples ./images
30 |
31 | # cleanup test dependencies
32 | - name: Cleanup dependencies
33 | run: go mod tidy
34 |
35 | - name: List files
36 | run: tree -Cfi
37 | - name: Write new go.mod into logs
38 | run: cat go.mod
39 | - name: Write new go.sum into logs
40 | run: cat go.sum
41 |
42 | - name: Create tag
43 | run: |
44 | git config --global user.name '${{ github.triggering_actor }}'
45 | git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com"
46 |
47 | git add .
48 | git commit --allow-empty -m 'bump ${{ inputs.semver }}'
49 | git tag ${{ inputs.semver }}
50 | git push origin ${{ inputs.semver }}
51 |
52 | - name: Release
53 | uses: softprops/action-gh-release@v2
54 | with:
55 | name: ${{ inputs.semver }}
56 | tag_name: ${{ inputs.semver }}
57 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | tags:
6 | branches:
7 | pull_request:
8 |
9 | jobs:
10 |
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | go:
16 | - '1.21'
17 | - '1.22'
18 | - '1.23'
19 | - '1.24'
20 | - '1.x'
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - name: Set up Go
25 | uses: actions/setup-go@v5
26 | with:
27 | go-version: ${{ matrix.go }}
28 | stable: false
29 |
30 | - name: Build
31 | run: make build
32 |
33 | - name: Test
34 | run: make test
35 |
36 | - name: Test
37 | run: make coverage
38 |
39 | - name: Codecov
40 | uses: codecov/codecov-action@v5
41 | with:
42 | token: ${{ secrets.CODECOV_TOKEN }}
43 | file: ./cover.out
44 | flags: unittests
45 | verbose: true
46 | if: matrix.go == '1.21'
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/go
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=go
4 |
5 | ### Go ###
6 | # If you prefer the allow list template instead of the deny list, see community template:
7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
8 | #
9 | # Binaries for programs and plugins
10 | *.exe
11 | *.exe~
12 | *.dll
13 | *.so
14 | *.dylib
15 |
16 | # Test binary, built with `go test -c`
17 | *.test
18 |
19 | # Output of the go coverage tool, specifically when used with LiteIDE
20 | *.out
21 |
22 | # Dependency directories (remove the comment below to include it)
23 | # vendor/
24 |
25 | # Go workspace file
26 | go.work
27 |
28 | ### Go Patch ###
29 | /vendor/
30 | /Godeps/
31 |
32 | # End of https://www.toptal.com/developers/gitignore/api/go
33 |
34 | cover.out
35 | cover.html
36 | .vscode
37 |
38 | .idea/
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Samuel Berthe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | build:
3 | go build -v ./...
4 |
5 | test:
6 | go test -race -v ./...
7 | watch-test:
8 | reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
9 |
10 | bench:
11 | go test -benchmem -count 3 -bench ./...
12 | watch-bench:
13 | reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
14 |
15 | coverage:
16 | go test -v -coverprofile=cover.out -covermode=atomic ./...
17 | go tool cover -html=cover.out -o cover.html
18 |
19 | tools:
20 | go install github.com/cespare/reflex@latest
21 | go install github.com/rakyll/gotest@latest
22 | go install github.com/psampaz/go-mod-outdated@latest
23 | go install github.com/jondot/goweight@latest
24 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
25 | go get -t -u golang.org/x/tools/cmd/cover
26 | go install github.com/sonatype-nexus-community/nancy@latest
27 | go mod tidy
28 |
29 | lint:
30 | golangci-lint run --timeout 60s --max-same-issues 50 ./...
31 | lint-fix:
32 | golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
33 |
34 | audit:
35 | go list -json -m all | nancy sleuth
36 |
37 | outdated:
38 | go list -u -m -json all | go-mod-outdated -update -direct
39 |
40 | weight:
41 | goweight
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # slog: Fiber middleware
3 |
4 | [](https://github.com/samber/slog-fiber/releases)
5 | 
6 | [](https://pkg.go.dev/github.com/samber/slog-fiber)
7 | 
8 | [](https://goreportcard.com/report/github.com/samber/slog-fiber)
9 | [](https://codecov.io/gh/samber/slog-fiber)
10 | [](https://github.com/samber/slog-fiber/graphs/contributors)
11 | [](./LICENSE)
12 |
13 | [Fiber](https://github.com/gofiber/fiber) middleware to log http requests using [slog](https://pkg.go.dev/log/slog).
14 |
15 |
29 |
30 | **See also:**
31 |
32 | - [slog-multi](https://github.com/samber/slog-multi): `slog.Handler` chaining, fanout, routing, failover, load balancing...
33 | - [slog-formatter](https://github.com/samber/slog-formatter): `slog` attribute formatting
34 | - [slog-sampling](https://github.com/samber/slog-sampling): `slog` sampling policy
35 | - [slog-mock](https://github.com/samber/slog-mock): `slog.Handler` for test purposes
36 |
37 | **HTTP middlewares:**
38 |
39 | - [slog-gin](https://github.com/samber/slog-gin): Gin middleware for `slog` logger
40 | - [slog-echo](https://github.com/samber/slog-echo): Echo middleware for `slog` logger
41 | - [slog-fiber](https://github.com/samber/slog-fiber): Fiber middleware for `slog` logger
42 | - [slog-chi](https://github.com/samber/slog-chi): Chi middleware for `slog` logger
43 | - [slog-http](https://github.com/samber/slog-http): `net/http` middleware for `slog` logger
44 |
45 | **Loggers:**
46 |
47 | - [slog-zap](https://github.com/samber/slog-zap): A `slog` handler for `Zap`
48 | - [slog-zerolog](https://github.com/samber/slog-zerolog): A `slog` handler for `Zerolog`
49 | - [slog-logrus](https://github.com/samber/slog-logrus): A `slog` handler for `Logrus`
50 |
51 | **Log sinks:**
52 |
53 | - [slog-datadog](https://github.com/samber/slog-datadog): A `slog` handler for `Datadog`
54 | - [slog-betterstack](https://github.com/samber/slog-betterstack): A `slog` handler for `Betterstack`
55 | - [slog-rollbar](https://github.com/samber/slog-rollbar): A `slog` handler for `Rollbar`
56 | - [slog-loki](https://github.com/samber/slog-loki): A `slog` handler for `Loki`
57 | - [slog-sentry](https://github.com/samber/slog-sentry): A `slog` handler for `Sentry`
58 | - [slog-syslog](https://github.com/samber/slog-syslog): A `slog` handler for `Syslog`
59 | - [slog-logstash](https://github.com/samber/slog-logstash): A `slog` handler for `Logstash`
60 | - [slog-fluentd](https://github.com/samber/slog-fluentd): A `slog` handler for `Fluentd`
61 | - [slog-graylog](https://github.com/samber/slog-graylog): A `slog` handler for `Graylog`
62 | - [slog-quickwit](https://github.com/samber/slog-quickwit): A `slog` handler for `Quickwit`
63 | - [slog-slack](https://github.com/samber/slog-slack): A `slog` handler for `Slack`
64 | - [slog-telegram](https://github.com/samber/slog-telegram): A `slog` handler for `Telegram`
65 | - [slog-mattermost](https://github.com/samber/slog-mattermost): A `slog` handler for `Mattermost`
66 | - [slog-microsoft-teams](https://github.com/samber/slog-microsoft-teams): A `slog` handler for `Microsoft Teams`
67 | - [slog-webhook](https://github.com/samber/slog-webhook): A `slog` handler for `Webhook`
68 | - [slog-kafka](https://github.com/samber/slog-kafka): A `slog` handler for `Kafka`
69 | - [slog-nats](https://github.com/samber/slog-nats): A `slog` handler for `NATS`
70 | - [slog-parquet](https://github.com/samber/slog-parquet): A `slog` handler for `Parquet` + `Object Storage`
71 | - [slog-channel](https://github.com/samber/slog-channel): A `slog` handler for Go channels
72 |
73 | ## 🚀 Install
74 |
75 | ```sh
76 | # Fiber v2 (current)
77 | go get github.com/samber/slog-fiber
78 |
79 | # Fiber v3 (beta)
80 | go get github.com/samber/slog-fiber@fiber-v3
81 | ```
82 |
83 | **Compatibility**: go >= 1.21
84 |
85 | No breaking changes will be made to exported APIs before v2.0.0.
86 |
87 | ## 💡 Usage
88 |
89 | ### Handler options
90 |
91 | ```go
92 | type Config struct {
93 | DefaultLevel slog.Level
94 | ClientErrorLevel slog.Level
95 | ServerErrorLevel slog.Level
96 |
97 | WithUserAgent bool
98 | WithRequestID bool
99 | WithRequestBody bool
100 | WithRequestHeader bool
101 | WithResponseBody bool
102 | WithResponseHeader bool
103 | WithSpanID bool
104 | WithTraceID bool
105 |
106 | Filters []Filter
107 | }
108 | ```
109 |
110 | Attributes will be injected in log payload.
111 |
112 | Other global parameters:
113 |
114 | ```go
115 | slogfiber.TraceIDKey = "trace_id"
116 | slogfiber.SpanIDKey = "span_id"
117 | slogfiber.RequestBodyMaxSize = 64 * 1024 // 64KB
118 | slogfiber.ResponseBodyMaxSize = 64 * 1024 // 64KB
119 | slogfiber.HiddenRequestHeaders = map[string]struct{}{ ... }
120 | slogfiber.HiddenResponseHeaders = map[string]struct{}{ ... }
121 | slogfiber.RequestIDHeaderKey = "X-Request-Id"
122 | ```
123 |
124 | ### Minimal
125 |
126 | ```go
127 | import (
128 | "github.com/gofiber/fiber/v2"
129 | "github.com/gofiber/fiber/v2/middleware/recover"
130 | slogfiber "github.com/samber/slog-fiber"
131 | "log/slog"
132 | )
133 |
134 | // Create a slog logger, which:
135 | // - Logs to stdout.
136 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
137 |
138 | app := fiber.New()
139 |
140 | app.Use(slogfiber.New(logger))
141 | app.Use(recover.New())
142 |
143 | app.Get("/", func(c *fiber.Ctx) error {
144 | return c.SendString("Hello, World 👋!")
145 | })
146 |
147 | app.Listen(":4242")
148 |
149 | // output:
150 | // time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Incoming request" env=production request.time=2023-10-15T20:32:58.626+02:00 request.method=GET request.path=/ request.route="" request.ip=127.0.0.1:63932 request.length=0 response.time=2023-10-15T20:32:58.926+02:00 response.latency=100ms response.status=200 response.length=7 id=229c7fc8-64f5-4467-bc4a-940700503b0d
151 | ```
152 |
153 | ### OTEL
154 |
155 | ```go
156 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
157 |
158 | config := slogfiber.Config{
159 | WithSpanID: true,
160 | WithTraceID: true,
161 | }
162 |
163 | app := fiber.New()
164 | app.Use(slogfiber.NewWithConfig(logger, config))
165 | ```
166 |
167 | ### Custom log levels
168 |
169 | ```go
170 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
171 |
172 | config := slogfiber.Config{
173 | DefaultLevel: slog.LevelInfo,
174 | ClientErrorLevel: slog.LevelWarn,
175 | ServerErrorLevel: slog.LevelError,
176 | }
177 |
178 | app := fiber.New()
179 | app.Use(slogfiber.NewWithConfig(logger, config))
180 | ```
181 |
182 | ### Verbose
183 |
184 | ```go
185 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
186 |
187 | config := slogfiber.Config{
188 | WithRequestBody: true,
189 | WithResponseBody: true,
190 | WithRequestHeader: true,
191 | WithResponseHeader: true,
192 | }
193 |
194 | app := fiber.New()
195 | app.Use(slogfiber.NewWithConfig(logger, config))
196 | ```
197 |
198 | ### Filters
199 |
200 | ```go
201 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
202 |
203 | app := fiber.New()
204 | app.Use(
205 | slogfiber.NewWithFilters(
206 | logger,
207 | slogfiber.Accept(func (c *fiber.Ctx) bool {
208 | return xxx
209 | }),
210 | slogfiber.IgnoreStatus(401, 404),
211 | ),
212 | )
213 | app.Use(recover.New())
214 | ```
215 |
216 | Available filters:
217 | - Accept / Ignore
218 | - AcceptMethod / IgnoreMethod
219 | - AcceptStatus / IgnoreStatus
220 | - AcceptStatusGreaterThan / IgnoreStatusGreaterThan
221 | - AcceptStatusLessThan / IgnoreStatusLessThan
222 | - AcceptStatusGreaterThanOrEqual / IgnoreStatusGreaterThanOrEqual
223 | - AcceptStatusLessThanOrEqual / IgnoreStatusLessThanOrEqual
224 | - AcceptPath / IgnorePath
225 | - AcceptPathContains / IgnorePathContains
226 | - AcceptPathPrefix / IgnorePathPrefix
227 | - AcceptPathSuffix / IgnorePathSuffix
228 | - AcceptPathMatch / IgnorePathMatch
229 | - AcceptHost / IgnoreHost
230 | - AcceptHostContains / IgnoreHostContains
231 | - AcceptHostPrefix / IgnoreHostPrefix
232 | - AcceptHostSuffix / IgnoreHostSuffix
233 | - AcceptHostMatch / IgnoreHostMatch
234 |
235 | ### Using custom time formatters
236 |
237 | ```go
238 | // Create a slog logger, which:
239 | // - Logs to stdout.
240 | // - RFC3339 with UTC time format.
241 | logger := slog.New(
242 | slogformatter.NewFormatterHandler(
243 | slogformatter.TimezoneConverter(time.UTC),
244 | slogformatter.TimeFormatter(time.RFC3339, nil),
245 | )(
246 | slog.NewTextHandler(os.Stdout, nil),
247 | ),
248 | )
249 |
250 | app := fiber.New()
251 |
252 | app.Use(slogfiber.New(logger))
253 | app.Use(recover.New())
254 |
255 | app.Get("/", func(c *fiber.Ctx) error {
256 | return c.SendString("Hello, World 👋!")
257 | })
258 |
259 | app.Listen(":4242")
260 |
261 | // output:
262 | // time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Incoming request" env=production request.time=2023-10-15T20:32:58Z request.method=GET request.path=/ request.route="" request.ip=127.0.0.1:63932 request.length=0 response.time=2023-10-15T20:32:58Z response.latency=100ms response.status=200 response.length=7 id=229c7fc8-64f5-4467-bc4a-940700503b0d
263 | ```
264 |
265 | ### Using custom logger sub-group
266 |
267 | ```go
268 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
269 |
270 | app := fiber.New()
271 |
272 | app.Use(slogfiber.New(logger.WithGroup("http")))
273 | app.Use(recover.New())
274 |
275 | app.Get("/", func(c *fiber.Ctx) error {
276 | return c.SendString("Hello, World 👋!")
277 | })
278 |
279 | app.Listen(":4242")
280 |
281 | // output:
282 | // time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Incoming request" env=production http.request.time=2023-10-15T20:32:58.626+02:00 http.request.method=GET http.request.path=/ http.request.route="" http.request.ip=127.0.0.1:63932 http.request.length=0 http.response.time=2023-10-15T20:32:58.926+02:00 http.response.latency=100ms http.response.status=200 http.response.length=7 http.id=229c7fc8-64f5-4467-bc4a-940700503b0d
283 | ```
284 |
285 | ### Add logger to a single route
286 |
287 | ```go
288 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
289 |
290 | app := fiber.New()
291 |
292 | app.Use(recover.New())
293 |
294 | app.Get("/", slogfiber.New(logger), func(c *fiber.Ctx) error {
295 | return c.SendString("Hello, World 👋!")
296 | })
297 |
298 | app.Listen(":4242")
299 | ```
300 |
301 | ### Adding custom attributes
302 |
303 | ```go
304 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
305 |
306 | // Add an attribute to all log entries made through this logger.
307 | logger = logger.With("env", "production")
308 |
309 | app := fiber.New()
310 |
311 | app.Use(slogfiber.New(logger))
312 | app.Use(recover.New())
313 |
314 | app.Get("/", func(c *fiber.Ctx) error {
315 | // Add an attribute to a single log entry.
316 | slogfiber.AddCustomAttributes(c, slog.String("foo", "bar"))
317 | return c.SendString("Hello, World 👋!")
318 | })
319 |
320 | app.Listen(":4242")
321 |
322 | // output:
323 | // time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Incoming request" env=production request.time=2023-10-15T20:32:58.626+02:00 request.method=GET request.path=/ request.route="" request.ip=127.0.0.1:63932 request.length=0 response.time=2023-10-15T20:32:58.926+02:00 response.latency=100ms response.status=200 response.length=7 id=229c7fc8-64f5-4467-bc4a-940700503b0d foo=bar
324 | ```
325 |
326 | ### JSON output
327 |
328 | ```go
329 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)),
330 |
331 | app := fiber.New()
332 |
333 | app.Use(slogfiber.New(logger))
334 | app.Use(recover.New())
335 |
336 | app.Get("/", func(c *fiber.Ctx) error {
337 | return c.SendString("Hello, World 👋!")
338 | })
339 |
340 | app.Listen(":4242")
341 |
342 | // output:
343 | // {"time":"2023-10-15T20:32:58.926+02:00","level":"INFO","msg":"Incoming request","env":"production","http":{"request":{"time":"2023-10-15T20:32:58.626+02:00","method":"GET","path":"/","route":"","ip":"127.0.0.1:55296","length":0},"response":{"time":"2023-10-15T20:32:58.926+02:00","latency":100000,"status":200,"length":7},"id":"04201917-d7ba-4b20-a3bb-2fffba5f2bd9"}}
344 | ```
345 |
346 | ## 🤝 Contributing
347 |
348 | - Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :))
349 | - Fork the [project](https://github.com/samber/slog-fiber)
350 | - Fix [open issues](https://github.com/samber/slog-fiber/issues) or request new features
351 |
352 | Don't hesitate ;)
353 |
354 | ```bash
355 | # Install some dev dependencies
356 | make tools
357 |
358 | # Run tests
359 | make test
360 | # or
361 | make watch-test
362 | ```
363 |
364 | ## 👤 Contributors
365 |
366 | 
367 |
368 | ## 💫 Show your support
369 |
370 | Give a ⭐️ if this project helped you!
371 |
372 | [](https://github.com/sponsors/samber)
373 |
374 | ## 📝 License
375 |
376 | Copyright © 2023 [Samuel Berthe](https://github.com/samber).
377 |
378 | This project is [MIT](./LICENSE) licensed.
379 |
--------------------------------------------------------------------------------
/example/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "time"
7 |
8 | "log/slog"
9 |
10 | "github.com/gofiber/fiber/v2"
11 | "github.com/gofiber/fiber/v2/middleware/recover"
12 | slogfiber "github.com/samber/slog-fiber"
13 | slogformatter "github.com/samber/slog-formatter"
14 | )
15 |
16 | func main() {
17 | // Create a slog logger, which:
18 | // - Logs to stdout.
19 | // - RFC3339 with UTC time format.
20 | logger := slog.New(
21 | slogformatter.NewFormatterHandler(
22 | slogformatter.TimezoneConverter(time.UTC),
23 | slogformatter.TimeFormatter(time.RFC3339, nil),
24 | )(
25 | slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}),
26 | ),
27 | )
28 |
29 | // Add an attribute to all log entries made through this logger.
30 | logger = logger.With("env", "production")
31 |
32 | app := fiber.New()
33 |
34 | app.Use(slogfiber.New(logger.WithGroup("http")))
35 | // config := slogfiber.Config{WithRequestBody: true, WithResponseBody: true, WithRequestHeader: true, WithResponseHeader: true}
36 | // app.Use(slogfiber.NewWithConfig(logger, config))
37 | app.Use(recover.New())
38 |
39 | app.Get("/", func(c *fiber.Ctx) error {
40 | slogfiber.AddCustomAttributes(c, slog.String("foo", "bar"))
41 | return c.SendString("Hello, World 👋!")
42 | })
43 |
44 | app.Get("/crashme", func(c *fiber.Ctx) error {
45 | return c.Status(400).SendString("Oops i crashed :(")
46 | })
47 |
48 | app.Get("/foobar/:id", func(c *fiber.Ctx) error {
49 | return c.SendString("Hello, World 👋!")
50 | })
51 |
52 | app.Post("/bad", func(c *fiber.Ctx) error {
53 | return c.SendStatus(fiber.StatusBadRequest)
54 | })
55 | app.Get("/die", func(c *fiber.Ctx) error {
56 | panic("killed")
57 | })
58 | app.Post("/force", func(c *fiber.Ctx) error {
59 | return fiber.NewError(fiber.StatusUnauthorized)
60 | })
61 |
62 | // 404 Handler
63 | app.Use(func(c *fiber.Ctx) error {
64 | return c.SendStatus(fiber.StatusNotFound)
65 | })
66 |
67 | err := app.Listen(":4242")
68 | if err != nil {
69 | fmt.Println(err.Error())
70 | }
71 |
72 | // output:
73 | // time=2023-04-10T14:00:00.000+00:00 level=INFO msg="Incoming request" env=production http.status=200 http.method=GET http.path=/ http.route=/ http.ip=::1 http.latency=25.958µs http.user-agent=curl/7.77.0 http.time=2023-04-10T14:00:00Z http.request-id=229c7fc8-64f5-4467-bc4a-940700503b0d
74 | }
75 |
--------------------------------------------------------------------------------
/filters.go:
--------------------------------------------------------------------------------
1 | package slogfiber
2 |
3 | import (
4 | "regexp"
5 | "slices"
6 | "strings"
7 |
8 | "github.com/gofiber/fiber/v2"
9 | )
10 |
11 | type Filter func(ctx *fiber.Ctx) bool
12 |
13 | // Basic
14 | func Accept(filter Filter) Filter { return filter }
15 | func Ignore(filter Filter) Filter { return func(ctx *fiber.Ctx) bool { return !filter(ctx) } }
16 |
17 | // Method
18 | func AcceptMethod(methods ...string) Filter {
19 | return func(c *fiber.Ctx) bool {
20 | reqMethod := strings.ToLower(string(c.Context().Method()))
21 |
22 | for _, method := range methods {
23 | if strings.ToLower(method) == reqMethod {
24 | return true
25 | }
26 | }
27 |
28 | return false
29 | }
30 | }
31 |
32 | func IgnoreMethod(methods ...string) Filter {
33 | return func(c *fiber.Ctx) bool {
34 | reqMethod := strings.ToLower(string(c.Context().Method()))
35 |
36 | for _, method := range methods {
37 | if strings.ToLower(method) == reqMethod {
38 | return false
39 | }
40 | }
41 |
42 | return true
43 | }
44 | }
45 |
46 | // Status
47 | func AcceptStatus(statuses ...int) Filter {
48 | return func(c *fiber.Ctx) bool {
49 | return slices.Contains(statuses, c.Response().StatusCode())
50 | }
51 | }
52 |
53 | func IgnoreStatus(statuses ...int) Filter {
54 | return func(c *fiber.Ctx) bool {
55 | return !slices.Contains(statuses, c.Response().StatusCode())
56 | }
57 | }
58 |
59 | func AcceptStatusGreaterThan(status int) Filter {
60 | return func(c *fiber.Ctx) bool {
61 | return c.Response().StatusCode() > status
62 | }
63 | }
64 |
65 | func AcceptStatusGreaterThanOrEqual(status int) Filter {
66 | return func(c *fiber.Ctx) bool {
67 | return c.Response().StatusCode() >= status
68 | }
69 | }
70 |
71 | func AcceptStatusLessThan(status int) Filter {
72 | return func(c *fiber.Ctx) bool {
73 | return c.Response().StatusCode() < status
74 | }
75 | }
76 |
77 | func AcceptStatusLessThanOrEqual(status int) Filter {
78 | return func(c *fiber.Ctx) bool {
79 | return c.Response().StatusCode() <= status
80 | }
81 | }
82 |
83 | func IgnoreStatusGreaterThan(status int) Filter {
84 | return AcceptStatusLessThanOrEqual(status)
85 | }
86 |
87 | func IgnoreStatusGreaterThanOrEqual(status int) Filter {
88 | return AcceptStatusLessThan(status)
89 | }
90 |
91 | func IgnoreStatusLessThan(status int) Filter {
92 | return AcceptStatusGreaterThanOrEqual(status)
93 | }
94 |
95 | func IgnoreStatusLessThanOrEqual(status int) Filter {
96 | return AcceptStatusGreaterThan(status)
97 | }
98 |
99 | // Path
100 | func AcceptPath(urls ...string) Filter {
101 | return func(c *fiber.Ctx) bool {
102 | return slices.Contains(urls, c.Path())
103 | }
104 | }
105 |
106 | func IgnorePath(urls ...string) Filter {
107 | return func(c *fiber.Ctx) bool {
108 | return !slices.Contains(urls, c.Path())
109 | }
110 | }
111 |
112 | func AcceptPathContains(parts ...string) Filter {
113 | return func(c *fiber.Ctx) bool {
114 | for _, part := range parts {
115 | if strings.Contains(c.Path(), part) {
116 | return true
117 | }
118 | }
119 |
120 | return false
121 | }
122 | }
123 |
124 | func IgnorePathContains(parts ...string) Filter {
125 | return func(c *fiber.Ctx) bool {
126 | for _, part := range parts {
127 | if strings.Contains(c.Path(), part) {
128 | return false
129 | }
130 | }
131 |
132 | return true
133 | }
134 | }
135 |
136 | func AcceptPathPrefix(prefixs ...string) Filter {
137 | return func(c *fiber.Ctx) bool {
138 | for _, prefix := range prefixs {
139 | if strings.HasPrefix(c.Path(), prefix) {
140 | return true
141 | }
142 | }
143 |
144 | return false
145 | }
146 | }
147 |
148 | func IgnorePathPrefix(prefixs ...string) Filter {
149 | return func(c *fiber.Ctx) bool {
150 | for _, prefix := range prefixs {
151 | if strings.HasPrefix(c.Path(), prefix) {
152 | return false
153 | }
154 | }
155 |
156 | return true
157 | }
158 | }
159 |
160 | func AcceptPathSuffix(prefixs ...string) Filter {
161 | return func(c *fiber.Ctx) bool {
162 | for _, prefix := range prefixs {
163 | if strings.HasPrefix(c.Path(), prefix) {
164 | return true
165 | }
166 | }
167 |
168 | return false
169 | }
170 | }
171 |
172 | func IgnorePathSuffix(suffixs ...string) Filter {
173 | return func(c *fiber.Ctx) bool {
174 | for _, suffix := range suffixs {
175 | if strings.HasSuffix(c.Path(), suffix) {
176 | return false
177 | }
178 | }
179 |
180 | return true
181 | }
182 | }
183 |
184 | func AcceptPathMatch(regs ...regexp.Regexp) Filter {
185 | return func(c *fiber.Ctx) bool {
186 | for _, reg := range regs {
187 | if reg.Match([]byte(c.Path())) {
188 | return true
189 | }
190 | }
191 |
192 | return false
193 | }
194 | }
195 |
196 | func IgnorePathMatch(regs ...regexp.Regexp) Filter {
197 | return func(c *fiber.Ctx) bool {
198 | for _, reg := range regs {
199 | if reg.Match([]byte(c.Path())) {
200 | return false
201 | }
202 | }
203 |
204 | return true
205 | }
206 | }
207 |
208 | // Host
209 | func AcceptHost(hosts ...string) Filter {
210 | return func(c *fiber.Ctx) bool {
211 | return slices.Contains(hosts, c.Hostname())
212 | }
213 | }
214 |
215 | func IgnoreHost(hosts ...string) Filter {
216 | return func(c *fiber.Ctx) bool {
217 | return !slices.Contains(hosts, c.Hostname())
218 | }
219 | }
220 |
221 | func AcceptHostContains(parts ...string) Filter {
222 | return func(c *fiber.Ctx) bool {
223 | for _, part := range parts {
224 | if strings.Contains(c.Hostname(), part) {
225 | return true
226 | }
227 | }
228 |
229 | return false
230 | }
231 | }
232 |
233 | func IgnoreHostContains(parts ...string) Filter {
234 | return func(c *fiber.Ctx) bool {
235 | for _, part := range parts {
236 | if strings.Contains(c.Hostname(), part) {
237 | return false
238 | }
239 | }
240 |
241 | return true
242 | }
243 | }
244 |
245 | func AcceptHostPrefix(prefixs ...string) Filter {
246 | return func(c *fiber.Ctx) bool {
247 | for _, prefix := range prefixs {
248 | if strings.HasPrefix(c.Hostname(), prefix) {
249 | return true
250 | }
251 | }
252 |
253 | return false
254 | }
255 | }
256 |
257 | func IgnoreHostPrefix(prefixs ...string) Filter {
258 | return func(c *fiber.Ctx) bool {
259 | for _, prefix := range prefixs {
260 | if strings.HasPrefix(c.Hostname(), prefix) {
261 | return false
262 | }
263 | }
264 |
265 | return true
266 | }
267 | }
268 |
269 | func AcceptHostSuffix(prefixs ...string) Filter {
270 | return func(c *fiber.Ctx) bool {
271 | for _, prefix := range prefixs {
272 | if strings.HasPrefix(c.Hostname(), prefix) {
273 | return true
274 | }
275 | }
276 |
277 | return false
278 | }
279 | }
280 |
281 | func IgnoreHostSuffix(suffixs ...string) Filter {
282 | return func(c *fiber.Ctx) bool {
283 | for _, suffix := range suffixs {
284 | if strings.HasSuffix(c.Hostname(), suffix) {
285 | return false
286 | }
287 | }
288 |
289 | return true
290 | }
291 | }
292 |
293 | func AcceptHostMatch(regs ...regexp.Regexp) Filter {
294 | return func(c *fiber.Ctx) bool {
295 | for _, reg := range regs {
296 | if reg.Match([]byte(c.Hostname())) {
297 | return true
298 | }
299 | }
300 |
301 | return false
302 | }
303 | }
304 |
305 | func IgnoreHostMatch(regs ...regexp.Regexp) Filter {
306 | return func(c *fiber.Ctx) bool {
307 | for _, reg := range regs {
308 | if reg.Match([]byte(c.Hostname())) {
309 | return false
310 | }
311 | }
312 |
313 | return true
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/samber/slog-fiber
2 |
3 | go 1.21
4 |
5 | require golang.org/x/text v0.22.0 // indirect
6 |
7 | require (
8 | github.com/andybalholm/brotli v1.1.1 // indirect
9 | github.com/klauspost/compress v1.17.11 // indirect
10 | github.com/mattn/go-colorable v0.1.13 // indirect
11 | github.com/mattn/go-isatty v0.0.20 // indirect
12 | github.com/mattn/go-runewidth v0.0.16 // indirect
13 | github.com/rivo/uniseg v0.2.0 // indirect
14 | github.com/samber/lo v1.49.1 // indirect
15 | github.com/samber/slog-formatter v1.2.0
16 | github.com/samber/slog-multi v1.3.3 // indirect
17 | github.com/valyala/bytebufferpool v1.0.0 // indirect
18 | github.com/valyala/fasthttp v1.59.0
19 | go.opentelemetry.io/otel v1.29.0 // indirect
20 | golang.org/x/sys v0.30.0 // indirect
21 | )
22 |
23 | require (
24 | github.com/gofiber/fiber/v2 v2.52.8
25 | github.com/google/uuid v1.6.0
26 | go.opentelemetry.io/otel/trace v1.29.0
27 | go.uber.org/goleak v1.3.0
28 | )
29 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
6 | github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
12 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
13 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
14 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
15 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
16 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
17 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
18 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
19 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
22 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
23 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
24 | github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
25 | github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
26 | github.com/samber/slog-formatter v1.2.0 h1:gTSHm4CxyySyhcxRkzk21CSKbGCdZVipbRMhINkNtQU=
27 | github.com/samber/slog-formatter v1.2.0/go.mod h1:hgjhSd5Vf69XCOnVp0UW0QHCxJ8iDEm/qASjji6FNoI=
28 | github.com/samber/slog-multi v1.3.3 h1:qhFXaYdW73FIWLt8SrXMXfPwY58NpluzKDwRdPvhWWY=
29 | github.com/samber/slog-multi v1.3.3/go.mod h1:ACuZ5B6heK57TfMVkVknN2UZHoFfjCwRxR0Q2OXKHlo=
30 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
31 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
32 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
33 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
34 | github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
35 | github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
36 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
37 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
38 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
39 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
40 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
41 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
42 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
43 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
44 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
47 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
48 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
49 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
50 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
51 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
52 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package slogfiber
2 |
3 | import (
4 | "testing"
5 |
6 | "go.uber.org/goleak"
7 | )
8 |
9 | func TestMain(m *testing.M) {
10 | goleak.VerifyTestMain(m)
11 | }
12 |
--------------------------------------------------------------------------------
/middleware.go:
--------------------------------------------------------------------------------
1 | package slogfiber
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 | "sync"
9 | "time"
10 |
11 | "log/slog"
12 |
13 | "github.com/gofiber/fiber/v2"
14 | "github.com/google/uuid"
15 | "github.com/valyala/fasthttp"
16 | "go.opentelemetry.io/otel/trace"
17 | )
18 |
19 | type customAttributesCtxKeyType struct{}
20 |
21 | var customAttributesCtxKey = customAttributesCtxKeyType{}
22 |
23 | var (
24 | TraceIDKey = "trace_id"
25 | SpanIDKey = "span_id"
26 | RequestIDKey = "id"
27 |
28 | RequestBodyMaxSize = 64 * 1024 // 64KB
29 | ResponseBodyMaxSize = 64 * 1024 // 64KB
30 |
31 | HiddenRequestHeaders = map[string]struct{}{
32 | "authorization": {},
33 | "cookie": {},
34 | "set-cookie": {},
35 | "x-auth-token": {},
36 | "x-csrf-token": {},
37 | "x-xsrf-token": {},
38 | }
39 | HiddenResponseHeaders = map[string]struct{}{
40 | "set-cookie": {},
41 | }
42 |
43 | // Formatted with http.CanonicalHeaderKey
44 | RequestIDHeaderKey = "X-Request-Id"
45 | )
46 |
47 | type Config struct {
48 | DefaultLevel slog.Level
49 | ClientErrorLevel slog.Level
50 | ServerErrorLevel slog.Level
51 |
52 | WithUserAgent bool
53 | WithRequestID bool
54 | WithRequestBody bool
55 | WithRequestHeader bool
56 | WithResponseBody bool
57 | WithResponseHeader bool
58 | WithSpanID bool
59 | WithTraceID bool
60 |
61 | Filters []Filter
62 | }
63 |
64 | // New returns a fiber.Handler (middleware) that logs requests using slog.
65 | //
66 | // Requests with errors are logged using slog.Error().
67 | // Requests without errors are logged using slog.Info().
68 | func New(logger *slog.Logger) fiber.Handler {
69 | return NewWithConfig(logger, Config{
70 | DefaultLevel: slog.LevelInfo,
71 | ClientErrorLevel: slog.LevelWarn,
72 | ServerErrorLevel: slog.LevelError,
73 |
74 | WithUserAgent: false,
75 | WithRequestID: true,
76 | WithRequestBody: false,
77 | WithRequestHeader: false,
78 | WithResponseBody: false,
79 | WithResponseHeader: false,
80 | WithSpanID: false,
81 | WithTraceID: false,
82 |
83 | Filters: []Filter{},
84 | })
85 | }
86 |
87 | // NewWithFilters returns a fiber.Handler (middleware) that logs requests using slog.
88 | //
89 | // Requests with errors are logged using slog.Error().
90 | // Requests without errors are logged using slog.Info().
91 | func NewWithFilters(logger *slog.Logger, filters ...Filter) fiber.Handler {
92 | return NewWithConfig(logger, Config{
93 | DefaultLevel: slog.LevelInfo,
94 | ClientErrorLevel: slog.LevelWarn,
95 | ServerErrorLevel: slog.LevelError,
96 |
97 | WithUserAgent: false,
98 | WithRequestID: true,
99 | WithRequestBody: false,
100 | WithRequestHeader: false,
101 | WithResponseBody: false,
102 | WithResponseHeader: false,
103 | WithSpanID: false,
104 | WithTraceID: false,
105 |
106 | Filters: filters,
107 | })
108 | }
109 |
110 | // NewWithConfig returns a fiber.Handler (middleware) that logs requests using slog.
111 | func NewWithConfig(logger *slog.Logger, config Config) fiber.Handler {
112 | var (
113 | once sync.Once
114 | errHandler fiber.ErrorHandler
115 | )
116 |
117 | return func(c *fiber.Ctx) error {
118 | once.Do(func() {
119 | errHandler = c.App().ErrorHandler
120 | })
121 |
122 | start := time.Now()
123 | path := c.Path()
124 | query := string(c.Request().URI().QueryString())
125 |
126 | requestID := c.Get(RequestIDHeaderKey)
127 | if config.WithRequestID {
128 | if requestID == "" {
129 | requestID = uuid.New().String()
130 | }
131 | c.Context().SetUserValue("request-id", requestID)
132 | c.Set("X-Request-ID", requestID)
133 | }
134 |
135 | err := c.Next()
136 | if err != nil {
137 | if err = errHandler(c, err); err != nil {
138 | _ = c.SendStatus(fiber.StatusInternalServerError) //nolint:errcheck
139 | }
140 | }
141 |
142 | // Pass thru filters and skip early the code below, to prevent unnecessary processing.
143 | for _, filter := range config.Filters {
144 | if !filter(c) {
145 | return err
146 | }
147 | }
148 |
149 | status := c.Response().StatusCode()
150 | method := c.Context().Method()
151 | host := c.Hostname()
152 | params := c.AllParams()
153 | route := c.Route().Path
154 | end := time.Now()
155 | latency := end.Sub(start)
156 | userAgent := c.Context().UserAgent()
157 | referer := c.Get(fiber.HeaderReferer)
158 |
159 | ip := c.Context().RemoteIP().String()
160 | if len(c.IPs()) > 0 {
161 | ip = c.IPs()[0]
162 | }
163 |
164 | baseAttributes := []slog.Attr{}
165 |
166 | requestAttributes := []slog.Attr{
167 | slog.Time("time", start.UTC()),
168 | slog.String("method", string(method)),
169 | slog.String("host", host),
170 | slog.String("path", path),
171 | slog.String("query", query),
172 | slog.Any("params", params),
173 | slog.String("route", route),
174 | slog.String("ip", ip),
175 | slog.Any("x-forwarded-for", c.IPs()),
176 | slog.String("referer", referer),
177 | }
178 |
179 | responseAttributes := []slog.Attr{
180 | slog.Time("time", end.UTC()),
181 | slog.Duration("latency", latency),
182 | slog.Int("status", status),
183 | }
184 |
185 | if config.WithRequestID {
186 | baseAttributes = append(baseAttributes, slog.String(RequestIDKey, requestID))
187 | }
188 |
189 | // otel
190 | baseAttributes = append(baseAttributes, extractTraceSpanID(c.UserContext(), config.WithTraceID, config.WithSpanID)...)
191 |
192 | // request body
193 | requestAttributes = append(requestAttributes, slog.Int("length", len((c.Body()))))
194 | if config.WithRequestBody {
195 | body := c.Body()
196 | if len(body) > RequestBodyMaxSize {
197 | body = body[:RequestBodyMaxSize]
198 | }
199 | requestAttributes = append(requestAttributes, slog.String("body", string(body)))
200 | }
201 |
202 | // request headers
203 | if config.WithRequestHeader {
204 | kv := []any{}
205 |
206 | for k, v := range c.GetReqHeaders() {
207 | if _, found := HiddenRequestHeaders[strings.ToLower(k)]; found {
208 | continue
209 | }
210 | kv = append(kv, slog.Any(k, v))
211 | }
212 |
213 | requestAttributes = append(requestAttributes, slog.Group("header", kv...))
214 | }
215 |
216 | if config.WithUserAgent {
217 | requestAttributes = append(requestAttributes, slog.String("user-agent", string(userAgent)))
218 | }
219 |
220 | // response body
221 | responseAttributes = append(responseAttributes, slog.Int("length", len(c.Response().Body())))
222 | if config.WithResponseBody {
223 | body := c.Response().Body()
224 | if len(body) > ResponseBodyMaxSize {
225 | body = body[:ResponseBodyMaxSize]
226 | }
227 | responseAttributes = append(responseAttributes, slog.String("body", string(body)))
228 | }
229 |
230 | // response headers
231 | if config.WithResponseHeader {
232 | kv := []any{}
233 |
234 | for k, v := range c.GetRespHeaders() {
235 | if _, found := HiddenResponseHeaders[strings.ToLower(k)]; found {
236 | continue
237 | }
238 | kv = append(kv, slog.Any(k, v))
239 | }
240 |
241 | responseAttributes = append(responseAttributes, slog.Group("header", kv...))
242 | }
243 |
244 | attributes := append(
245 | []slog.Attr{
246 | {
247 | Key: "request",
248 | Value: slog.GroupValue(requestAttributes...),
249 | },
250 | {
251 | Key: "response",
252 | Value: slog.GroupValue(responseAttributes...),
253 | },
254 | },
255 | baseAttributes...,
256 | )
257 |
258 | // custom context values
259 | if v := c.Context().UserValue(customAttributesCtxKey); v != nil {
260 | switch attrs := v.(type) {
261 | case []slog.Attr:
262 | attributes = append(attributes, attrs...)
263 | }
264 | }
265 |
266 | logErr := err
267 | if logErr == nil {
268 | logErr = fiber.NewError(status)
269 | }
270 |
271 | level := config.DefaultLevel
272 | msg := "Incoming request"
273 | if status >= http.StatusInternalServerError {
274 | level = config.ServerErrorLevel
275 | msg = logErr.Error()
276 | if msg == "" {
277 | msg = fmt.Sprintf("HTTP error: %d %s", status, strings.ToLower(http.StatusText(status)))
278 | }
279 | } else if status >= http.StatusBadRequest && status < http.StatusInternalServerError {
280 | level = config.ClientErrorLevel
281 | msg = logErr.Error()
282 | if msg == "" {
283 | msg = fmt.Sprintf("HTTP error: %d %s", status, strings.ToLower(http.StatusText(status)))
284 | }
285 | }
286 |
287 | logger.LogAttrs(c.UserContext(), level, msg, attributes...)
288 |
289 | return err
290 | }
291 | }
292 |
293 | // GetRequestID returns the request identifier.
294 | func GetRequestID(c *fiber.Ctx) string {
295 | return GetRequestIDFromContext(c.Context())
296 | }
297 |
298 | // GetRequestIDFromContext returns the request identifier from the context.
299 | func GetRequestIDFromContext(ctx *fasthttp.RequestCtx) string {
300 | requestID, ok := ctx.UserValue("request-id").(string)
301 | if !ok {
302 | return ""
303 | }
304 |
305 | return requestID
306 | }
307 |
308 | // AddCustomAttributes adds custom attributes to the request context.
309 | func AddCustomAttributes(c *fiber.Ctx, attr slog.Attr) {
310 | v := c.Context().UserValue(customAttributesCtxKey)
311 | if v == nil {
312 | c.Context().SetUserValue(customAttributesCtxKey, []slog.Attr{attr})
313 | return
314 | }
315 |
316 | switch attrs := v.(type) {
317 | case []slog.Attr:
318 | c.Context().SetUserValue(customAttributesCtxKey, append(attrs, attr))
319 | }
320 | }
321 |
322 | func extractTraceSpanID(ctx context.Context, withTraceID bool, withSpanID bool) []slog.Attr {
323 | if !withTraceID && !withSpanID {
324 | return []slog.Attr{}
325 | }
326 |
327 | span := trace.SpanFromContext(ctx)
328 | if !span.IsRecording() {
329 | return []slog.Attr{}
330 | }
331 |
332 | attrs := []slog.Attr{}
333 | spanCtx := span.SpanContext()
334 |
335 | if withTraceID && spanCtx.HasTraceID() {
336 | traceID := trace.SpanFromContext(ctx).SpanContext().TraceID().String()
337 | attrs = append(attrs, slog.String(TraceIDKey, traceID))
338 | }
339 |
340 | if withSpanID && spanCtx.HasSpanID() {
341 | spanID := spanCtx.SpanID().String()
342 | attrs = append(attrs, slog.String(SpanIDKey, spanID))
343 | }
344 |
345 | return attrs
346 | }
347 |
--------------------------------------------------------------------------------