From 445fe9542f8e36b367db03bfb5183e47e0b8fd9e Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Sun, 16 Apr 2023 21:34:36 +0200 Subject: [PATCH] initial commit --- .github/FUNDING.yml | 1 + .github/workflows/lint.yml | 44 ++++++ .github/workflows/release.yml | 56 +++++++ .github/workflows/test.yml | 37 +++++ .gitignore | 38 +++++ LICENSE | 21 +++ Makefile | 43 +++++ README.md | 287 ++++++++++++++++++++++++++++++++++ examples/example.go | 48 ++++++ go.mod | 38 +++++ go.sum | 88 +++++++++++ middleware.go | 59 +++++++ 12 files changed, 760 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 examples/example.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 middleware.go diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e4e0d3c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [samber] diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..26ae514 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +name: Lint + +on: + push: + tags: + branches: + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v2 + with: + go-version: 1.20.1 + stable: false + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + working-directory: ./ + + # Optional: golangci-lint command line arguments. + args: --timeout 60s --max-same-issues 50 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the action will use pre-installed Go. + # skip-go-installation: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + # optionally use a specific version of Go rather than the latest one + go_version: '1.20.1' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a298aec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release + +on: + workflow_dispatch: + inputs: + semver: + type: string + description: 'Semver (eg: v1.2.3)' + required: true + +jobs: + release: + if: github.triggering_actor == 'samber' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.20.1 + stable: false + + - name: Test + run: make test + + # remove tests in order to clean dependencies + - name: Remove xxx_test.go files + run: rm -rf *_test.go ./examples ./images + + # cleanup test dependencies + - name: Cleanup dependencies + run: go mod tidy + + - name: List files + run: tree -Cfi + - name: Write new go.mod into logs + run: cat go.mod + - name: Write new go.sum into logs + run: cat go.sum + + - name: Create tag + run: | + git config --global user.name '${{ github.triggering_actor }}' + git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com" + + git add . + git commit --allow-empty -m 'bump ${{ inputs.semver }}' + git tag ${{ inputs.semver }} + git push origin ${{ inputs.semver }} + + - name: Release + uses: softprops/action-gh-release@v1 + with: + name: ${{ inputs.semver }} + tag_name: ${{ inputs.semver }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b6b2299 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + push: + tags: + branches: + pull_request: + +jobs: + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.20.1 + stable: false + + - name: Build + run: make build + + - name: Test + run: make test + + - name: Test + run: make coverage + + - name: Codecov + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./cover.out + flags: unittests + verbose: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5ecc5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Go Patch ### +/vendor/ +/Godeps/ + +# End of https://www.toptal.com/developers/gitignore/api/go + +cover.out +cover.html +.vscode + +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4845c99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Samuel Berthe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5e189e7 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ + +BIN=go + +build: + ${BIN} build -v ./... + +test: + go test -race -v ./... +watch-test: + reflex -t 50ms -s -- sh -c 'gotest -race -v ./...' + +bench: + go test -benchmem -count 3 -bench ./... +watch-bench: + reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' + +coverage: + ${BIN} test -v -coverprofile=cover.out -covermode=atomic . + ${BIN} tool cover -html=cover.out -o cover.html + +tools: + ${BIN} install github.com/cespare/reflex@latest + ${BIN} install github.com/rakyll/gotest@latest + ${BIN} install github.com/psampaz/go-mod-outdated@latest + ${BIN} install github.com/jondot/goweight@latest + ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + ${BIN} get -t -u golang.org/x/tools/cmd/cover + ${BIN} install github.com/sonatype-nexus-community/nancy@latest + go mod tidy + +lint: + golangci-lint run --timeout 60s --max-same-issues 50 ./... +lint-fix: + golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... + +audit: tools + ${BIN} list -json -m all | nancy sleuth + +outdated: tools + ${BIN} list -u -m -json all | go-mod-outdated -update -direct + +weight: tools + goweight diff --git a/README.md b/README.md new file mode 100644 index 0000000..14df134 --- /dev/null +++ b/README.md @@ -0,0 +1,287 @@ + +# slog: Gin middleware + +[![tag](https://img.shields.io/github/tag/samber/slog-gin.svg)](https://github.com/samber/slog-gin/releases) +![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.20.1-%23007d9c) +[![GoDoc](https://godoc.org/github.com/samber/slog-gin?status.svg)](https://pkg.go.dev/github.com/samber/slog-gin) +![Build Status](https://github.com/samber/slog-gin/actions/workflows/test.yml/badge.svg) +[![Go report](https://goreportcard.com/badge/github.com/samber/slog-gin)](https://goreportcard.com/report/github.com/samber/slog-gin) +[![Coverage](https://img.shields.io/codecov/c/github/samber/slog-gin)](https://codecov.io/gh/samber/slog-gin) +[![Contributors](https://img.shields.io/github/contributors/samber/slog-gin)](https://github.com/samber/slog-gin/graphs/contributors) +[![License](https://img.shields.io/github/license/samber/slog-gin)](./LICENSE) + +Gin middleware to log http requests using [slog](https://pkg.go.dev/golang.org/x/exp/slog). + +**See also:** + +- [slog-multi](https://github.com/samber/slog-multi): workflows of `slog` handlers (pipeline, fanout, ...) +- [slog-formatter](https://github.com/samber/slog-formatter): `slog` attribute formatting +- [slog-datadog](https://github.com/samber/slog-datadog): A `slog` handler for `Datadog` +- [slog-logstash](https://github.com/samber/slog-logstash): A `slog` handler for `Logstash` +- [slog-slack](https://github.com/samber/slog-slack): A `slog` handler for `Slack` +- [slog-loki](https://github.com/samber/slog-loki): A `slog` handler for `Loki` +- [slog-sentry](https://github.com/samber/slog-sentry): A `slog` handler for `Sentry` +- [slog-fluentd](https://github.com/samber/slog-fluentd): A `slog` handler for `Fluentd` +- [slog-syslog](https://github.com/samber/slog-syslog): A `slog` handler for `Syslog` +- [slog-graylog](https://github.com/samber/slog-graylog): A `slog` handler for `Graylog` + +## 🚀 Install + +```sh +go get github.com/samber/slog-gin +``` + +**Compatibility**: go >= 1.20.1 + +This library is v0 and follows SemVer strictly. On `slog` final release (go 1.21), this library will go v1. + +No breaking changes will be made to exported APIs before v1.0.0. + +## 💡 Usage + +### Minimal + +```go +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +// Create a slog logger, which: +// - Logs to stdout. +logger := slog.New(slog.NewTextHandler(os.Stdout)) + +router := gin.New() + +// Add the sloggin middleware to all routes. +// The middleware will log all requests attributes. +router.Use(sloggin.New(logger)) + +// Example pong request. +router.GET("/pong", func(c *gin.Context) { + c.String(http.StatusOK, "pong") +}) + +router.Run(":1234") + +// output: +// time=2023-04-10T14:00:0.000000+02:00 level=INFO msg="HTTP Request" status=200 method=GET path=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time=2023-04-10T14:00:00.000+02:00 +``` + +### Using custom time formatters + +```go +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +// Create a slog logger, which: +// - Logs to stdout. +// - RFC3339 with UTC time format. +logger := slog.New( + slogformatter.NewFormatterHandler( + slogformatter.TimezoneConverter(time.UTC), + slogformatter.TimeFormatter(time.DateTime, nil), + )( + slog.NewTextHandler(os.Stdout), + ), +) + +router := gin.New() + +// Add the sloggin middleware to all routes. +// The middleware will log all requests attributes. +router.Use(sloggin.New(logger)) + +// Example pong request. +router.GET("/pong", func(c *gin.Context) { + c.String(http.StatusOK, "pong") +}) + +router.Run(":1234") + +// output: +// time="2023-04-10 14:00:00" level=INFO msg="HTTP Request" status=200 method=GET path=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time="2023-04-10 14:00:00" +``` + +### Using custom logger sub-group + +```go +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +// Create a slog logger, which: +// - Logs to stdout. +logger := slog.New(slog.NewTextHandler(os.Stdout)) + +router := gin.New() + +// Add the sloggin middleware to all routes. +// The middleware will log all requests attributes under a "http" group. +router.Use(sloggin.New(logger.WithGroup("http"))) + +// Example pong request. +router.GET("/pong", func(c *gin.Context) { + c.String(http.StatusOK, "pong") +}) + +router.Run(":1234") + +// output: +// time=2023-04-10T14:00:0.000000+02:00 level=INFO msg="HTTP Request" http.status=200 http.method=GET http.path=/pong http.ip=127.0.0.1 http.latency=20.125µs http.user-agent=curl/7.77.0 time=2023-04-10T14:00:00.000+02:00 +``` + +### Add logger to a single route + +```go +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +// Create a slog logger, which: +// - Logs to stdout. +logger := slog.New(slog.NewTextHandler(os.Stdout)) + +router := gin.New() + +// Example pong request. +// Add the sloggin middleware to a single routes. +router.GET("/pong", sloggin.New(logger), func(c *gin.Context) { + c.String(http.StatusOK, "pong") +}) + +router.Run(":1234") + +// output: +// time="2023-04-10 14:00:00" level=INFO msg="HTTP Request" status=200 method=GET path=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time="2023-04-10 14:00:00" +``` + +### Adding custom attributes + +```go +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +// Create a slog logger, which: +// - Logs to stdout. +logger := slog.New(slog.NewTextHandler(os.Stdout)). + With("environment", "production"). + With("server", "gin/1.9.0"). + With("server_start_time", time.Now()). + With("gin_mode", gin.EnvGinMode) + +router := gin.New() + +// Add the sloggin middleware to all routes. +// The middleware will log all requests attributes. +router.Use(sloggin.New(logger)) + +// Example pong request. +router.GET("/pong", func(c *gin.Context) { + c.String(http.StatusOK, "pong") +}) + +router.Run(":1234") + +// output: +// time=2023-04-10T14:00:0.000000+02:00 level=INFO msg="HTTP Request" environment=production server=gin/1.9.0 gin_mode=release server_start_time=2023-04-10T10:00:00.000+02:00 status=200 method=GET path=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time=2023-04-10T14:00:00.000+02:00 +``` + +### JSON output + +```go +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +// Create a slog logger, which: +// - Logs to stdout. +logger := slog.New(slog.NewJSONHandler(os.Stdout)) + +router := gin.New() + +// Add the sloggin middleware to all routes. +// The middleware will log all requests attributes. +router.Use(sloggin.New(logger)) + +// Example pong request. +router.GET("/pong", func(c *gin.Context) { + c.String(http.StatusOK, "pong") +}) + +router.Run(":1234") + +// output: +// {"time":"2023-04-10T14:00:0.000000+02:00","level":"INFO","msg":"HTTP Request","gin_mode":"GIN_MODE","status":200,"method":"GET","path":"/pong","ip":"127.0.0.1","latency":15542,"user-agent":"curl/7.77.0","time":"2023-04-10T14:00:0.000000+02:00"} +``` + +## 🤝 Contributing + +- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) +- Fork the [project](https://github.com/samber/slog-gin) +- Fix [open issues](https://github.com/samber/slog-gin/issues) or request new features + +Don't hesitate ;) + +```bash +# Install some dev dependencies +make tools + +# Run tests +make test +# or +make watch-test +``` + +## 👤 Contributors + +![Contributors](https://contrib.rocks/image?repo=samber/slog-gin) + +## 💫 Show your support + +Give a ⭐️ if this project helped you! + +[![GitHub Sponsors](https://img.shields.io/github/sponsors/samber?style=for-the-badge)](https://github.com/sponsors/samber) + +## 📝 License + +Copyright © 2023 [Samuel Berthe](https://github.com/samber). + +This project is [MIT](./LICENSE) licensed. diff --git a/examples/example.go b/examples/example.go new file mode 100644 index 0000000..6b9fcf4 --- /dev/null +++ b/examples/example.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + slogformatter "github.com/samber/slog-formatter" + sloggin "github.com/samber/slog-gin" + "golang.org/x/exp/slog" +) + +func main() { + // Create a slog logger, which: + // - Logs to stdout. + // - RFC3339 with UTC time format. + logger := slog.New( + slogformatter.NewFormatterHandler( + slogformatter.TimezoneConverter(time.UTC), + slogformatter.TimeFormatter(time.RFC3339, nil), + )( + slog.NewJSONHandler(os.Stdout), + ), + ) + + // Add an attribute to all log entries made through this logger. + logger = logger.With("gin_mode", gin.EnvGinMode) + + router := gin.New() + + // Add the sloggin middleware to all routes. + // The middleware will log all requests attributes under a "http" group. + router.Use(sloggin.New(logger.WithGroup("http"))) + + // Example pong request. + router.GET("/pong", func(c *gin.Context) { + c.String(http.StatusOK, "pong") + }) + + logger.Info("Starting server") + if err := router.Run(":1234"); err != nil { + logger.Error("can' start server with 1234 port") + } + + // output: + // time=2023-04-10T14:00:0.000000+00:00 level=ERROR msg="HTTP 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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..08b5283 --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module github.com/samber/slog-gin + +go 1.20 + +require golang.org/x/exp v0.0.0-20230321023759-10a507213a29 + +require ( + github.com/bytedance/sonic v1.8.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.11.2 // indirect + github.com/goccy/go-json v0.10.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/samber/slog-multi v0.4.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.9 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/gin-gonic/gin v1.9.0 + github.com/samber/slog-formatter v0.3.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6630f36 --- /dev/null +++ b/go.sum @@ -0,0 +1,88 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= +github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= +github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/slog-formatter v0.3.2 h1:yOxgpBmiLyMYqx8txnwOekfPEHR3yZ9JtHSc9Ef+lQY= +github.com/samber/slog-formatter v0.3.2/go.mod h1:YKAVaCNPbyEaGpRWrbhkGUlu7n2J+4P54XAER8TxzVU= +github.com/samber/slog-multi v0.4.0 h1:QTQAo+9AP295irccqKdNwJ/2XflRMuL/aHqk7RblOhE= +github.com/samber/slog-multi v0.4.0/go.mod h1:QDicB1R5oTcbSSqlYmskphC7fNcjHgdxqZdns1eAvDc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= +github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..9e28da2 --- /dev/null +++ b/middleware.go @@ -0,0 +1,59 @@ +package sloggin + +import ( + "context" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/exp/slog" +) + +type Config struct { + DefaultLevel slog.Level + ClientErrorLevel slog.Level + ServerErrorLevel slog.Level +} + +// New returns a gin.HandlerFunc (middleware) that logs requests using slog. +// +// Requests with errors are logged using slog.Error(). +// Requests without errors are logged using slog.Info(). +func New(logger *slog.Logger) gin.HandlerFunc { + return NewWithConfig(logger, Config{ + DefaultLevel: slog.LevelInfo, + ClientErrorLevel: slog.LevelWarn, + ServerErrorLevel: slog.LevelError, + }) +} + +func NewWithConfig(logger *slog.Logger, config Config) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + + c.Next() + + end := time.Now() + latency := end.Sub(start) + + attributes := []slog.Attr{ + slog.Int("status", c.Writer.Status()), + slog.String("method", c.Request.Method), + slog.String("path", path), + slog.String("ip", c.ClientIP()), + slog.Duration("latency", latency), + slog.String("user-agent", c.Request.UserAgent()), + slog.Time("time", end), + } + + switch { + case c.Writer.Status() >= http.StatusBadRequest && c.Writer.Status() < http.StatusInternalServerError: + logger.LogAttrs(context.Background(), config.ClientErrorLevel, c.Errors.String(), attributes...) + case c.Writer.Status() >= http.StatusInternalServerError: + logger.LogAttrs(context.Background(), config.ServerErrorLevel, c.Errors.String(), attributes...) + default: + logger.LogAttrs(context.Background(), config.DefaultLevel, "HTTP Request", attributes...) + } + } +}