├── .github ├── dependabot.yml └── workflows │ ├── bearer.yml │ ├── codeql.yml │ ├── go.yml │ └── goreleaser.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── _example ├── example01 │ └── main.go ├── example02 │ └── main.go ├── example03 │ └── main.go └── example04 │ └── main.go ├── go.mod ├── go.sum ├── zap.go └── zap_test.go /.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/bearer.yml: -------------------------------------------------------------------------------- 1 | name: Bearer PR Check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | rule_check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - uses: reviewdog/action-setup@v1 19 | with: 20 | reviewdog_version: latest 21 | 22 | - name: Run Report 23 | id: report 24 | uses: bearer/bearer-action@v2 25 | with: 26 | format: rdjson 27 | output: rd.json 28 | diff: true 29 | 30 | - name: Run reviewdog 31 | if: always() 32 | env: 33 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | cat rd.json | reviewdog -f=rdjson -reporter=github-pr-review 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | - name: Setup go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version-file: go.mod 21 | check-latest: true 22 | - name: Setup golangci-lint 23 | uses: golangci/golangci-lint-action@v7 24 | with: 25 | version: v2.0 26 | 27 | test: 28 | strategy: 29 | matrix: 30 | os: [ubuntu-latest] 31 | go: [1.23, 1.24] 32 | include: 33 | - os: ubuntu-latest 34 | go-build: ~/.cache/go-build 35 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 36 | runs-on: ${{ matrix.os }} 37 | env: 38 | GO111MODULE: on 39 | GOPROXY: https://proxy.golang.org 40 | steps: 41 | - name: Set up Go ${{ matrix.go }} 42 | uses: actions/setup-go@v5 43 | with: 44 | go-version: ${{ matrix.go }} 45 | 46 | - name: Checkout Code 47 | uses: actions/checkout@v4 48 | with: 49 | ref: ${{ github.ref }} 50 | 51 | - uses: actions/cache@v4 52 | with: 53 | path: | 54 | ${{ matrix.go-build }} 55 | ~/go/pkg/mod 56 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 57 | restore-keys: | 58 | ${{ runner.os }}-go- 59 | - name: Run Tests 60 | run: | 61 | go test -v -covermode=atomic -coverprofile=coverage.out 62 | 63 | - name: Upload coverage to Codecov 64 | uses: codecov/codecov-action@v5 65 | with: 66 | flags: ${{ matrix.os }},go-${{ matrix.go }} 67 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | # either 'goreleaser' (default) or 'goreleaser-pro' 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - dogsled 7 | - dupl 8 | - errcheck 9 | - exhaustive 10 | - gochecknoinits 11 | - goconst 12 | - gocritic 13 | - gocyclo 14 | - goprintffuncname 15 | - gosec 16 | - govet 17 | - ineffassign 18 | - lll 19 | - misspell 20 | - nakedret 21 | - noctx 22 | - nolintlint 23 | - rowserrcheck 24 | - staticcheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - whitespace 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - gofumpt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | 4 | changelog: 5 | use: github 6 | groups: 7 | - title: Features 8 | regexp: "^.*feat[(\\w)]*:+.*$" 9 | order: 0 10 | - title: "Bug fixes" 11 | regexp: "^.*fix[(\\w)]*:+.*$" 12 | order: 1 13 | - title: "Enhancements" 14 | regexp: "^.*chore[(\\w)]*:+.*$" 15 | order: 2 16 | - title: "Refactor" 17 | regexp: "^.*refactor[(\\w)]*:+.*$" 18 | order: 3 19 | - title: "Build process updates" 20 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 21 | order: 4 22 | - title: "Documentation updates" 23 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: Others 26 | order: 999 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 gin-contrib 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zap 2 | 3 | [![Run Tests](https://github.com/gin-contrib/zap/actions/workflows/go.yml/badge.svg)](https://github.com/gin-contrib/zap/actions/workflows/go.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/zap)](https://goreportcard.com/report/github.com/gin-contrib/zap) 5 | [![GoDoc](https://godoc.org/github.com/gin-contrib/zap?status.svg)](https://godoc.org/github.com/gin-contrib/zap) 6 | [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin) 7 | 8 | Alternative logging through [zap](https://github.com/uber-go/zap). Thanks for [Pull Request](https://github.com/gin-gonic/contrib/pull/129) from [@yezooz](https://github.com/yezooz) 9 | 10 | ## Requirement 11 | 12 | Require Go **1.19** or later. 13 | 14 | ## Usage 15 | 16 | ### Start using it 17 | 18 | Download and install it: 19 | 20 | ```sh 21 | go get github.com/gin-contrib/zap 22 | ``` 23 | 24 | Import it in your code: 25 | 26 | ```go 27 | import "github.com/gin-contrib/zap" 28 | ``` 29 | 30 | ## Example 31 | 32 | See the [example](_example/example01/main.go). 33 | 34 | ```go 35 | package main 36 | 37 | import ( 38 | "fmt" 39 | "time" 40 | 41 | ginzap "github.com/gin-contrib/zap" 42 | "github.com/gin-gonic/gin" 43 | "go.uber.org/zap" 44 | ) 45 | 46 | func main() { 47 | r := gin.New() 48 | 49 | logger, _ := zap.NewProduction() 50 | 51 | // Add a ginzap middleware, which: 52 | // - Logs all requests, like a combined access and error log. 53 | // - Logs to stdout. 54 | // - RFC3339 with UTC time format. 55 | r.Use(ginzap.Ginzap(logger, time.RFC3339, true)) 56 | 57 | // Logs all panic to error log 58 | // - stack means whether output the stack info. 59 | r.Use(ginzap.RecoveryWithZap(logger, true)) 60 | 61 | // Example ping request. 62 | r.GET("/ping", func(c *gin.Context) { 63 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 64 | }) 65 | 66 | // Example when panic happen. 67 | r.GET("/panic", func(c *gin.Context) { 68 | panic("An unexpected error happen!") 69 | }) 70 | 71 | // Listen and Server in 0.0.0.0:8080 72 | r.Run(":8080") 73 | } 74 | ``` 75 | 76 | ## Skip logging 77 | 78 | When you want to skip logging for specific path, 79 | please use GinzapWithConfig 80 | 81 | ```go 82 | r.Use(GinzapWithConfig(utcLogger, &Config{ 83 | TimeFormat: time.RFC3339, 84 | UTC: true, 85 | SkipPaths: []string{"/no_log"}, 86 | })) 87 | ``` 88 | 89 | ## Custom Zap fields 90 | 91 | example for custom log request body, response request ID or log [Open Telemetry](https://opentelemetry.io/) TraceID. 92 | 93 | ```go 94 | func main() { 95 | r := gin.New() 96 | 97 | logger, _ := zap.NewProduction() 98 | 99 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 100 | UTC: true, 101 | TimeFormat: time.RFC3339, 102 | Context: ginzap.Fn(func(c *gin.Context) []zapcore.Field { 103 | fields := []zapcore.Field{} 104 | // log request ID 105 | if requestID := c.Writer.Header().Get("X-Request-Id"); requestID != "" { 106 | fields = append(fields, zap.String("request_id", requestID)) 107 | } 108 | 109 | // log trace and span ID 110 | if trace.SpanFromContext(c.Request.Context()).SpanContext().IsValid() { 111 | fields = append(fields, zap.String("trace_id", trace.SpanFromContext(c.Request.Context()).SpanContext().TraceID().String())) 112 | fields = append(fields, zap.String("span_id", trace.SpanFromContext(c.Request.Context()).SpanContext().SpanID().String())) 113 | } 114 | 115 | // log request body 116 | var body []byte 117 | var buf bytes.Buffer 118 | tee := io.TeeReader(c.Request.Body, &buf) 119 | body, _ = io.ReadAll(tee) 120 | c.Request.Body = io.NopCloser(&buf) 121 | fields = append(fields, zap.String("body", string(body))) 122 | 123 | return fields 124 | }), 125 | })) 126 | 127 | // Example ping request. 128 | r.GET("/ping", func(c *gin.Context) { 129 | c.Writer.Header().Add("X-Request-Id", "1234-5678-9012") 130 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 131 | }) 132 | 133 | r.POST("/ping", func(c *gin.Context) { 134 | c.Writer.Header().Add("X-Request-Id", "9012-5678-1234") 135 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 136 | }) 137 | 138 | // Listen and Server in 0.0.0.0:8080 139 | if err := r.Run(":8080"); err != nil { 140 | panic(err) 141 | } 142 | } 143 | ``` 144 | 145 | ## Custom `skipper` function 146 | 147 | Example for custom `skipper` function 148 | 149 | ```go 150 | r.Use(GinzapWithConfig(logger, &Config{ 151 | TimeFormat: time.RFC3339, 152 | UTC: true, 153 | Skipper: func(c *gin.Context) bool { 154 | return c.Request.URL.Path == "/ping" && c.Request.Method == "GET" 155 | }, 156 | })) 157 | ``` 158 | 159 | Full example 160 | 161 | ```go 162 | package main 163 | 164 | import ( 165 | "fmt" 166 | "time" 167 | 168 | ginzap "github.com/gin-contrib/zap" 169 | 170 | "github.com/gin-gonic/gin" 171 | "go.uber.org/zap" 172 | ) 173 | 174 | func main() { 175 | r := gin.New() 176 | 177 | logger, _ := zap.NewProduction() 178 | 179 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 180 | UTC: true, 181 | TimeFormat: time.RFC3339, 182 | Skipper: func(c *gin.Context) bool { 183 | return c.Request.URL.Path == "/ping" && c.Request.Method == "GET" 184 | }, 185 | })) 186 | 187 | // Example ping request. 188 | r.GET("/ping", func(c *gin.Context) { 189 | c.Writer.Header().Add("X-Request-Id", "1234-5678-9012") 190 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 191 | }) 192 | 193 | r.POST("/ping", func(c *gin.Context) { 194 | c.Writer.Header().Add("X-Request-Id", "9012-5678-1234") 195 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 196 | }) 197 | 198 | // Listen and Server in 0.0.0.0:8080 199 | if err := r.Run(":8080"); err != nil { 200 | panic(err) 201 | } 202 | } 203 | ``` 204 | 205 | ## Custom `SkipPathRegexps` function 206 | 207 | Example for custom `SkipPathRegexps` function 208 | 209 | ```go 210 | rxURL := regexp.MustCompile(`^/ping\s*`) 211 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 212 | UTC: true, 213 | TimeFormat: time.RFC3339, 214 | SkipPathRegexps: []*regexp.Regexp{rxURL}, 215 | })) 216 | ``` 217 | 218 | Full example 219 | 220 | ```go 221 | package main 222 | 223 | import ( 224 | "fmt" 225 | "regexp" 226 | "time" 227 | 228 | ginzap "github.com/gin-contrib/zap" 229 | 230 | "github.com/gin-gonic/gin" 231 | "go.uber.org/zap" 232 | ) 233 | 234 | func main() { 235 | r := gin.New() 236 | 237 | logger, _ := zap.NewProduction() 238 | rxURL := regexp.MustCompile(`^/ping\s*`) 239 | 240 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 241 | UTC: true, 242 | TimeFormat: time.RFC3339, 243 | SkipPathRegexps: []*regexp.Regexp{rxURL}, 244 | })) 245 | 246 | // Example ping request. 247 | r.GET("/ping1234", func(c *gin.Context) { 248 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 249 | }) 250 | 251 | // Listen and Server in 0.0.0.0:8080 252 | if err := r.Run(":8080"); err != nil { 253 | panic(err) 254 | } 255 | } 256 | ``` 257 | -------------------------------------------------------------------------------- /_example/example01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | ginzap "github.com/gin-contrib/zap" 8 | "github.com/gin-gonic/gin" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func main() { 13 | r := gin.New() 14 | 15 | logger, _ := zap.NewProduction() 16 | 17 | // Add a ginzap middleware, which: 18 | // - Logs all requests, like a combined access and error log. 19 | // - Logs to stdout. 20 | // - RFC3339 with UTC time format. 21 | r.Use(ginzap.Ginzap(logger, time.RFC3339, true)) 22 | 23 | // Logs all panic to error log 24 | // - stack means whether output the stack info. 25 | r.Use(ginzap.RecoveryWithZap(logger, true)) 26 | 27 | // Example ping request. 28 | r.GET("/ping", func(c *gin.Context) { 29 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 30 | }) 31 | 32 | // Example when panic happen. 33 | r.GET("/panic", func(c *gin.Context) { 34 | panic("An unexpected error happen!") 35 | }) 36 | 37 | // Listen and Server in 0.0.0.0:8080 38 | if err := r.Run(":8080"); err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /_example/example02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | ginzap "github.com/gin-contrib/zap" 10 | 11 | "github.com/gin-gonic/gin" 12 | "go.opentelemetry.io/otel/trace" 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zapcore" 15 | ) 16 | 17 | func main() { 18 | r := gin.New() 19 | 20 | logger, _ := zap.NewProduction() 21 | 22 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 23 | UTC: true, 24 | TimeFormat: time.RFC3339, 25 | Context: ginzap.Fn(func(c *gin.Context) []zapcore.Field { 26 | fields := []zapcore.Field{} 27 | // log request ID 28 | if requestID := c.Writer.Header().Get("X-Request-Id"); requestID != "" { 29 | fields = append(fields, zap.String("request_id", requestID)) 30 | } 31 | 32 | // log trace and span ID 33 | if trace.SpanFromContext(c.Request.Context()).SpanContext().IsValid() { 34 | fields = append(fields, zap.String("trace_id", trace.SpanFromContext(c.Request.Context()).SpanContext().TraceID().String())) 35 | fields = append(fields, zap.String("span_id", trace.SpanFromContext(c.Request.Context()).SpanContext().SpanID().String())) 36 | } 37 | 38 | // log request body 39 | var body []byte 40 | var buf bytes.Buffer 41 | tee := io.TeeReader(c.Request.Body, &buf) 42 | body, _ = io.ReadAll(tee) 43 | c.Request.Body = io.NopCloser(&buf) 44 | fields = append(fields, zap.String("body", string(body))) 45 | 46 | return fields 47 | }), 48 | })) 49 | 50 | // Example ping request. 51 | r.GET("/ping", func(c *gin.Context) { 52 | c.Writer.Header().Add("X-Request-Id", "1234-5678-9012") 53 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 54 | }) 55 | 56 | r.POST("/ping", func(c *gin.Context) { 57 | c.Writer.Header().Add("X-Request-Id", "9012-5678-1234") 58 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 59 | }) 60 | 61 | // Listen and Server in 0.0.0.0:8080 62 | if err := r.Run(":8080"); err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /_example/example03/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | ginzap "github.com/gin-contrib/zap" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func main() { 14 | r := gin.New() 15 | 16 | logger, _ := zap.NewProduction() 17 | 18 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 19 | UTC: true, 20 | TimeFormat: time.RFC3339, 21 | Skipper: func(c *gin.Context) bool { 22 | return c.Request.URL.Path == "/ping" && c.Request.Method == "GET" 23 | }, 24 | })) 25 | 26 | // Example ping request. 27 | r.GET("/ping", func(c *gin.Context) { 28 | c.Writer.Header().Add("X-Request-Id", "1234-5678-9012") 29 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 30 | }) 31 | 32 | r.POST("/ping", func(c *gin.Context) { 33 | c.Writer.Header().Add("X-Request-Id", "9012-5678-1234") 34 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 35 | }) 36 | 37 | // Listen and Server in 0.0.0.0:8080 38 | if err := r.Run(":8080"); err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /_example/example04/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "time" 7 | 8 | ginzap "github.com/gin-contrib/zap" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | func main() { 15 | r := gin.New() 16 | 17 | logger, _ := zap.NewProduction() 18 | rxURL := regexp.MustCompile(`^/ping\s*`) 19 | 20 | r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{ 21 | UTC: true, 22 | TimeFormat: time.RFC3339, 23 | SkipPathRegexps: []*regexp.Regexp{rxURL}, 24 | })) 25 | 26 | // Example ping request. 27 | r.GET("/ping1234", func(c *gin.Context) { 28 | c.String(200, "pong "+fmt.Sprint(time.Now().Unix())) 29 | }) 30 | 31 | // Listen and Server in 0.0.0.0:8080 32 | if err := r.Run(":8080"); err != nil { 33 | panic(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gin-contrib/zap 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | go.uber.org/zap v1.27.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.2 // indirect 12 | github.com/bytedance/sonic/loader v0.2.4 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 15 | github.com/gin-contrib/sse v1.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.26.0 // indirect 19 | github.com/goccy/go-json v0.10.5 // indirect 20 | github.com/json-iterator/go v1.1.12 // indirect 21 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 22 | github.com/leodido/go-urn v1.4.0 // indirect 23 | github.com/mattn/go-isatty v0.0.20 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | github.com/ugorji/go/codec v1.2.12 // indirect 29 | go.uber.org/multierr v1.11.0 // indirect 30 | golang.org/x/arch v0.16.0 // indirect 31 | golang.org/x/crypto v0.37.0 // indirect 32 | golang.org/x/net v0.39.0 // indirect 33 | golang.org/x/sys v0.32.0 // indirect 34 | golang.org/x/text v0.24.0 // indirect 35 | google.golang.org/protobuf v1.36.6 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 2 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 5 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 13 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 14 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 15 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 25 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 26 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 27 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 28 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 34 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 35 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 36 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 47 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 59 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 60 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 61 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 62 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 63 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 64 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 65 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 66 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 67 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 68 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 69 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 70 | golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= 71 | golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 72 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 73 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 74 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 75 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 78 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 79 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 80 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 84 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 91 | -------------------------------------------------------------------------------- /zap.go: -------------------------------------------------------------------------------- 1 | // Package ginzap provides log handling using zap package. 2 | // Code structure based on ginrus package. 3 | package ginzap 4 | 5 | import ( 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | "os" 10 | "regexp" 11 | "runtime/debug" 12 | "strings" 13 | "time" 14 | 15 | "github.com/gin-gonic/gin" 16 | "go.uber.org/zap" 17 | "go.uber.org/zap/zapcore" 18 | ) 19 | 20 | // Fn is a function to get zap fields from gin.Context 21 | type Fn func(c *gin.Context) []zapcore.Field 22 | 23 | // Skipper is a function to skip logs based on provided Context 24 | type Skipper func(c *gin.Context) bool 25 | 26 | // ZapLogger is the minimal logger interface compatible with zap.Logger 27 | type ZapLogger interface { 28 | Info(msg string, fields ...zap.Field) 29 | Error(msg string, fields ...zap.Field) 30 | } 31 | 32 | // Config is config setting for Ginzap 33 | type Config struct { 34 | TimeFormat string 35 | UTC bool 36 | SkipPaths []string 37 | SkipPathRegexps []*regexp.Regexp 38 | Context Fn 39 | DefaultLevel zapcore.Level 40 | // skip is a Skipper that indicates which logs should not be written. 41 | // Optional. 42 | Skipper Skipper 43 | } 44 | 45 | // Ginzap returns a gin.HandlerFunc (middleware) that logs requests using uber-go/zap. 46 | // 47 | // Requests with errors are logged using zap.Error(). 48 | // Requests without errors are logged using zap.Info(). 49 | // 50 | // It receives: 51 | // 1. A time package format string (e.g. time.RFC3339). 52 | // 2. A boolean stating whether to use UTC time zone or local. 53 | func Ginzap(logger ZapLogger, timeFormat string, utc bool) gin.HandlerFunc { 54 | return GinzapWithConfig(logger, &Config{TimeFormat: timeFormat, UTC: utc, DefaultLevel: zapcore.InfoLevel}) 55 | } 56 | 57 | // GinzapWithConfig returns a gin.HandlerFunc (middleware) that logs requests using uber-go/zap. 58 | // 59 | // Requests with errors are logged using zap.Error(). 60 | // Requests without errors are logged using zap.Info(). 61 | // 62 | // It receives a Config struct and a ZapLogger. 63 | // The Config struct allows you to configure the logging format, the time format, and the UTC time zone. 64 | // The ZapLogger is the minimal logger interface compatible with zap.Logger. 65 | func GinzapWithConfig(logger ZapLogger, conf *Config) gin.HandlerFunc { 66 | skipPaths := make(map[string]bool, len(conf.SkipPaths)) 67 | for _, path := range conf.SkipPaths { 68 | skipPaths[path] = true 69 | } 70 | 71 | return func(c *gin.Context) { 72 | start := time.Now() 73 | // some evil middlewares modify this values 74 | path := c.Request.URL.Path 75 | query := c.Request.URL.RawQuery 76 | c.Next() 77 | track := true 78 | 79 | if _, ok := skipPaths[path]; ok || (conf.Skipper != nil && conf.Skipper(c)) { 80 | track = false 81 | } 82 | 83 | if track && len(conf.SkipPathRegexps) > 0 { 84 | for _, reg := range conf.SkipPathRegexps { 85 | if !reg.MatchString(path) { 86 | continue 87 | } 88 | 89 | track = false 90 | break 91 | } 92 | } 93 | 94 | if track { 95 | end := time.Now() 96 | latency := end.Sub(start) 97 | if conf.UTC { 98 | end = end.UTC() 99 | } 100 | 101 | fields := []zapcore.Field{ 102 | zap.Int("status", c.Writer.Status()), 103 | zap.String("method", c.Request.Method), 104 | zap.String("path", path), 105 | zap.String("query", query), 106 | zap.String("ip", c.ClientIP()), 107 | zap.String("user-agent", c.Request.UserAgent()), 108 | zap.Duration("latency", latency), 109 | } 110 | if conf.TimeFormat != "" { 111 | fields = append(fields, zap.String("time", end.Format(conf.TimeFormat))) 112 | } 113 | 114 | if conf.Context != nil { 115 | fields = append(fields, conf.Context(c)...) 116 | } 117 | 118 | if len(c.Errors) > 0 { 119 | // Append error field if this is an erroneous request. 120 | for _, e := range c.Errors.Errors() { 121 | logger.Error(e, fields...) 122 | } 123 | } else { 124 | if zl, ok := logger.(*zap.Logger); ok { 125 | zl.Log(conf.DefaultLevel, path, fields...) 126 | } else if conf.DefaultLevel == zapcore.InfoLevel { 127 | logger.Info(path, fields...) 128 | } else { 129 | logger.Error(path, fields...) 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | func defaultHandleRecovery(c *gin.Context, err interface{}) { 137 | c.AbortWithStatus(http.StatusInternalServerError) 138 | } 139 | 140 | // RecoveryWithZap returns a gin.HandlerFunc (middleware) 141 | // that recovers from any panics and logs requests using uber-go/zap. 142 | // All errors are logged using zap.Error(). 143 | // stack means whether output the stack info. 144 | // The stack info is easy to find where the error occurs but the stack info is too large. 145 | func RecoveryWithZap(logger ZapLogger, stack bool) gin.HandlerFunc { 146 | return CustomRecoveryWithZap(logger, stack, defaultHandleRecovery) 147 | } 148 | 149 | // CustomRecoveryWithZap returns a gin.HandlerFunc (middleware) with a custom recovery handler 150 | // that recovers from any panics and logs requests using uber-go/zap. 151 | // All errors are logged using zap.Error(). 152 | // stack means whether output the stack info. 153 | // The stack info is easy to find where the error occurs but the stack info is too large. 154 | func CustomRecoveryWithZap(logger ZapLogger, stack bool, recovery gin.RecoveryFunc) gin.HandlerFunc { 155 | return func(c *gin.Context) { 156 | defer func() { 157 | if err := recover(); err != nil { 158 | // Check for a broken connection, as it is not really a 159 | // condition that warrants a panic stack trace. 160 | var brokenPipe bool 161 | if ne, ok := err.(*net.OpError); ok { 162 | if se, ok := ne.Err.(*os.SyscallError); ok { 163 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || 164 | strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { 165 | brokenPipe = true 166 | } 167 | } 168 | } 169 | 170 | httpRequest, _ := httputil.DumpRequest(c.Request, false) 171 | if brokenPipe { 172 | logger.Error(c.Request.URL.Path, 173 | zap.Any("error", err), 174 | zap.String("request", string(httpRequest)), 175 | ) 176 | // If the connection is dead, we can't write a status to it. 177 | c.Error(err.(error)) //nolint: errcheck 178 | c.Abort() 179 | return 180 | } 181 | 182 | if stack { 183 | logger.Error("[Recovery from panic]", 184 | zap.Time("time", time.Now()), 185 | zap.Any("error", err), 186 | zap.String("request", string(httpRequest)), 187 | zap.String("stack", string(debug.Stack())), 188 | ) 189 | } else { 190 | logger.Error("[Recovery from panic]", 191 | zap.Time("time", time.Now()), 192 | zap.Any("error", err), 193 | zap.String("request", string(httpRequest)), 194 | ) 195 | } 196 | recovery(c, err) 197 | } 198 | }() 199 | c.Next() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /zap_test.go: -------------------------------------------------------------------------------- 1 | package ginzap 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "regexp" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | 14 | "go.uber.org/zap" 15 | "go.uber.org/zap/zapcore" 16 | "go.uber.org/zap/zaptest/observer" 17 | ) 18 | 19 | const testPath = "/test" 20 | 21 | func buildDummyLogger() (*zap.Logger, *observer.ObservedLogs) { 22 | core, obs := observer.New(zap.InfoLevel) 23 | logger := zap.New(core) 24 | return logger, obs 25 | } 26 | 27 | func timestampLocationCheck(timestampStr string, location *time.Location) error { 28 | timestamp, err := time.Parse(time.RFC3339, timestampStr) 29 | if err != nil { 30 | return err 31 | } 32 | if timestamp.Location() != location { 33 | return fmt.Errorf("timestamp should be utc but %v", timestamp.Location()) 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func TestGinzap(t *testing.T) { 40 | ctx, cancel := context.WithCancel(context.Background()) 41 | defer cancel() 42 | r := gin.New() 43 | 44 | utcLogger, utcLoggerObserved := buildDummyLogger() 45 | r.Use(Ginzap(utcLogger, time.RFC3339, true)) 46 | 47 | localLogger, localLoggerObserved := buildDummyLogger() 48 | r.Use(Ginzap(localLogger, time.RFC3339, false)) 49 | 50 | r.GET(testPath, func(c *gin.Context) { 51 | c.JSON(204, nil) 52 | }) 53 | 54 | res1 := httptest.NewRecorder() 55 | req1, _ := http.NewRequestWithContext(ctx, "GET", testPath, nil) 56 | r.ServeHTTP(res1, req1) 57 | 58 | if len(utcLoggerObserved.All()) != 1 { 59 | t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All())) 60 | } 61 | 62 | logLine := utcLoggerObserved.All()[0] 63 | pathStr := logLine.Context[2].String 64 | if pathStr != testPath { 65 | t.Fatalf("logged path should be /test but %s", pathStr) 66 | } 67 | 68 | err := timestampLocationCheck(logLine.Context[7].String, time.UTC) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | if len(localLoggerObserved.All()) != 1 { 74 | t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All())) 75 | } 76 | 77 | logLine = localLoggerObserved.All()[0] 78 | pathStr = logLine.Context[2].String 79 | if pathStr != testPath { 80 | t.Fatalf("logged path should be /test but %s", pathStr) 81 | } 82 | } 83 | 84 | func TestGinzapWithConfig(t *testing.T) { 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | defer cancel() 87 | 88 | r := gin.New() 89 | 90 | utcLogger, utcLoggerObserved := buildDummyLogger() 91 | r.Use(GinzapWithConfig(utcLogger, &Config{ 92 | TimeFormat: time.RFC3339, 93 | UTC: true, 94 | SkipPaths: []string{"/no_log"}, 95 | DefaultLevel: zapcore.WarnLevel, 96 | })) 97 | 98 | r.GET(testPath, func(c *gin.Context) { 99 | c.JSON(204, nil) 100 | }) 101 | 102 | r.GET("/no_log", func(c *gin.Context) { 103 | c.JSON(204, nil) 104 | }) 105 | 106 | res1 := httptest.NewRecorder() 107 | req1, _ := http.NewRequestWithContext(ctx, "GET", testPath, nil) 108 | r.ServeHTTP(res1, req1) 109 | 110 | res2 := httptest.NewRecorder() 111 | req2, _ := http.NewRequestWithContext(ctx, "GET", "/no_log", nil) 112 | r.ServeHTTP(res2, req2) 113 | 114 | if res2.Code != 204 { 115 | t.Fatalf("request /no_log is failed (%d)", res2.Code) 116 | } 117 | 118 | if len(utcLoggerObserved.All()) != 1 { 119 | t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All())) 120 | } 121 | 122 | logLine := utcLoggerObserved.All()[0] 123 | pathStr := logLine.Context[2].String 124 | if pathStr != testPath { 125 | t.Fatalf("logged path should be /test but %s", pathStr) 126 | } 127 | 128 | err := timestampLocationCheck(logLine.Context[7].String, time.UTC) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | if logLine.Level != zapcore.WarnLevel { 134 | t.Fatalf("log level should be warn but was %s", logLine.Level.String()) 135 | } 136 | } 137 | 138 | func TestLoggerSkipper(t *testing.T) { 139 | ctx, cancel := context.WithCancel(context.Background()) 140 | defer cancel() 141 | r := gin.New() 142 | 143 | utcLogger, utcLoggerObserved := buildDummyLogger() 144 | r.Use(GinzapWithConfig(utcLogger, &Config{ 145 | TimeFormat: time.RFC3339, 146 | UTC: true, 147 | Skipper: func(c *gin.Context) bool { 148 | return c.Request.URL.Path == "/no_log" 149 | }, 150 | })) 151 | 152 | r.GET(testPath, func(c *gin.Context) { 153 | c.JSON(204, nil) 154 | }) 155 | 156 | r.GET("/no_log", func(c *gin.Context) { 157 | c.JSON(204, nil) 158 | }) 159 | 160 | res1 := httptest.NewRecorder() 161 | req1, _ := http.NewRequestWithContext(ctx, "GET", testPath, nil) 162 | r.ServeHTTP(res1, req1) 163 | 164 | res2 := httptest.NewRecorder() 165 | req2, _ := http.NewRequestWithContext(ctx, "GET", "/no_log", nil) 166 | r.ServeHTTP(res2, req2) 167 | 168 | if res2.Code != 204 { 169 | t.Fatalf("request /no_log is failed (%d)", res2.Code) 170 | } 171 | 172 | if len(utcLoggerObserved.All()) != 1 { 173 | t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All())) 174 | } 175 | 176 | logLine := utcLoggerObserved.All()[0] 177 | pathStr := logLine.Context[2].String 178 | if pathStr != testPath { 179 | t.Fatalf("logged path should be /test but %s", pathStr) 180 | } 181 | } 182 | 183 | func TestSkipPathRegexps(t *testing.T) { 184 | ctx, cancel := context.WithCancel(context.Background()) 185 | defer cancel() 186 | r := gin.New() 187 | 188 | rxURL := regexp.MustCompile(`^/no_\s*`) 189 | 190 | utcLogger, utcLoggerObserved := buildDummyLogger() 191 | r.Use(GinzapWithConfig(utcLogger, &Config{ 192 | TimeFormat: time.RFC3339, 193 | UTC: true, 194 | SkipPathRegexps: []*regexp.Regexp{rxURL}, 195 | })) 196 | 197 | r.GET(testPath, func(c *gin.Context) { 198 | c.JSON(204, nil) 199 | }) 200 | 201 | r.GET("/no_log", func(c *gin.Context) { 202 | c.JSON(204, nil) 203 | }) 204 | 205 | res1 := httptest.NewRecorder() 206 | req1, _ := http.NewRequestWithContext(ctx, "GET", testPath, nil) 207 | r.ServeHTTP(res1, req1) 208 | 209 | res2 := httptest.NewRecorder() 210 | req2, _ := http.NewRequestWithContext(ctx, "GET", "/no_log", nil) 211 | r.ServeHTTP(res2, req2) 212 | 213 | if res2.Code != 204 { 214 | t.Fatalf("request /no_log is failed (%d)", res2.Code) 215 | } 216 | 217 | if len(utcLoggerObserved.All()) != 1 { 218 | t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All())) 219 | } 220 | 221 | logLine := utcLoggerObserved.All()[0] 222 | pathStr := logLine.Context[2].String 223 | if pathStr != testPath { 224 | t.Fatalf("logged path should be /test but %s", pathStr) 225 | } 226 | } 227 | --------------------------------------------------------------------------------