├── .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 | [![tag](https://img.shields.io/github/tag/samber/slog-fiber.svg)](https://github.com/samber/slog-fiber/releases) 5 | ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c) 6 | [![GoDoc](https://godoc.org/github.com/samber/slog-fiber?status.svg)](https://pkg.go.dev/github.com/samber/slog-fiber) 7 | ![Build Status](https://github.com/samber/slog-fiber/actions/workflows/test.yml/badge.svg) 8 | [![Go report](https://goreportcard.com/badge/github.com/samber/slog-fiber)](https://goreportcard.com/report/github.com/samber/slog-fiber) 9 | [![Coverage](https://img.shields.io/codecov/c/github/samber/slog-fiber)](https://codecov.io/gh/samber/slog-fiber) 10 | [![Contributors](https://img.shields.io/github/contributors/samber/slog-fiber)](https://github.com/samber/slog-fiber/graphs/contributors) 11 | [![License](https://img.shields.io/github/license/samber/slog-fiber)](./LICENSE) 12 | 13 | [Fiber](https://github.com/gofiber/fiber) middleware to log http requests using [slog](https://pkg.go.dev/log/slog). 14 | 15 |
16 |
17 | Sponsored by: 18 |
19 | 20 |
21 | Quickwit 22 |
23 |
24 | Cloud-native search engine for observability - An OSS alternative to Splunk, Elasticsearch, Loki, and Tempo. 25 |
26 |
27 |
28 |
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 | ![Contributors](https://contrib.rocks/image?repo=samber/slog-fiber) 367 | 368 | ## 💫 Show your support 369 | 370 | Give a ⭐️ if this project helped you! 371 | 372 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/samber?style=for-the-badge)](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 | --------------------------------------------------------------------------------