├── .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 | [](https://github.com/samber/slog-gin/releases)
5 | 
6 | [](https://pkg.go.dev/github.com/samber/slog-gin)
7 | 
8 | [](https://goreportcard.com/report/github.com/samber/slog-gin)
9 | [](https://codecov.io/gh/samber/slog-gin)
10 | [](https://github.com/samber/slog-gin/graphs/contributors)
11 | [](./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 |
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 | 
385 |
386 | ## 💫 Show your support
387 |
388 | Give a ⭐️ if this project helped you!
389 |
390 | [](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 |
--------------------------------------------------------------------------------