├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── dump.go ├── example ├── example.go ├── go.mod └── go.sum ├── filters.go ├── go.mod ├── go.sum ├── go.work.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: Gin middleware 3 | 4 | [![tag](https://img.shields.io/github/tag/samber/slog-gin.svg)](https://github.com/samber/slog-gin/releases) 5 | ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c) 6 | [![GoDoc](https://godoc.org/github.com/samber/slog-gin?status.svg)](https://pkg.go.dev/github.com/samber/slog-gin) 7 | ![Build Status](https://github.com/samber/slog-gin/actions/workflows/test.yml/badge.svg) 8 | [![Go report](https://goreportcard.com/badge/github.com/samber/slog-gin)](https://goreportcard.com/report/github.com/samber/slog-gin) 9 | [![Coverage](https://img.shields.io/codecov/c/github/samber/slog-gin)](https://codecov.io/gh/samber/slog-gin) 10 | [![Contributors](https://img.shields.io/github/contributors/samber/slog-gin)](https://github.com/samber/slog-gin/graphs/contributors) 11 | [![License](https://img.shields.io/github/license/samber/slog-gin)](./LICENSE) 12 | 13 | [Gin](https://github.com/gin-gonic/gin) 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 | go get github.com/samber/slog-gin 77 | ``` 78 | 79 | **Compatibility**: go >= 1.21 80 | 81 | No breaking changes will be made to exported APIs before v2.0.0. 82 | 83 | ## 💡 Usage 84 | 85 | ### Handler options 86 | 87 | ```go 88 | type Config struct { 89 | DefaultLevel slog.Level 90 | ClientErrorLevel slog.Level 91 | ServerErrorLevel slog.Level 92 | 93 | WithUserAgent bool 94 | WithRequestID bool 95 | WithRequestBody bool 96 | WithRequestHeader bool 97 | WithResponseBody bool 98 | WithResponseHeader bool 99 | WithSpanID bool 100 | WithTraceID bool 101 | 102 | Filters []Filter 103 | } 104 | ``` 105 | 106 | Attributes will be injected in log payload. 107 | 108 | Other global parameters: 109 | 110 | ```go 111 | sloggin.TraceIDKey = "trace_id" 112 | sloggin.SpanIDKey = "span_id" 113 | sloggin.RequestBodyMaxSize = 64 * 1024 // 64KB 114 | sloggin.ResponseBodyMaxSize = 64 * 1024 // 64KB 115 | sloggin.HiddenRequestHeaders = map[string]struct{}{ ... } 116 | sloggin.HiddenResponseHeaders = map[string]struct{}{ ... } 117 | sloggin.RequestIDHeaderKey = "X-Request-Id" 118 | ``` 119 | 120 | ### Minimal 121 | 122 | ```go 123 | import ( 124 | "github.com/gin-gonic/gin" 125 | sloggin "github.com/samber/slog-gin" 126 | "log/slog" 127 | ) 128 | 129 | // Create a slog logger, which: 130 | // - Logs to stdout. 131 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 132 | 133 | router := gin.New() 134 | 135 | // Add the sloggin middleware to all routes. 136 | // The middleware will log all requests attributes. 137 | router.Use(sloggin.New(logger)) 138 | router.Use(gin.Recovery()) 139 | 140 | // Example pong request. 141 | router.GET("/pong", func(c *gin.Context) { 142 | c.String(http.StatusOK, "pong") 143 | }) 144 | 145 | router.Run(":1234") 146 | 147 | // output: 148 | // 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.query="" 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="" 149 | ``` 150 | 151 | ### OTEL 152 | 153 | ```go 154 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 155 | 156 | config := sloggin.Config{ 157 | WithSpanID: true, 158 | WithTraceID: true, 159 | } 160 | 161 | router := gin.New() 162 | router.Use(sloggin.NewWithConfig(logger, config)) 163 | ``` 164 | 165 | ### Custom log levels 166 | 167 | ```go 168 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 169 | 170 | config := sloggin.Config{ 171 | DefaultLevel: slog.LevelInfo, 172 | ClientErrorLevel: slog.LevelWarn, 173 | ServerErrorLevel: slog.LevelError, 174 | } 175 | 176 | router := gin.New() 177 | router.Use(sloggin.NewWithConfig(logger, config)) 178 | ``` 179 | 180 | ### Verbose 181 | 182 | ```go 183 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 184 | 185 | config := sloggin.Config{ 186 | WithRequestBody: true, 187 | WithResponseBody: true, 188 | WithRequestHeader: true, 189 | WithResponseHeader: true, 190 | } 191 | 192 | router := gin.New() 193 | router.Use(sloggin.NewWithConfig(logger, config)) 194 | ``` 195 | 196 | ### Filters 197 | 198 | ```go 199 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 200 | 201 | router := gin.New() 202 | router.Use( 203 | sloggin.NewWithFilters( 204 | logger, 205 | sloggin.Accept(func (c *gin.Context) bool { 206 | return xxx 207 | }), 208 | sloggin.IgnoreStatus(401, 404), 209 | ), 210 | ) 211 | ``` 212 | 213 | Available filters: 214 | - Accept / Ignore 215 | - AcceptMethod / IgnoreMethod 216 | - AcceptStatus / IgnoreStatus 217 | - AcceptStatusGreaterThan / IgnoreStatusGreaterThan 218 | - AcceptStatusLessThan / IgnoreStatusLessThan 219 | - AcceptStatusGreaterThanOrEqual / IgnoreStatusGreaterThanOrEqual 220 | - AcceptStatusLessThanOrEqual / IgnoreStatusLessThanOrEqual 221 | - AcceptPath / IgnorePath 222 | - AcceptPathContains / IgnorePathContains 223 | - AcceptPathPrefix / IgnorePathPrefix 224 | - AcceptPathSuffix / IgnorePathSuffix 225 | - AcceptPathMatch / IgnorePathMatch 226 | - AcceptHost / IgnoreHost 227 | - AcceptHostContains / IgnoreHostContains 228 | - AcceptHostPrefix / IgnoreHostPrefix 229 | - AcceptHostSuffix / IgnoreHostSuffix 230 | - AcceptHostMatch / IgnoreHostMatch 231 | 232 | ### Using custom time formatters 233 | 234 | ```go 235 | import ( 236 | "github.com/gin-gonic/gin" 237 | sloggin "github.com/samber/slog-gin" 238 | slogformatter "github.com/samber/slog-formatter" 239 | "log/slog" 240 | ) 241 | 242 | // Create a slog logger, which: 243 | // - Logs to stdout. 244 | // - RFC3339 with UTC time format. 245 | logger := slog.New( 246 | slogformatter.NewFormatterHandler( 247 | slogformatter.TimezoneConverter(time.UTC), 248 | slogformatter.TimeFormatter(time.DateTime, nil), 249 | )( 250 | slog.NewTextHandler(os.Stdout, nil), 251 | ), 252 | ) 253 | 254 | router := gin.New() 255 | 256 | // Add the sloggin middleware to all routes. 257 | // The middleware will log all requests attributes. 258 | router.Use(sloggin.New(logger)) 259 | router.Use(gin.Recovery()) 260 | 261 | // Example pong request. 262 | router.GET("/pong", func(c *gin.Context) { 263 | c.String(http.StatusOK, "pong") 264 | }) 265 | 266 | router.Run(":1234") 267 | 268 | // output: 269 | // 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.query="" 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="" 270 | ``` 271 | 272 | ### Using custom logger sub-group 273 | 274 | ```go 275 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 276 | 277 | router := gin.New() 278 | 279 | // Add the sloggin middleware to all routes. 280 | // The middleware will log all requests attributes under a "http" group. 281 | router.Use(sloggin.New(logger.WithGroup("http"))) 282 | router.Use(gin.Recovery()) 283 | 284 | // Example pong request. 285 | router.GET("/pong", func(c *gin.Context) { 286 | c.String(http.StatusOK, "pong") 287 | }) 288 | 289 | router.Run(":1234") 290 | 291 | // output: 292 | // 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=/ request.query="" 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="" 293 | ``` 294 | 295 | ### Add logger to a single route 296 | 297 | ```go 298 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 299 | 300 | router := gin.New() 301 | router.Use(gin.Recovery()) 302 | 303 | // Example pong request. 304 | // Add the sloggin middleware to a single routes. 305 | router.GET("/pong", sloggin.New(logger), func(c *gin.Context) { 306 | c.String(http.StatusOK, "pong") 307 | }) 308 | 309 | router.Run(":1234") 310 | ``` 311 | 312 | ### Adding custom attributes 313 | 314 | ```go 315 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)). 316 | With("environment", "production"). 317 | With("server", "gin/1.9.0"). 318 | With("server_start_time", time.Now()). 319 | With("gin_mode", gin.EnvGinMode) 320 | 321 | router := gin.New() 322 | 323 | // Add the sloggin middleware to all routes. 324 | // The middleware will log all requests attributes. 325 | router.Use(sloggin.New(logger)) 326 | router.Use(gin.Recovery()) 327 | 328 | // Example pong request. 329 | router.GET("/pong", func(c *gin.Context) { 330 | // Add an attribute to a single log entry. 331 | sloggin.AddCustomAttributes(c, slog.String("foo", "bar")) 332 | c.String(http.StatusOK, "pong") 333 | }) 334 | 335 | router.Run(":1234") 336 | 337 | // output: 338 | // time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Incoming request" environment=production server=gin/1.9.0 gin_mode=release request.time=2023-10-15T20:32:58.626+02:00 request.method=GET request.path=/ request.query="" 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="" foo=bar 339 | ``` 340 | 341 | ### JSON output 342 | 343 | ```go 344 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 345 | 346 | router := gin.New() 347 | 348 | // Add the sloggin middleware to all routes. 349 | // The middleware will log all requests attributes. 350 | router.Use(sloggin.New(logger)) 351 | router.Use(gin.Recovery()) 352 | 353 | // Example pong request. 354 | router.GET("/pong", func(c *gin.Context) { 355 | c.String(http.StatusOK, "pong") 356 | }) 357 | 358 | router.Run(":1234") 359 | 360 | // output: 361 | // {"time":"2023-10-15T20:32:58.926+02:00","level":"INFO","msg":"Incoming request","gin_mode":"GIN_MODE","env":"production","http":{"request":{"time":"2023-10-15T20:32:58.626+02:00","method":"GET","path":"/","query":"","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":""}} 362 | ``` 363 | 364 | ## 🤝 Contributing 365 | 366 | - Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) 367 | - Fork the [project](https://github.com/samber/slog-gin) 368 | - Fix [open issues](https://github.com/samber/slog-gin/issues) or request new features 369 | 370 | Don't hesitate ;) 371 | 372 | ```bash 373 | # Install some dev dependencies 374 | make tools 375 | 376 | # Run tests 377 | make test 378 | # or 379 | make watch-test 380 | ``` 381 | 382 | ## 👤 Contributors 383 | 384 | ![Contributors](https://contrib.rocks/image?repo=samber/slog-gin) 385 | 386 | ## 💫 Show your support 387 | 388 | Give a ⭐️ if this project helped you! 389 | 390 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/samber?style=for-the-badge)](https://github.com/sponsors/samber) 391 | 392 | ## 📝 License 393 | 394 | Copyright © 2023 [Samuel Berthe](https://github.com/samber). 395 | 396 | This project is [MIT](./LICENSE) licensed. 397 | -------------------------------------------------------------------------------- /dump.go: -------------------------------------------------------------------------------- 1 | package sloggin 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | var _ http.ResponseWriter = (*bodyWriter)(nil) 12 | var _ http.Flusher = (*bodyWriter)(nil) 13 | var _ http.Hijacker = (*bodyWriter)(nil) 14 | 15 | type bodyWriter struct { 16 | gin.ResponseWriter 17 | body *bytes.Buffer 18 | maxSize int 19 | bytes int 20 | } 21 | 22 | // implements gin.ResponseWriter 23 | func (w *bodyWriter) Write(b []byte) (int, error) { 24 | if w.body != nil { 25 | if w.body.Len()+len(b) > w.maxSize { 26 | w.body.Write(b[:w.maxSize-w.body.Len()]) 27 | } else { 28 | w.body.Write(b) 29 | } 30 | } 31 | 32 | w.bytes += len(b) //nolint:staticcheck 33 | return w.ResponseWriter.Write(b) 34 | } 35 | 36 | func newBodyWriter(writer gin.ResponseWriter, maxSize int, recordBody bool) *bodyWriter { 37 | var body *bytes.Buffer 38 | if recordBody { 39 | body = bytes.NewBufferString("") 40 | } 41 | 42 | return &bodyWriter{ 43 | ResponseWriter: writer, 44 | body: body, 45 | maxSize: maxSize, 46 | bytes: 0, 47 | } 48 | } 49 | 50 | type bodyReader struct { 51 | io.ReadCloser 52 | body *bytes.Buffer 53 | maxSize int 54 | bytes int 55 | } 56 | 57 | // implements io.Reader 58 | func (r *bodyReader) Read(b []byte) (int, error) { 59 | n, err := r.ReadCloser.Read(b) 60 | if r.body != nil { 61 | if r.body.Len()+n > r.maxSize { 62 | r.body.Write(b[:r.maxSize-r.body.Len()]) 63 | } else { 64 | r.body.Write(b[:n]) 65 | } 66 | } 67 | r.bytes += n 68 | return n, err 69 | } 70 | 71 | func newBodyReader(reader io.ReadCloser, maxSize int, recordBody bool) *bodyReader { 72 | var body *bytes.Buffer 73 | if recordBody { 74 | body = bytes.NewBufferString("") 75 | } 76 | 77 | return &bodyReader{ 78 | ReadCloser: reader, 79 | body: body, 80 | maxSize: maxSize, 81 | bytes: 0, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | slogformatter "github.com/samber/slog-formatter" 12 | sloggin "github.com/samber/slog-gin" 13 | "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/attribute" 16 | "go.opentelemetry.io/otel/propagation" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 19 | ) 20 | 21 | func initTracerProvider() (*sdktrace.TracerProvider, error) { 22 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 23 | defer cancel() 24 | 25 | resource, err := resource.New(ctx, resource.WithAttributes( 26 | attribute.String("service.name", "example"), 27 | attribute.String("service.namespace", "default"), 28 | )) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // tracer provider 34 | tp := sdktrace.NewTracerProvider( 35 | sdktrace.WithSampler(sdktrace.AlwaysSample()), 36 | sdktrace.WithResource(resource), 37 | ) 38 | otel.SetTracerProvider(tp) 39 | 40 | // Set up a text map propagator 41 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 42 | 43 | return tp, nil 44 | } 45 | 46 | func main() { 47 | tp, _ := initTracerProvider() 48 | defer tp.Shutdown(context.Background()) 49 | 50 | // Create a slog logger, which: 51 | // - Logs to stdout. 52 | // - RFC3339 with UTC time format. 53 | logger := slog.New( 54 | slogformatter.NewFormatterHandler( 55 | slogformatter.TimezoneConverter(time.UTC), 56 | slogformatter.TimeFormatter(time.RFC3339, nil), 57 | )( 58 | slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}), 59 | ), 60 | ) 61 | 62 | // Add an attribute to all log entries made through this logger. 63 | logger = logger.With("gin_mode", gin.EnvGinMode) 64 | 65 | router := gin.New() 66 | router.Use(otelgin.Middleware("example")) 67 | 68 | // Add the sloggin middleware to all routes. 69 | // The middleware will log all requests attributes under a "http" group. 70 | //router.Use(sloggin.New(logger)) 71 | config := sloggin.Config{ 72 | WithRequestID: true, 73 | WithSpanID: true, 74 | WithTraceID: true, 75 | } 76 | router.Use(sloggin.NewWithConfig(logger, config)) 77 | 78 | router.Use(gin.Recovery()) 79 | 80 | // Example pong request. 81 | router.GET("/pong", func(c *gin.Context) { 82 | sloggin.AddCustomAttributes(c, slog.String("foo", "bar")) 83 | c.String(http.StatusOK, "pong") 84 | }) 85 | router.GET("/pong/:id", func(c *gin.Context) { 86 | id := c.Param("id") 87 | c.String(http.StatusOK, "pong %s", id) 88 | }) 89 | 90 | logger.Info("Starting server") 91 | if err := router.Run(":4242"); err != nil { 92 | logger.Error("can' start server with 4242 port") 93 | } 94 | 95 | // output: 96 | // time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="Incoming request" gin_mode=GIN_MODE http.status=200 http.method=GET http.path=/pong http.ip=127.0.0.1 http.latency=25.5µs http.user-agent=curl/7.77.0 http.time=2023-04-10T14:00:00.000+00:00 97 | } 98 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.21 4 | toolchain go1.24.1 5 | 6 | replace github.com/samber/slog-gin => ../ 7 | 8 | require ( 9 | github.com/gin-gonic/gin v1.10.0 10 | github.com/samber/slog-formatter v1.0.1 11 | github.com/samber/slog-gin v1.13.3 12 | go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.53.0 13 | go.opentelemetry.io/otel v1.29.0 14 | go.opentelemetry.io/otel/sdk v1.28.0 15 | ) 16 | 17 | require ( 18 | github.com/bytedance/sonic v1.11.9 // indirect 19 | github.com/bytedance/sonic/loader v0.1.1 // indirect 20 | github.com/cloudwego/base64x v0.1.4 // indirect 21 | github.com/cloudwego/iasm v0.2.0 // indirect 22 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect 23 | github.com/gin-contrib/sse v0.1.0 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/go-playground/validator/v10 v10.22.0 // indirect 29 | github.com/goccy/go-json v0.10.3 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/json-iterator/go v1.1.12 // indirect 32 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 38 | github.com/samber/lo v1.47.0 // indirect 39 | github.com/samber/slog-multi v1.1.0 // indirect 40 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 41 | github.com/ugorji/go/codec v1.2.12 // indirect 42 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 43 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 44 | golang.org/x/arch v0.8.0 // indirect 45 | golang.org/x/crypto v0.36.0 // indirect 46 | golang.org/x/net v0.38.0 // indirect 47 | golang.org/x/sys v0.31.0 // indirect 48 | golang.org/x/text v0.23.0 // indirect 49 | google.golang.org/protobuf v1.34.2 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= 2 | github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 6 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 7 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= 13 | github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= 14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 16 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 17 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 18 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 19 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 20 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 22 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 23 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 24 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 25 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 26 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 27 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 28 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 29 | github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= 30 | github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 31 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 32 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 33 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 34 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 36 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 37 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 38 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 39 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 40 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 41 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 42 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 43 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 44 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 45 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 46 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 47 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 48 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 52 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 53 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 54 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 58 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 59 | github.com/samber/slog-formatter v1.0.1 h1:p7siOGfBrxD/Pdaqg+caRtEp3EfLch1MwHqerL6IBGs= 60 | github.com/samber/slog-formatter v1.0.1/go.mod h1:xJvsffDWM5KxZCucmT9FfX80QfHMr2K92gv/9rO3Sr4= 61 | github.com/samber/slog-multi v1.1.0 h1:m5wfpXE8Qu2gCiR/JnhFGsLcWDOmTxnso32EMffVAY0= 62 | github.com/samber/slog-multi v1.1.0/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 65 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 66 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 67 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 68 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 69 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 71 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 72 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 73 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 74 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 75 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 76 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 77 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 78 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 79 | go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.53.0 h1:ktt8061VV/UU5pdPF6AcEFyuPxMizf/vU6eD1l+13LI= 80 | go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.53.0/go.mod h1:JSRiHPV7E3dbOAP0N6SRPg2nC/cugJnVXRqP018ejtY= 81 | go.opentelemetry.io/contrib/propagators/b3 v1.28.0 h1:XR6CFQrQ/ttAYmTBX2loUEFGdk1h17pxYI8828dk/1Y= 82 | go.opentelemetry.io/contrib/propagators/b3 v1.28.0/go.mod h1:DWRkzJONLquRz7OJPh2rRbZ7MugQj62rk7g6HRnEqh0= 83 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 84 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 85 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 86 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 87 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 88 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 89 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 90 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 91 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 92 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 93 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 94 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 95 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 96 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 97 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 98 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 99 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 100 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 103 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 104 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 105 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 106 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 107 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 108 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 109 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 110 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 111 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 112 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 113 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 114 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 115 | -------------------------------------------------------------------------------- /filters.go: -------------------------------------------------------------------------------- 1 | package sloggin 2 | 3 | import ( 4 | "regexp" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type Filter func(ctx *gin.Context) bool 12 | 13 | // Basic 14 | func Accept(filter Filter) Filter { return filter } 15 | func Ignore(filter Filter) Filter { return func(ctx *gin.Context) bool { return !filter(ctx) } } 16 | 17 | // Method 18 | func AcceptMethod(methods ...string) Filter { 19 | return func(c *gin.Context) bool { 20 | reqMethod := strings.ToLower(c.Request.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 *gin.Context) bool { 34 | reqMethod := strings.ToLower(c.Request.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 *gin.Context) bool { 49 | return slices.Contains(statuses, c.Writer.Status()) 50 | } 51 | } 52 | 53 | func IgnoreStatus(statuses ...int) Filter { 54 | return func(c *gin.Context) bool { 55 | return !slices.Contains(statuses, c.Writer.Status()) 56 | } 57 | } 58 | 59 | func AcceptStatusGreaterThan(status int) Filter { 60 | return func(c *gin.Context) bool { 61 | return c.Writer.Status() > status 62 | } 63 | } 64 | 65 | func AcceptStatusGreaterThanOrEqual(status int) Filter { 66 | return func(c *gin.Context) bool { 67 | return c.Writer.Status() >= status 68 | } 69 | } 70 | 71 | func AcceptStatusLessThan(status int) Filter { 72 | return func(c *gin.Context) bool { 73 | return c.Writer.Status() < status 74 | } 75 | } 76 | 77 | func AcceptStatusLessThanOrEqual(status int) Filter { 78 | return func(c *gin.Context) bool { 79 | return c.Writer.Status() <= 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 *gin.Context) bool { 102 | return slices.Contains(urls, c.Request.URL.Path) 103 | } 104 | } 105 | 106 | func IgnorePath(urls ...string) Filter { 107 | return func(c *gin.Context) bool { 108 | return !slices.Contains(urls, c.Request.URL.Path) 109 | } 110 | } 111 | 112 | func AcceptPathContains(parts ...string) Filter { 113 | return func(c *gin.Context) bool { 114 | for _, part := range parts { 115 | if strings.Contains(c.Request.URL.Path, part) { 116 | return true 117 | } 118 | } 119 | 120 | return false 121 | } 122 | } 123 | 124 | func IgnorePathContains(parts ...string) Filter { 125 | return func(c *gin.Context) bool { 126 | for _, part := range parts { 127 | if strings.Contains(c.Request.URL.Path, part) { 128 | return false 129 | } 130 | } 131 | 132 | return true 133 | } 134 | } 135 | 136 | func AcceptPathPrefix(prefixs ...string) Filter { 137 | return func(c *gin.Context) bool { 138 | for _, prefix := range prefixs { 139 | if strings.HasPrefix(c.Request.URL.Path, prefix) { 140 | return true 141 | } 142 | } 143 | 144 | return false 145 | } 146 | } 147 | 148 | func IgnorePathPrefix(prefixs ...string) Filter { 149 | return func(c *gin.Context) bool { 150 | for _, prefix := range prefixs { 151 | if strings.HasPrefix(c.Request.URL.Path, prefix) { 152 | return false 153 | } 154 | } 155 | 156 | return true 157 | } 158 | } 159 | 160 | func AcceptPathSuffix(prefixs ...string) Filter { 161 | return func(c *gin.Context) bool { 162 | for _, prefix := range prefixs { 163 | if strings.HasPrefix(c.Request.URL.Path, prefix) { 164 | return true 165 | } 166 | } 167 | 168 | return false 169 | } 170 | } 171 | 172 | func IgnorePathSuffix(suffixs ...string) Filter { 173 | return func(c *gin.Context) bool { 174 | for _, suffix := range suffixs { 175 | if strings.HasSuffix(c.Request.URL.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 *gin.Context) bool { 186 | for _, reg := range regs { 187 | if reg.Match([]byte(c.Request.URL.Path)) { 188 | return true 189 | } 190 | } 191 | 192 | return false 193 | } 194 | } 195 | 196 | func IgnorePathMatch(regs ...regexp.Regexp) Filter { 197 | return func(c *gin.Context) bool { 198 | for _, reg := range regs { 199 | if reg.Match([]byte(c.Request.URL.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 *gin.Context) bool { 211 | return slices.Contains(hosts, c.Request.URL.Host) 212 | } 213 | } 214 | 215 | func IgnoreHost(hosts ...string) Filter { 216 | return func(c *gin.Context) bool { 217 | return !slices.Contains(hosts, c.Request.URL.Host) 218 | } 219 | } 220 | 221 | func AcceptHostContains(parts ...string) Filter { 222 | return func(c *gin.Context) bool { 223 | for _, part := range parts { 224 | if strings.Contains(c.Request.URL.Host, part) { 225 | return true 226 | } 227 | } 228 | 229 | return false 230 | } 231 | } 232 | 233 | func IgnoreHostContains(parts ...string) Filter { 234 | return func(c *gin.Context) bool { 235 | for _, part := range parts { 236 | if strings.Contains(c.Request.URL.Host, part) { 237 | return false 238 | } 239 | } 240 | 241 | return true 242 | } 243 | } 244 | 245 | func AcceptHostPrefix(prefixs ...string) Filter { 246 | return func(c *gin.Context) bool { 247 | for _, prefix := range prefixs { 248 | if strings.HasPrefix(c.Request.URL.Host, prefix) { 249 | return true 250 | } 251 | } 252 | 253 | return false 254 | } 255 | } 256 | 257 | func IgnoreHostPrefix(prefixs ...string) Filter { 258 | return func(c *gin.Context) bool { 259 | for _, prefix := range prefixs { 260 | if strings.HasPrefix(c.Request.URL.Host, prefix) { 261 | return false 262 | } 263 | } 264 | 265 | return true 266 | } 267 | } 268 | 269 | func AcceptHostSuffix(prefixs ...string) Filter { 270 | return func(c *gin.Context) bool { 271 | for _, prefix := range prefixs { 272 | if strings.HasPrefix(c.Request.URL.Host, prefix) { 273 | return true 274 | } 275 | } 276 | 277 | return false 278 | } 279 | } 280 | 281 | func IgnoreHostSuffix(suffixs ...string) Filter { 282 | return func(c *gin.Context) bool { 283 | for _, suffix := range suffixs { 284 | if strings.HasSuffix(c.Request.URL.Host, suffix) { 285 | return false 286 | } 287 | } 288 | 289 | return true 290 | } 291 | } 292 | 293 | func AcceptHostMatch(regs ...regexp.Regexp) Filter { 294 | return func(c *gin.Context) bool { 295 | for _, reg := range regs { 296 | if reg.Match([]byte(c.Request.URL.Host)) { 297 | return true 298 | } 299 | } 300 | 301 | return false 302 | } 303 | } 304 | 305 | func IgnoreHostMatch(regs ...regexp.Regexp) Filter { 306 | return func(c *gin.Context) bool { 307 | for _, reg := range regs { 308 | if reg.Match([]byte(c.Request.URL.Host)) { 309 | return false 310 | } 311 | } 312 | 313 | return true 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samber/slog-gin 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/bytedance/sonic v1.11.9 // indirect 7 | github.com/bytedance/sonic/loader v0.1.1 // indirect 8 | github.com/cloudwego/base64x v0.1.4 // indirect 9 | github.com/cloudwego/iasm v0.2.0 // indirect 10 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.22.0 // indirect 15 | github.com/goccy/go-json v0.10.3 // indirect 16 | github.com/json-iterator/go v1.1.12 // indirect 17 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/leodido/go-urn v1.4.0 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 22 | github.com/modern-go/reflect2 v1.0.2 // indirect 23 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 24 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 25 | github.com/ugorji/go/codec v1.2.12 // indirect 26 | go.opentelemetry.io/otel v1.29.0 // indirect 27 | golang.org/x/arch v0.8.0 // indirect 28 | golang.org/x/crypto v0.31.0 // indirect 29 | golang.org/x/net v0.33.0 // indirect 30 | golang.org/x/sys v0.28.0 // indirect 31 | golang.org/x/text v0.21.0 // indirect 32 | google.golang.org/protobuf v1.34.2 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | 36 | require ( 37 | github.com/gin-gonic/gin v1.10.1 38 | github.com/google/uuid v1.6.0 39 | go.opentelemetry.io/otel/trace v1.29.0 40 | go.uber.org/goleak v1.3.0 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= 2 | github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 6 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 7 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= 14 | github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= 15 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 16 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 17 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 18 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 19 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 20 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 21 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 22 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 23 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 24 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 25 | github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= 26 | github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 27 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 28 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 29 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 30 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 33 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 34 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 35 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 36 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 37 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 38 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 39 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 40 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 41 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 42 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 43 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 44 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 45 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 46 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 47 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 48 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 52 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 53 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 54 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 59 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 60 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 61 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 62 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 65 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 66 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 67 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 68 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 69 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 70 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 71 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 72 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 73 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 74 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 75 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 76 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 77 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 78 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 79 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 80 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 81 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 82 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 83 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 84 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 85 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 86 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 89 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 90 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 91 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 92 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 93 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 96 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 99 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 101 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 102 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= 2 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 3 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 4 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 5 | github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= 6 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 7 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 8 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 9 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 10 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 11 | golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= 12 | golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 13 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 14 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 15 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 16 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 17 | nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= 18 | rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= 19 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package sloggin 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 sloggin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/google/uuid" 13 | "go.opentelemetry.io/otel/trace" 14 | ) 15 | 16 | const ( 17 | customAttributesCtxKey = "slog-gin.custom-attributes" 18 | requestIDCtx = "slog-gin.request-id" 19 | ) 20 | 21 | var ( 22 | TraceIDKey = "trace_id" 23 | SpanIDKey = "span_id" 24 | RequestIDKey = "id" 25 | 26 | RequestBodyMaxSize = 64 * 1024 // 64KB 27 | ResponseBodyMaxSize = 64 * 1024 // 64KB 28 | 29 | HiddenRequestHeaders = map[string]struct{}{ 30 | "authorization": {}, 31 | "cookie": {}, 32 | "set-cookie": {}, 33 | "x-auth-token": {}, 34 | "x-csrf-token": {}, 35 | "x-xsrf-token": {}, 36 | } 37 | HiddenResponseHeaders = map[string]struct{}{ 38 | "set-cookie": {}, 39 | } 40 | 41 | // Formatted with http.CanonicalHeaderKey 42 | RequestIDHeaderKey = "X-Request-Id" 43 | ) 44 | 45 | type Config struct { 46 | DefaultLevel slog.Level 47 | ClientErrorLevel slog.Level 48 | ServerErrorLevel slog.Level 49 | 50 | WithUserAgent bool 51 | WithRequestID bool 52 | WithRequestBody bool 53 | WithRequestHeader bool 54 | WithResponseBody bool 55 | WithResponseHeader bool 56 | WithSpanID bool 57 | WithTraceID bool 58 | 59 | Filters []Filter 60 | } 61 | 62 | // New returns a gin.HandlerFunc (middleware) that logs requests using slog. 63 | // 64 | // Requests with errors are logged using slog.Error(). 65 | // Requests without errors are logged using slog.Info(). 66 | func New(logger *slog.Logger) gin.HandlerFunc { 67 | return NewWithConfig(logger, Config{ 68 | DefaultLevel: slog.LevelInfo, 69 | ClientErrorLevel: slog.LevelWarn, 70 | ServerErrorLevel: slog.LevelError, 71 | 72 | WithUserAgent: false, 73 | WithRequestID: true, 74 | WithRequestBody: false, 75 | WithRequestHeader: false, 76 | WithResponseBody: false, 77 | WithResponseHeader: false, 78 | WithSpanID: false, 79 | WithTraceID: false, 80 | 81 | Filters: []Filter{}, 82 | }) 83 | } 84 | 85 | // NewWithFilters returns a gin.HandlerFunc (middleware) that logs requests using slog. 86 | // 87 | // Requests with errors are logged using slog.Error(). 88 | // Requests without errors are logged using slog.Info(). 89 | func NewWithFilters(logger *slog.Logger, filters ...Filter) gin.HandlerFunc { 90 | return NewWithConfig(logger, Config{ 91 | DefaultLevel: slog.LevelInfo, 92 | ClientErrorLevel: slog.LevelWarn, 93 | ServerErrorLevel: slog.LevelError, 94 | 95 | WithUserAgent: false, 96 | WithRequestID: true, 97 | WithRequestBody: false, 98 | WithRequestHeader: false, 99 | WithResponseBody: false, 100 | WithResponseHeader: false, 101 | WithSpanID: false, 102 | WithTraceID: false, 103 | 104 | Filters: filters, 105 | }) 106 | } 107 | 108 | // NewWithConfig returns a gin.HandlerFunc (middleware) that logs requests using slog. 109 | func NewWithConfig(logger *slog.Logger, config Config) gin.HandlerFunc { 110 | return func(c *gin.Context) { 111 | start := time.Now() 112 | path := c.Request.URL.Path 113 | query := c.Request.URL.RawQuery 114 | 115 | params := map[string]string{} 116 | for _, p := range c.Params { 117 | params[p.Key] = p.Value 118 | } 119 | 120 | requestID := c.GetHeader(RequestIDHeaderKey) 121 | if config.WithRequestID { 122 | if requestID == "" { 123 | requestID = uuid.New().String() 124 | c.Header(RequestIDHeaderKey, requestID) 125 | } 126 | c.Set(requestIDCtx, requestID) 127 | } 128 | 129 | // dump request body 130 | br := newBodyReader(c.Request.Body, RequestBodyMaxSize, config.WithRequestBody) 131 | c.Request.Body = br 132 | 133 | // dump response body 134 | bw := newBodyWriter(c.Writer, ResponseBodyMaxSize, config.WithResponseBody) 135 | c.Writer = bw 136 | 137 | c.Next() 138 | 139 | // Pass thru filters and skip early the code below, to prevent unnecessary processing. 140 | for _, filter := range config.Filters { 141 | if !filter(c) { 142 | return 143 | } 144 | } 145 | 146 | status := c.Writer.Status() 147 | method := c.Request.Method 148 | host := c.Request.Host 149 | route := c.FullPath() 150 | end := time.Now() 151 | latency := end.Sub(start) 152 | userAgent := c.Request.UserAgent() 153 | ip := c.ClientIP() 154 | referer := c.Request.Referer() 155 | 156 | baseAttributes := []slog.Attr{} 157 | 158 | requestAttributes := []slog.Attr{ 159 | slog.Time("time", start.UTC()), 160 | slog.String("method", method), 161 | slog.String("host", host), 162 | slog.String("path", path), 163 | slog.String("query", query), 164 | slog.Any("params", params), 165 | slog.String("route", route), 166 | slog.String("ip", ip), 167 | slog.String("referer", referer), 168 | } 169 | 170 | responseAttributes := []slog.Attr{ 171 | slog.Time("time", end.UTC()), 172 | slog.Duration("latency", latency), 173 | slog.Int("status", status), 174 | } 175 | 176 | if config.WithRequestID { 177 | baseAttributes = append(baseAttributes, slog.String(RequestIDKey, requestID)) 178 | } 179 | 180 | // otel 181 | baseAttributes = append(baseAttributes, extractTraceSpanID(c.Request.Context(), config.WithTraceID, config.WithSpanID)...) 182 | 183 | // request body 184 | requestAttributes = append(requestAttributes, slog.Int("length", br.bytes)) 185 | if config.WithRequestBody { 186 | requestAttributes = append(requestAttributes, slog.String("body", br.body.String())) 187 | } 188 | 189 | // request headers 190 | if config.WithRequestHeader { 191 | kv := []any{} 192 | 193 | for k, v := range c.Request.Header { 194 | if _, found := HiddenRequestHeaders[strings.ToLower(k)]; found { 195 | continue 196 | } 197 | kv = append(kv, slog.Any(k, v)) 198 | } 199 | 200 | requestAttributes = append(requestAttributes, slog.Group("header", kv...)) 201 | } 202 | 203 | if config.WithUserAgent { 204 | requestAttributes = append(requestAttributes, slog.String("user-agent", userAgent)) 205 | } 206 | 207 | // response body 208 | responseAttributes = append(responseAttributes, slog.Int("length", bw.bytes)) 209 | if config.WithResponseBody { 210 | responseAttributes = append(responseAttributes, slog.String("body", bw.body.String())) 211 | } 212 | 213 | // response headers 214 | if config.WithResponseHeader { 215 | kv := []any{} 216 | 217 | for k, v := range c.Writer.Header() { 218 | if _, found := HiddenResponseHeaders[strings.ToLower(k)]; found { 219 | continue 220 | } 221 | kv = append(kv, slog.Any(k, v)) 222 | } 223 | 224 | responseAttributes = append(responseAttributes, slog.Group("header", kv...)) 225 | } 226 | 227 | attributes := append( 228 | []slog.Attr{ 229 | { 230 | Key: "request", 231 | Value: slog.GroupValue(requestAttributes...), 232 | }, 233 | { 234 | Key: "response", 235 | Value: slog.GroupValue(responseAttributes...), 236 | }, 237 | }, 238 | baseAttributes..., 239 | ) 240 | 241 | // custom context values 242 | if v, ok := c.Get(customAttributesCtxKey); ok { 243 | switch attrs := v.(type) { 244 | case []slog.Attr: 245 | attributes = append(attributes, attrs...) 246 | } 247 | } 248 | 249 | level := config.DefaultLevel 250 | msg := "Incoming request" 251 | if status >= http.StatusBadRequest && status < http.StatusInternalServerError { 252 | level = config.ClientErrorLevel 253 | msg = strings.TrimSuffix(c.Errors.String(), "\n") 254 | if msg == "" { 255 | msg = fmt.Sprintf("HTTP error: %d %s", status, strings.ToLower(http.StatusText(status))) 256 | } 257 | } else if status >= http.StatusInternalServerError { 258 | level = config.ServerErrorLevel 259 | msg = strings.TrimSuffix(c.Errors.String(), "\n") 260 | if msg == "" { 261 | msg = fmt.Sprintf("HTTP error: %d %s", status, strings.ToLower(http.StatusText(status))) 262 | } 263 | } 264 | 265 | logger.LogAttrs(c.Request.Context(), level, msg, attributes...) 266 | } 267 | } 268 | 269 | // GetRequestID returns the request identifier. 270 | func GetRequestID(c *gin.Context) string { 271 | requestID, ok := c.Get(requestIDCtx) 272 | if !ok { 273 | return "" 274 | } 275 | 276 | if id, ok := requestID.(string); ok { 277 | return id 278 | } 279 | 280 | return "" 281 | } 282 | 283 | // AddCustomAttributes adds custom attributes to the request context. 284 | func AddCustomAttributes(c *gin.Context, attr slog.Attr) { 285 | v, exists := c.Get(customAttributesCtxKey) 286 | if !exists { 287 | c.Set(customAttributesCtxKey, []slog.Attr{attr}) 288 | return 289 | } 290 | 291 | switch attrs := v.(type) { 292 | case []slog.Attr: 293 | c.Set(customAttributesCtxKey, append(attrs, attr)) 294 | } 295 | } 296 | 297 | func extractTraceSpanID(ctx context.Context, withTraceID bool, withSpanID bool) []slog.Attr { 298 | if !withTraceID && !withSpanID { 299 | return []slog.Attr{} 300 | } 301 | 302 | span := trace.SpanFromContext(ctx) 303 | if !span.IsRecording() { 304 | return []slog.Attr{} 305 | } 306 | 307 | attrs := []slog.Attr{} 308 | spanCtx := span.SpanContext() 309 | 310 | if withTraceID && spanCtx.HasTraceID() { 311 | traceID := trace.SpanFromContext(ctx).SpanContext().TraceID().String() 312 | attrs = append(attrs, slog.String(TraceIDKey, traceID)) 313 | } 314 | 315 | if withSpanID && spanCtx.HasSpanID() { 316 | spanID := spanCtx.SpanID().String() 317 | attrs = append(attrs, slog.String(SpanIDKey, spanID)) 318 | } 319 | 320 | return attrs 321 | } 322 | --------------------------------------------------------------------------------