├── .github └── workflows │ ├── codeql-analysis.yml │ └── main.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── context.go ├── context_test.go ├── go.mod ├── go.sum ├── levels.go ├── logger.go ├── logger_test.go ├── middleware.go ├── middleware_test.go ├── options.go └── options_test.go /.github/workflows/codeql-analysis.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, v1 ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '16 4 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | goVer: [1.21, 1.22, 1.23, 1.24] 12 | steps: 13 | - name: Set up Go ${{ matrix.goVer }} 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: ${{ matrix.goVer }} 17 | id: go 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v4 21 | 22 | - name: Get dependencies 23 | run: go get 24 | 25 | - name: Run tests 26 | run: go test ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea/ 15 | vendor/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tim Voronov 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 | default: install build 2 | 3 | add: 4 | go get -u -v ${PKG} 5 | 6 | install-tools: 7 | go install honnef.co/go/tools/cmd/staticcheck@latest && \ 8 | go install golang.org/x/tools/cmd/goimports@latest 9 | 10 | 11 | install-deps: 12 | go mod tidy 13 | 14 | install: install-tools install-deps 15 | 16 | build: lint test 17 | 18 | test: 19 | go test -race ./... 20 | 21 | lint: 22 | go vet ./... && \ 23 | staticcheck -tests=false ./... 24 | 25 | fmt: 26 | go fmt ./... && \ 27 | goimports -w . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lecho :tomato: 2 | 3 | [Zerolog](https://github.com/rs/zerolog) wrapper for [Echo](https://echo.labstack.com/) web framework. 4 | 5 | ## Installation 6 | 7 | For Echo v4: 8 | 9 | ``` 10 | go get github.com/ziflex/lecho/v3 11 | ``` 12 | 13 | For Echo v3: 14 | 15 | ``` 16 | go get github.com/ziflex/lecho 17 | ``` 18 | 19 | ## Quick start 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "os" 26 | "github.com/labstack/echo/v4" 27 | "github.com/labstack/echo/v4/middleware" 28 | "github.com/ziflex/lecho/v3" 29 | ) 30 | 31 | func main() { 32 | e := echo.New() 33 | e.Logger = lecho.New(os.Stdout) 34 | } 35 | ``` 36 | 37 | ### Using existing zerolog instance 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "os" 44 | "github.com/labstack/echo/v4" 45 | "github.com/labstack/echo/v4/middleware" 46 | "github.com/ziflex/lecho/v3" 47 | "github.com/rs/zerolog" 48 | ) 49 | 50 | func main() { 51 | log := zerolog.New(os.Stdout) 52 | e := echo.New() 53 | e.Logger = lecho.From(log) 54 | } 55 | 56 | ``` 57 | 58 | ## Options 59 | 60 | ```go 61 | 62 | import ( 63 | "os", 64 | "github.com/labstack/echo" 65 | "github.com/labstack/echo/middleware" 66 | "github.com/ziflex/lecho/v3" 67 | ) 68 | 69 | func main() { 70 | e := echo.New() 71 | e.Logger = lecho.New( 72 | os.Stdout, 73 | lecho.WithLevel(log.DEBUG), 74 | lecho.WithFields(map[string]interface{}{ "name": "lecho factory"}), 75 | lecho.WithTimestamp(), 76 | lecho.WithCaller(), 77 | lecho.WithPrefix("we ❤️ lecho"), 78 | lecho.WithHook(...), 79 | lecho.WithHookFunc(...), 80 | ) 81 | } 82 | ``` 83 | 84 | ## Middleware 85 | 86 | ### Logging requests and attaching request id to a context logger 87 | 88 | ```go 89 | 90 | import ( 91 | "os", 92 | "github.com/labstack/echo" 93 | "github.com/labstack/echo/middleware" 94 | "github.com/ziflex/lecho/v3" 95 | "github.com/rs/zerolog" 96 | ) 97 | 98 | func main() { 99 | e := echo.New() 100 | logger := lecho.New( 101 | os.Stdout, 102 | lecho.WithLevel(log.DEBUG), 103 | lecho.WithTimestamp(), 104 | lecho.WithCaller(), 105 | ) 106 | e.Logger = logger 107 | 108 | e.Use(middleware.RequestID()) 109 | e.Use(lecho.Middleware(lecho.Config{ 110 | Logger: logger 111 | })) 112 | e.GET("/", func(c echo.Context) error { 113 | c.Logger().Print("Echo interface") 114 | zerolog.Ctx(c.Request().Context()).Print("Zerolog interface") 115 | 116 | return c.String(http.StatusOK, "Hello, World!") 117 | }) 118 | } 119 | 120 | ``` 121 | 122 | ### Escalate log level for slow requests: 123 | ```go 124 | e.Use(lecho.Middleware(lecho.Config{ 125 | Logger: logger, 126 | RequestLatencyLevel: zerolog.WarnLevel, 127 | RequestLatencyLimit: 500 * time.Millisecond, 128 | })) 129 | ``` 130 | 131 | 132 | ### Nesting under a sub dictionary 133 | 134 | ```go 135 | e.Use(lecho.Middleware(lecho.Config{ 136 | Logger: logger, 137 | NestKey: "request" 138 | })) 139 | // Output: {"level":"info","request":{"remote_ip":"5.6.7.8","method":"GET", ...}, ...} 140 | ``` 141 | 142 | ### Enricher 143 | 144 | Enricher allows you to add additional fields to the log entry. 145 | 146 | ```go 147 | e.Use(lecho.Middleware(lecho.Config{ 148 | Logger: logger, 149 | Enricher: func(c echo.Context, logger zerolog.Context) zerolog.Context { 150 | return e.Str("user_id", c.Get("user_id")) 151 | }, 152 | })) 153 | // Output: {"level":"info","user_id":"123", ...} 154 | ``` 155 | 156 | ### Errors 157 | Since lecho v3.4.0, the middleware does not automatically propagate errors up the chain. 158 | If you want to do that, you can set `HandleError` to ``true``. 159 | 160 | ```go 161 | e.Use(lecho.Middleware(lecho.Config{ 162 | Logger: logger, 163 | HandleError: true, 164 | })) 165 | ``` 166 | 167 | ## Helpers 168 | 169 | ### Level converters 170 | 171 | ```go 172 | 173 | import ( 174 | "fmt", 175 | "github.com/labstack/echo" 176 | "github.com/labstack/echo/middleware" 177 | "github.com/labstack/gommon/log" 178 | "github.com/ziflex/lecho/v3" 179 | ) 180 | 181 | func main() { 182 | var z zerolog.Level 183 | var e log.Lvl 184 | 185 | z, e = lecho.MatchEchoLevel(log.WARN) 186 | 187 | fmt.Println(z, e) 188 | 189 | e, z = lecho.MatchZeroLevel(zerolog.INFO) 190 | 191 | fmt.Println(z, e) 192 | } 193 | 194 | ``` 195 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package lecho 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | // WithContext returns a new context with the provided logger. 10 | func (l Logger) WithContext(ctx context.Context) context.Context { 11 | zerologger := l.Unwrap() 12 | return zerologger.WithContext(ctx) 13 | } 14 | 15 | // Ctx returns a logger from the provided context. 16 | // If no logger is found in the context, a new one is created. 17 | func Ctx(ctx context.Context) *zerolog.Logger { 18 | return zerolog.Ctx(ctx) 19 | } 20 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package lecho_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/ziflex/lecho/v3" 11 | ) 12 | 13 | func TestCtx(t *testing.T) { 14 | b := &bytes.Buffer{} 15 | l := lecho.New(b) 16 | zerologger := l.Unwrap() 17 | ctx := l.WithContext(context.Background()) 18 | 19 | assert.Equal(t, lecho.Ctx(ctx), &zerologger) 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ziflex/lecho/v3 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/labstack/echo/v4 v4.13.3 7 | github.com/labstack/gommon v0.4.2 8 | github.com/rs/zerolog v1.34.0 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/valyala/bytebufferpool v1.0.0 // indirect 18 | github.com/valyala/fasttemplate v1.2.2 // indirect 19 | golang.org/x/crypto v0.31.0 // indirect 20 | golang.org/x/net v0.33.0 // indirect 21 | golang.org/x/sys v0.28.0 // indirect 22 | golang.org/x/text v0.21.0 // indirect 23 | golang.org/x/time v0.8.0 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 5 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 6 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 7 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 8 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 9 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 10 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 11 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 12 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 13 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 14 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 19 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 20 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 21 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 22 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 24 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 25 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 26 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 27 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 28 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 29 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 30 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 31 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 35 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 37 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 38 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 39 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /levels.go: -------------------------------------------------------------------------------- 1 | package lecho 2 | 3 | import ( 4 | "github.com/labstack/gommon/log" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | var ( 9 | echoLevels = map[log.Lvl]zerolog.Level{ 10 | log.DEBUG: zerolog.DebugLevel, 11 | log.INFO: zerolog.InfoLevel, 12 | log.WARN: zerolog.WarnLevel, 13 | log.ERROR: zerolog.ErrorLevel, 14 | log.OFF: zerolog.NoLevel, 15 | } 16 | 17 | zeroLevels = map[zerolog.Level]log.Lvl{ 18 | zerolog.TraceLevel: log.DEBUG, 19 | zerolog.DebugLevel: log.DEBUG, 20 | zerolog.InfoLevel: log.INFO, 21 | zerolog.WarnLevel: log.WARN, 22 | zerolog.ErrorLevel: log.ERROR, 23 | zerolog.NoLevel: log.OFF, 24 | } 25 | ) 26 | 27 | // MatchEchoLevel returns a zerolog level and echo level for a given echo level 28 | func MatchEchoLevel(level log.Lvl) (zerolog.Level, log.Lvl) { 29 | zlvl, found := echoLevels[level] 30 | 31 | if found { 32 | return zlvl, level 33 | } 34 | 35 | return zerolog.NoLevel, log.OFF 36 | } 37 | 38 | // MatchZeroLevel returns an echo level and zerolog level for a given zerolog level 39 | func MatchZeroLevel(level zerolog.Level) (log.Lvl, zerolog.Level) { 40 | elvl, found := zeroLevels[level] 41 | 42 | if found { 43 | return elvl, level 44 | } 45 | 46 | return log.OFF, zerolog.NoLevel 47 | } 48 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package lecho 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/labstack/gommon/log" 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | // Logger is a wrapper around `zerolog.Logger` that provides an implementation of `echo.Logger` interface 12 | type Logger struct { 13 | log zerolog.Logger 14 | out io.Writer 15 | level log.Lvl 16 | prefix string 17 | setters []Setter 18 | } 19 | 20 | // New returns a new Logger instance 21 | func New(out io.Writer, setters ...Setter) *Logger { 22 | switch l := out.(type) { 23 | case zerolog.Logger: 24 | return newLogger(l, setters) 25 | default: 26 | return newLogger(zerolog.New(out), setters) 27 | } 28 | } 29 | 30 | // From returns a new Logger instance using existing zerolog log. 31 | func From(log zerolog.Logger, setters ...Setter) *Logger { 32 | return newLogger(log, setters) 33 | } 34 | 35 | func newLogger(log zerolog.Logger, setters []Setter) *Logger { 36 | opts := newOptions(log, setters) 37 | 38 | return &Logger{ 39 | log: opts.context.Logger(), 40 | out: nil, 41 | level: opts.level, 42 | prefix: opts.prefix, 43 | setters: setters, 44 | } 45 | } 46 | 47 | func (l Logger) Debug(i ...interface{}) { 48 | l.log.Debug().Msg(fmt.Sprint(i...)) 49 | } 50 | 51 | func (l Logger) Debugf(format string, i ...interface{}) { 52 | l.log.Debug().Msgf(format, i...) 53 | } 54 | 55 | func (l Logger) Debugj(j log.JSON) { 56 | l.logJSON(l.log.Debug(), j) 57 | } 58 | 59 | func (l Logger) Info(i ...interface{}) { 60 | l.log.Info().Msg(fmt.Sprint(i...)) 61 | } 62 | 63 | func (l Logger) Infof(format string, i ...interface{}) { 64 | l.log.Info().Msgf(format, i...) 65 | } 66 | 67 | func (l Logger) Infoj(j log.JSON) { 68 | l.logJSON(l.log.Info(), j) 69 | } 70 | 71 | func (l Logger) Warn(i ...interface{}) { 72 | l.log.Warn().Msg(fmt.Sprint(i...)) 73 | } 74 | 75 | func (l Logger) Warnf(format string, i ...interface{}) { 76 | l.log.Warn().Msgf(format, i...) 77 | } 78 | 79 | func (l Logger) Warnj(j log.JSON) { 80 | l.logJSON(l.log.Warn(), j) 81 | } 82 | 83 | func (l Logger) Error(i ...interface{}) { 84 | l.log.Error().Msg(fmt.Sprint(i...)) 85 | } 86 | 87 | func (l Logger) Errorf(format string, i ...interface{}) { 88 | l.log.Error().Msgf(format, i...) 89 | } 90 | 91 | func (l Logger) Errorj(j log.JSON) { 92 | l.logJSON(l.log.Error(), j) 93 | } 94 | 95 | func (l Logger) Fatal(i ...interface{}) { 96 | l.log.Fatal().Msg(fmt.Sprint(i...)) 97 | } 98 | 99 | func (l Logger) Fatalf(format string, i ...interface{}) { 100 | l.log.Fatal().Msgf(format, i...) 101 | } 102 | 103 | func (l Logger) Fatalj(j log.JSON) { 104 | l.logJSON(l.log.Fatal(), j) 105 | } 106 | 107 | func (l Logger) Panic(i ...interface{}) { 108 | l.log.Panic().Msg(fmt.Sprint(i...)) 109 | } 110 | 111 | func (l Logger) Panicf(format string, i ...interface{}) { 112 | l.log.Panic().Msgf(format, i...) 113 | } 114 | 115 | func (l Logger) Panicj(j log.JSON) { 116 | l.logJSON(l.log.Panic(), j) 117 | } 118 | 119 | func (l Logger) Print(i ...interface{}) { 120 | l.log.WithLevel(zerolog.NoLevel).Str("level", "-").Msg(fmt.Sprint(i...)) 121 | } 122 | 123 | func (l Logger) Printf(format string, i ...interface{}) { 124 | l.log.WithLevel(zerolog.NoLevel).Str("level", "-").Msgf(format, i...) 125 | } 126 | 127 | func (l Logger) Printj(j log.JSON) { 128 | l.logJSON(l.log.WithLevel(zerolog.NoLevel).Str("level", "-"), j) 129 | } 130 | 131 | func (l Logger) Output() io.Writer { 132 | return l.log 133 | } 134 | 135 | func (l *Logger) SetOutput(newOut io.Writer) { 136 | l.out = newOut 137 | l.log = l.log.Output(newOut) 138 | } 139 | 140 | func (l Logger) Level() log.Lvl { 141 | return l.level 142 | } 143 | 144 | func (l *Logger) SetLevel(level log.Lvl) { 145 | zlvl, elvl := MatchEchoLevel(level) 146 | 147 | l.setters = append(l.setters, WithLevel(elvl)) 148 | l.level = elvl 149 | l.log = l.log.Level(zlvl) 150 | } 151 | 152 | func (l Logger) Prefix() string { 153 | return l.prefix 154 | } 155 | 156 | func (l Logger) SetHeader(h string) { 157 | // not implemented 158 | } 159 | 160 | func (l *Logger) SetPrefix(newPrefix string) { 161 | l.setters = append(l.setters, WithPrefix(newPrefix)) 162 | 163 | opts := newOptions(l.log, l.setters) 164 | 165 | l.prefix = newPrefix 166 | l.log = opts.context.Logger() 167 | } 168 | 169 | func (l *Logger) Unwrap() zerolog.Logger { 170 | return l.log 171 | } 172 | 173 | func (l *Logger) logJSON(event *zerolog.Event, j log.JSON) { 174 | for k, v := range j { 175 | event = event.Interface(k, v) 176 | } 177 | 178 | event.Msg("") 179 | } 180 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package lecho_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/labstack/gommon/log" 9 | "github.com/rs/zerolog" 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/ziflex/lecho/v3" 13 | ) 14 | 15 | func TestNew(t *testing.T) { 16 | b := &bytes.Buffer{} 17 | 18 | l := lecho.New(b) 19 | 20 | l.Print("foo") 21 | 22 | assert.Equal( 23 | t, 24 | `{"level":"-","message":"foo"} 25 | `, 26 | b.String(), 27 | ) 28 | } 29 | 30 | func TestNewWithZerolog(t *testing.T) { 31 | b := &bytes.Buffer{} 32 | zl := zerolog.New(b) 33 | 34 | l := lecho.New(zl.With().Str("key", "test").Logger()) 35 | 36 | l.Print("foo") 37 | 38 | assert.Equal( 39 | t, 40 | `{"key":"test","level":"-","message":"foo"} 41 | `, 42 | b.String(), 43 | ) 44 | } 45 | 46 | func TestFrom(t *testing.T) { 47 | b := &bytes.Buffer{} 48 | 49 | zl := zerolog.New(b) 50 | l := lecho.From(zl.With().Str("key", "test").Logger()) 51 | 52 | l.Print("foo") 53 | 54 | assert.Equal( 55 | t, 56 | `{"key":"test","level":"-","message":"foo"} 57 | `, 58 | b.String(), 59 | ) 60 | } 61 | 62 | func TestLogger_SetPrefix(t *testing.T) { 63 | // b := &bytes.Buffer{} 64 | // 65 | // l := lecho.New(b) 66 | // 67 | // l.Print("t-e-s-t") 68 | // 69 | // assert.Equal( 70 | // t, 71 | // `{"level":"-","message":"t-e-s-t"} 72 | //`, 73 | // b.String(), 74 | // ) 75 | // 76 | // b.Reset() 77 | // 78 | // l.SetPrefix("foo") 79 | // l.Print("test") 80 | // 81 | // assert.Equal( 82 | // t, 83 | // `{"prefix":"foo","level":"-","message":"test"} 84 | //`, 85 | // b.String(), 86 | // ) 87 | // 88 | // b.Reset() 89 | // 90 | // l.SetPrefix("bar") 91 | // l.Print("test-test") 92 | // 93 | // assert.Equal( 94 | // t, 95 | // `{"prefix":"bar","level":"-","message":"test-test"} 96 | //`, 97 | // b.String(), 98 | // ) 99 | } 100 | 101 | func TestLogger_Output(t *testing.T) { 102 | out1 := &bytes.Buffer{} 103 | 104 | l := lecho.New(out1) 105 | 106 | l.Print("foo") 107 | l.Print("bar") 108 | 109 | out2 := &bytes.Buffer{} 110 | l.SetOutput(out2) 111 | 112 | l.Print("baz") 113 | 114 | assert.Equal( 115 | t, 116 | `{"level":"-","message":"foo"} 117 | {"level":"-","message":"bar"} 118 | `, 119 | out1.String(), 120 | ) 121 | 122 | assert.Equal( 123 | t, 124 | `{"level":"-","message":"baz"} 125 | `, 126 | out2.String(), 127 | ) 128 | } 129 | 130 | func TestLogger_SetLevel(t *testing.T) { 131 | b := &bytes.Buffer{} 132 | 133 | l := lecho.New(b) 134 | 135 | l.Debug("foo") 136 | 137 | assert.Equal( 138 | t, 139 | `{"level":"debug","message":"foo"} 140 | `, 141 | b.String(), 142 | ) 143 | 144 | b.Reset() 145 | 146 | l.SetLevel(log.WARN) 147 | 148 | l.Debug("foo") 149 | 150 | assert.Equal(t, "", b.String()) 151 | } 152 | 153 | func TestLogger(t *testing.T) { 154 | type ( 155 | SimpleLog struct { 156 | Level zerolog.Level 157 | Fn func(i ...interface{}) 158 | } 159 | 160 | FormattedLog struct { 161 | Level zerolog.Level 162 | Fn func(format string, i ...interface{}) 163 | } 164 | 165 | JSONLog struct { 166 | Level zerolog.Level 167 | Fn func(j log.JSON) 168 | } 169 | ) 170 | 171 | b := &bytes.Buffer{} 172 | l := lecho.New(b) 173 | 174 | simpleLogs := []SimpleLog{ 175 | { 176 | Level: zerolog.DebugLevel, 177 | Fn: l.Debug, 178 | }, 179 | { 180 | Level: zerolog.InfoLevel, 181 | Fn: l.Info, 182 | }, 183 | { 184 | Level: zerolog.WarnLevel, 185 | Fn: l.Warn, 186 | }, 187 | { 188 | Level: zerolog.ErrorLevel, 189 | Fn: l.Error, 190 | }, 191 | } 192 | 193 | for _, l := range simpleLogs { 194 | b.Reset() 195 | 196 | l.Fn("foobar") 197 | assert.Equal(t, fmt.Sprintf(`{"level":"%s","message":"foobar"} 198 | `, l.Level), 199 | b.String()) 200 | } 201 | 202 | formattedLogs := []FormattedLog{ 203 | { 204 | Level: zerolog.DebugLevel, 205 | Fn: l.Debugf, 206 | }, 207 | { 208 | Level: zerolog.InfoLevel, 209 | Fn: l.Infof, 210 | }, 211 | { 212 | Level: zerolog.WarnLevel, 213 | Fn: l.Warnf, 214 | }, 215 | { 216 | Level: zerolog.ErrorLevel, 217 | Fn: l.Errorf, 218 | }, 219 | } 220 | 221 | for _, l := range formattedLogs { 222 | b.Reset() 223 | 224 | l.Fn("foo%s", "bar") 225 | assert.Equal(t, fmt.Sprintf(`{"level":"%s","message":"foobar"} 226 | `, l.Level), 227 | b.String()) 228 | } 229 | 230 | jsonLogs := []JSONLog{ 231 | { 232 | Level: zerolog.DebugLevel, 233 | Fn: l.Debugj, 234 | }, 235 | { 236 | Level: zerolog.InfoLevel, 237 | Fn: l.Infoj, 238 | }, 239 | { 240 | Level: zerolog.WarnLevel, 241 | Fn: l.Warnj, 242 | }, 243 | { 244 | Level: zerolog.ErrorLevel, 245 | Fn: l.Errorj, 246 | }, 247 | } 248 | 249 | for _, l := range jsonLogs { 250 | b.Reset() 251 | 252 | l.Fn(log.JSON{ 253 | "message": "foobar", 254 | }) 255 | assert.Equal(t, fmt.Sprintf(`{"level":"%s","message":"foobar"} 256 | `, l.Level), 257 | b.String()) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package lecho 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/rs/zerolog" 10 | 11 | "github.com/labstack/echo/v4" 12 | "github.com/labstack/echo/v4/middleware" 13 | ) 14 | 15 | type ( 16 | // Config is the configuration for the middleware. 17 | Config struct { 18 | // Logger is a custom instance of the logger to use. 19 | Logger *Logger 20 | // Skipper defines a function to skip middleware. 21 | Skipper middleware.Skipper 22 | // AfterNextSkipper defines a function to skip middleware after the next handler is called. 23 | AfterNextSkipper middleware.Skipper 24 | // BeforeNext is a function that is executed before the next handler is called. 25 | BeforeNext middleware.BeforeFunc 26 | // Enricher is a function that can be used to enrich the logger with additional information. 27 | Enricher Enricher 28 | // RequestIDHeader is the header name to use for the request ID in a log record. 29 | RequestIDHeader string 30 | // RequestIDKey is the key name to use for the request ID in a log record. 31 | RequestIDKey string 32 | // NestKey is the key name to use for the nested logger in a log record. 33 | NestKey string 34 | // HandleError indicates whether to propagate errors up the middleware chain, so the global error handler can decide appropriate status code. 35 | HandleError bool 36 | // For long-running requests that take longer than this limit, log at a different level. Ignored by default 37 | RequestLatencyLimit time.Duration 38 | // The level to log at if RequestLatencyLimit is exceeded 39 | RequestLatencyLevel zerolog.Level 40 | } 41 | 42 | // Enricher is a function that can be used to enrich the logger with additional information. 43 | Enricher func(c echo.Context, logger zerolog.Context) zerolog.Context 44 | 45 | // Context is a wrapper around echo.Context that provides a logger. 46 | Context struct { 47 | echo.Context 48 | logger *Logger 49 | } 50 | ) 51 | 52 | // NewContext returns a new Context. 53 | func NewContext(ctx echo.Context, logger *Logger) *Context { 54 | return &Context{ctx, logger} 55 | } 56 | 57 | func (c *Context) Logger() echo.Logger { 58 | return c.logger 59 | } 60 | 61 | // Middleware returns a middleware which logs HTTP requests. 62 | func Middleware(config Config) echo.MiddlewareFunc { 63 | if config.Skipper == nil { 64 | config.Skipper = middleware.DefaultSkipper 65 | } 66 | 67 | if config.AfterNextSkipper == nil { 68 | config.AfterNextSkipper = middleware.DefaultSkipper 69 | } 70 | 71 | if config.Logger == nil { 72 | config.Logger = New(os.Stdout, WithTimestamp()) 73 | } 74 | 75 | if config.RequestIDKey == "" { 76 | config.RequestIDKey = "id" 77 | } 78 | 79 | if config.RequestIDHeader == "" { 80 | config.RequestIDHeader = echo.HeaderXRequestID 81 | } 82 | 83 | return func(next echo.HandlerFunc) echo.HandlerFunc { 84 | return func(c echo.Context) error { 85 | if config.Skipper(c) { 86 | return next(c) 87 | } 88 | 89 | var err error 90 | req := c.Request() 91 | res := c.Response() 92 | start := time.Now() 93 | 94 | id := req.Header.Get(config.RequestIDHeader) 95 | 96 | if id == "" { 97 | id = res.Header().Get(config.RequestIDHeader) 98 | } 99 | 100 | cloned := false 101 | logger := config.Logger 102 | 103 | if id != "" { 104 | logger = From(logger.log, WithField(config.RequestIDKey, id)) 105 | cloned = true 106 | } 107 | 108 | if config.Enricher != nil { 109 | // to avoid mutation of shared instance 110 | if !cloned { 111 | logger = From(logger.log) 112 | cloned = true 113 | } 114 | 115 | logger.log = config.Enricher(c, logger.log.With()).Logger() 116 | } 117 | 118 | ctx := req.Context() 119 | 120 | if ctx == nil { 121 | ctx = context.Background() 122 | } 123 | 124 | // Pass logger down to request context 125 | c.SetRequest(req.WithContext(logger.WithContext(ctx))) 126 | c = NewContext(c, logger) 127 | 128 | if config.BeforeNext != nil { 129 | config.BeforeNext(c) 130 | } 131 | 132 | if err = next(c); err != nil { 133 | if config.HandleError { 134 | c.Error(err) 135 | } 136 | } 137 | 138 | if config.AfterNextSkipper(c) { 139 | return err 140 | } 141 | 142 | stop := time.Now() 143 | latency := stop.Sub(start) 144 | var mainEvt *zerolog.Event 145 | if err != nil { 146 | mainEvt = logger.log.Err(err) 147 | } else if config.RequestLatencyLimit != 0 && latency > config.RequestLatencyLimit { 148 | mainEvt = logger.log.WithLevel(config.RequestLatencyLevel) 149 | } else { 150 | mainEvt = logger.log.WithLevel(logger.log.GetLevel()) 151 | } 152 | 153 | var evt *zerolog.Event 154 | if config.NestKey != "" { // Start a new event (dict) if there's a nest key. 155 | evt = zerolog.Dict() 156 | } else { 157 | evt = mainEvt 158 | } 159 | 160 | evt.Str("remote_ip", c.RealIP()) 161 | evt.Str("host", req.Host) 162 | evt.Str("method", req.Method) 163 | evt.Str("uri", req.RequestURI) 164 | evt.Str("user_agent", req.UserAgent()) 165 | evt.Int("status", res.Status) 166 | evt.Str("referer", req.Referer()) 167 | evt.Dur("latency", latency) 168 | evt.Str("latency_human", latency.String()) 169 | 170 | cl := req.Header.Get(echo.HeaderContentLength) 171 | if cl == "" { 172 | cl = "0" 173 | } 174 | 175 | evt.Str("bytes_in", cl) 176 | evt.Str("bytes_out", strconv.FormatInt(res.Size, 10)) 177 | 178 | if config.NestKey != "" { // Nest the new event (dict) under the nest key. 179 | mainEvt.Dict(config.NestKey, evt) 180 | } 181 | mainEvt.Send() 182 | 183 | return err 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package lecho_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | 11 | "github.com/labstack/echo/v4" 12 | "github.com/labstack/gommon/log" 13 | "github.com/rs/zerolog" 14 | "github.com/stretchr/testify/assert" 15 | 16 | "github.com/ziflex/lecho/v3" 17 | ) 18 | 19 | func TestMiddleware(t *testing.T) { 20 | t.Run("should not trigger error handler when HandleError is false", func(t *testing.T) { 21 | var called bool 22 | e := echo.New() 23 | e.HTTPErrorHandler = func(err error, c echo.Context) { 24 | called = true 25 | 26 | c.JSON(http.StatusInternalServerError, err.Error()) 27 | } 28 | req := httptest.NewRequest(http.MethodGet, "/", nil) 29 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 30 | rec := httptest.NewRecorder() 31 | c := e.NewContext(req, rec) 32 | 33 | m := lecho.Middleware(lecho.Config{}) 34 | 35 | next := func(c echo.Context) error { 36 | return errors.New("error") 37 | } 38 | 39 | handler := m(next) 40 | err := handler(c) 41 | 42 | assert.Error(t, err, "should return error") 43 | assert.False(t, called, "should not call error handler") 44 | }) 45 | 46 | t.Run("should trigger error handler when HandleError is true", func(t *testing.T) { 47 | var called bool 48 | e := echo.New() 49 | e.HTTPErrorHandler = func(err error, c echo.Context) { 50 | called = true 51 | 52 | c.JSON(http.StatusInternalServerError, err.Error()) 53 | } 54 | req := httptest.NewRequest(http.MethodGet, "/", nil) 55 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 56 | rec := httptest.NewRecorder() 57 | c := e.NewContext(req, rec) 58 | 59 | m := lecho.Middleware(lecho.Config{ 60 | HandleError: true, 61 | }) 62 | 63 | next := func(c echo.Context) error { 64 | return errors.New("error") 65 | } 66 | 67 | handler := m(next) 68 | err := handler(c) 69 | 70 | assert.Error(t, err, "should return error") 71 | assert.Truef(t, called, "should call error handler") 72 | }) 73 | 74 | t.Run("should use enricher", func(t *testing.T) { 75 | e := echo.New() 76 | req := httptest.NewRequest(http.MethodGet, "/", nil) 77 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 78 | rec := httptest.NewRecorder() 79 | c := e.NewContext(req, rec) 80 | 81 | b := &bytes.Buffer{} 82 | 83 | l := lecho.New(b) 84 | m := lecho.Middleware(lecho.Config{ 85 | Logger: l, 86 | Enricher: func(c echo.Context, logger zerolog.Context) zerolog.Context { 87 | return logger.Str("test", "test") 88 | }, 89 | }) 90 | 91 | next := func(c echo.Context) error { 92 | return nil 93 | } 94 | 95 | handler := m(next) 96 | err := handler(c) 97 | 98 | assert.NoError(t, err, "should not return error") 99 | 100 | str := b.String() 101 | assert.Contains(t, str, `"test":"test"`) 102 | }) 103 | 104 | t.Run("should escalate log level for slow requests", func(t *testing.T) { 105 | e := echo.New() 106 | req := httptest.NewRequest(http.MethodGet, "/", nil) 107 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 108 | rec := httptest.NewRecorder() 109 | c := e.NewContext(req, rec) 110 | 111 | b := &bytes.Buffer{} 112 | l := lecho.New(b) 113 | l.SetLevel(log.INFO) 114 | m := lecho.Middleware(lecho.Config{ 115 | Logger: l, 116 | RequestLatencyLimit: 5 * time.Millisecond, 117 | RequestLatencyLevel: zerolog.WarnLevel, 118 | }) 119 | 120 | // Slow request should be logged at the escalated level 121 | next := func(c echo.Context) error { 122 | time.Sleep(5 * time.Millisecond) 123 | return nil 124 | } 125 | handler := m(next) 126 | err := handler(c) 127 | assert.NoError(t, err, "should not return error") 128 | 129 | str := b.String() 130 | assert.Contains(t, str, `"level":"warn"`) 131 | assert.NotContains(t, str, `"level":"info"`) 132 | }) 133 | 134 | t.Run("shouldn't escalate log level for fast requests", func(t *testing.T) { 135 | e := echo.New() 136 | req := httptest.NewRequest(http.MethodGet, "/", nil) 137 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 138 | rec := httptest.NewRecorder() 139 | c := e.NewContext(req, rec) 140 | 141 | b := &bytes.Buffer{} 142 | l := lecho.New(b) 143 | l.SetLevel(log.INFO) 144 | m := lecho.Middleware(lecho.Config{ 145 | Logger: l, 146 | RequestLatencyLimit: 5 * time.Millisecond, 147 | RequestLatencyLevel: zerolog.WarnLevel, 148 | }) 149 | 150 | // Fast request should be logged at the default level 151 | next := func(c echo.Context) error { 152 | time.Sleep(1 * time.Millisecond) 153 | return nil 154 | } 155 | 156 | handler := m(next) 157 | err := handler(c) 158 | 159 | assert.NoError(t, err, "should not return error") 160 | 161 | str := b.String() 162 | assert.Contains(t, str, `"level":"info"`) 163 | assert.NotContains(t, str, `"level":"warn"`) 164 | }) 165 | 166 | t.Run("should skip middleware before calling next handler when Skipper func returns true", func(t *testing.T) { 167 | e := echo.New() 168 | req := httptest.NewRequest(http.MethodGet, "/skip", nil) 169 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 170 | rec := httptest.NewRecorder() 171 | c := e.NewContext(req, rec) 172 | 173 | b := &bytes.Buffer{} 174 | l := lecho.New(b) 175 | l.SetLevel(log.INFO) 176 | m := lecho.Middleware(lecho.Config{ 177 | Logger: l, 178 | Skipper: func(c echo.Context) bool { 179 | return c.Request().URL.Path == "/skip" 180 | }, 181 | }) 182 | 183 | next := func(c echo.Context) error { 184 | return nil 185 | } 186 | 187 | handler := m(next) 188 | err := handler(c) 189 | 190 | assert.NoError(t, err, "should not return error") 191 | 192 | str := b.String() 193 | assert.Empty(t, str, "should not log anything") 194 | }) 195 | 196 | t.Run("should skip middleware after calling next handler when AfterNextSkipper func returns true", func(t *testing.T) { 197 | e := echo.New() 198 | req := httptest.NewRequest(http.MethodGet, "/", nil) 199 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 200 | rec := httptest.NewRecorder() 201 | c := e.NewContext(req, rec) 202 | 203 | b := &bytes.Buffer{} 204 | l := lecho.New(b) 205 | l.SetLevel(log.INFO) 206 | m := lecho.Middleware(lecho.Config{ 207 | Logger: l, 208 | AfterNextSkipper: func(c echo.Context) bool { 209 | return c.Response().Status == http.StatusMovedPermanently 210 | }, 211 | }) 212 | 213 | next := func(c echo.Context) error { 214 | return c.Redirect(http.StatusMovedPermanently, "/other") 215 | } 216 | 217 | handler := m(next) 218 | err := handler(c) 219 | 220 | assert.NoError(t, err, "should not return error") 221 | 222 | str := b.String() 223 | assert.Empty(t, str, "should not log anything") 224 | }) 225 | } 226 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package lecho 2 | 3 | import ( 4 | "github.com/labstack/gommon/log" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | type ( 9 | Options struct { 10 | context zerolog.Context 11 | level log.Lvl 12 | prefix string 13 | } 14 | 15 | Setter func(opts *Options) 16 | ) 17 | 18 | func newOptions(log zerolog.Logger, setters []Setter) *Options { 19 | elvl, _ := MatchZeroLevel(log.GetLevel()) 20 | 21 | opts := &Options{ 22 | context: log.With(), 23 | level: elvl, 24 | } 25 | 26 | for _, set := range setters { 27 | set(opts) 28 | } 29 | 30 | return opts 31 | } 32 | 33 | func WithLevel(level log.Lvl) Setter { 34 | return func(opts *Options) { 35 | zlvl, elvl := MatchEchoLevel(level) 36 | 37 | opts.context = opts.context.Logger().Level(zlvl).With() 38 | opts.level = elvl 39 | } 40 | } 41 | 42 | func WithField(name string, value interface{}) Setter { 43 | return func(opts *Options) { 44 | opts.context = opts.context.Interface(name, value) 45 | } 46 | } 47 | 48 | func WithFields(fields map[string]interface{}) Setter { 49 | return func(opts *Options) { 50 | opts.context = opts.context.Fields(fields) 51 | } 52 | } 53 | 54 | func WithTimestamp() Setter { 55 | return func(opts *Options) { 56 | opts.context = opts.context.Timestamp() 57 | } 58 | } 59 | 60 | func WithCaller() Setter { 61 | return func(opts *Options) { 62 | opts.context = opts.context.Caller() 63 | } 64 | } 65 | 66 | func WithCallerWithSkipFrameCount(skipFrameCount int) Setter { 67 | return func(opts *Options) { 68 | opts.context = opts.context.CallerWithSkipFrameCount(skipFrameCount) 69 | } 70 | } 71 | 72 | func WithPrefix(prefix string) Setter { 73 | return func(opts *Options) { 74 | opts.context = opts.context.Str("prefix", prefix) 75 | } 76 | } 77 | 78 | func WithHook(hook zerolog.Hook) Setter { 79 | return func(opts *Options) { 80 | opts.context = opts.context.Logger().Hook(hook).With() 81 | } 82 | } 83 | 84 | func WithHookFunc(hook zerolog.HookFunc) Setter { 85 | return func(opts *Options) { 86 | opts.context = opts.context.Logger().Hook(hook).With() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package lecho_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/labstack/gommon/log" 12 | "github.com/rs/zerolog" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/ziflex/lecho/v3" 15 | ) 16 | 17 | func TestWithCaller(t *testing.T) { 18 | b := &bytes.Buffer{} 19 | 20 | l := lecho.New(b, lecho.WithCaller()) 21 | 22 | l.Print("foobar") 23 | 24 | type Log struct { 25 | Level string `json:"level"` 26 | Caller string `json:"caller"` 27 | Message string `json:"message"` 28 | } 29 | 30 | log := &Log{} 31 | 32 | err := json.Unmarshal(b.Bytes(), log) 33 | 34 | assert.NoError(t, err) 35 | 36 | segments := strings.Split(log.Caller, ":") 37 | filePath := filepath.Base(segments[0]) 38 | 39 | assert.Equal(t, filePath, "logger.go") 40 | } 41 | 42 | func TestWithCallerWithSkipFrameCount(t *testing.T) { 43 | b := &bytes.Buffer{} 44 | 45 | l := lecho.New(b, lecho.WithCallerWithSkipFrameCount(3)) 46 | 47 | l.Print("foobar") 48 | 49 | type Log struct { 50 | Level string `json:"level"` 51 | Caller string `json:"caller"` 52 | Message string `json:"message"` 53 | } 54 | 55 | log := &Log{} 56 | 57 | err := json.Unmarshal(b.Bytes(), log) 58 | 59 | assert.NoError(t, err) 60 | 61 | segments := strings.Split(log.Caller, ":") 62 | filePath := filepath.Base(segments[0]) 63 | 64 | assert.Equal(t, filePath, "options_test.go") 65 | } 66 | 67 | func TestWithField(t *testing.T) { 68 | b := &bytes.Buffer{} 69 | 70 | l := lecho.New(b, lecho.WithField("service", "logging")) 71 | 72 | l.Print("foobar") 73 | 74 | type Log struct { 75 | Level string `json:"level"` 76 | Service string `json:"service"` 77 | Message string `json:"message"` 78 | } 79 | 80 | log := &Log{} 81 | 82 | err := json.Unmarshal(b.Bytes(), log) 83 | 84 | assert.NoError(t, err) 85 | assert.Equal(t, log.Service, "logging") 86 | } 87 | 88 | func TestWithFields(t *testing.T) { 89 | b := &bytes.Buffer{} 90 | 91 | l := lecho.New(b, lecho.WithFields(map[string]interface{}{ 92 | "host": "localhost", 93 | "port": 8080, 94 | })) 95 | 96 | l.Print("foobar") 97 | 98 | type Log struct { 99 | Level string `json:"level"` 100 | Host string `json:"host"` 101 | Port int `json:"port"` 102 | Message string `json:"message"` 103 | } 104 | 105 | log := &Log{} 106 | 107 | err := json.Unmarshal(b.Bytes(), log) 108 | 109 | assert.NoError(t, err) 110 | assert.Equal(t, log.Host, "localhost") 111 | assert.Equal(t, log.Port, 8080) 112 | } 113 | 114 | type ( 115 | Hook struct { 116 | logs []HookLog 117 | } 118 | 119 | HookLog struct { 120 | level zerolog.Level 121 | message string 122 | } 123 | ) 124 | 125 | func (h *Hook) Run(e *zerolog.Event, level zerolog.Level, message string) { 126 | h.logs = append(h.logs, HookLog{ 127 | level: level, 128 | message: message, 129 | }) 130 | } 131 | 132 | func TestWithHook(t *testing.T) { 133 | b := &bytes.Buffer{} 134 | h := &Hook{} 135 | l := lecho.New(b, lecho.WithHook(h)) 136 | 137 | l.Info("Foo") 138 | l.Warn("Bar") 139 | 140 | assert.Len(t, h.logs, 2) 141 | assert.Equal(t, h.logs[0].level, zerolog.InfoLevel) 142 | assert.Equal(t, h.logs[0].message, "Foo") 143 | assert.Equal(t, h.logs[1].level, zerolog.WarnLevel) 144 | assert.Equal(t, h.logs[1].message, "Bar") 145 | } 146 | 147 | func TestWithHookFunc(t *testing.T) { 148 | b := &bytes.Buffer{} 149 | logs := make([]HookLog, 0, 2) 150 | l := lecho.New(b, lecho.WithHookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { 151 | logs = append(logs, HookLog{ 152 | level: level, 153 | message: message, 154 | }) 155 | })) 156 | 157 | l.Info("Foo") 158 | l.Warn("Bar") 159 | 160 | assert.Len(t, logs, 2) 161 | assert.Equal(t, logs[0].level, zerolog.InfoLevel) 162 | assert.Equal(t, logs[0].message, "Foo") 163 | assert.Equal(t, logs[1].level, zerolog.WarnLevel) 164 | assert.Equal(t, logs[1].message, "Bar") 165 | } 166 | 167 | func TestWithLevel(t *testing.T) { 168 | b := &bytes.Buffer{} 169 | l := lecho.New(b, lecho.WithLevel(log.WARN)) 170 | 171 | l.Debug("Test") 172 | 173 | assert.Equal(t, b.String(), "") 174 | 175 | l.Warn("Foobar") 176 | 177 | assert.Equal(t, b.String(), `{"level":"warn","message":"Foobar"} 178 | `) 179 | } 180 | 181 | func TestWithPrefix(t *testing.T) { 182 | b := &bytes.Buffer{} 183 | l := lecho.New(b, lecho.WithPrefix("Test")) 184 | 185 | l.Warn("Foobar") 186 | 187 | assert.Equal(t, b.String(), `{"level":"warn","prefix":"Test","message":"Foobar"} 188 | `) 189 | } 190 | 191 | func TestWithTimestamp(t *testing.T) { 192 | b := &bytes.Buffer{} 193 | 194 | l := lecho.New(b, lecho.WithTimestamp()) 195 | 196 | l.Print("foobar") 197 | 198 | type Log struct { 199 | Level string `json:"level"` 200 | Message string `json:"message"` 201 | Time time.Time `json:"time"` 202 | } 203 | 204 | log := &Log{} 205 | 206 | err := json.Unmarshal(b.Bytes(), log) 207 | 208 | assert.NoError(t, err) 209 | assert.NotEmpty(t, log.Time) 210 | } 211 | --------------------------------------------------------------------------------