├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmarks ├── apex_test.go ├── doc.go ├── go.mod ├── go.sum ├── kit_test.go ├── log15_test.go ├── logn_test.go ├── logrus_test.go ├── scenario_bench_test.go ├── zap_test.go └── zerolog_test.go ├── doc.go ├── entry.go ├── entry_test.go ├── example_test.go ├── go.mod ├── go.sum ├── handler.go ├── handlers ├── cli │ └── cli.go ├── handlers.go ├── json │ ├── json.go │ └── json_test.go ├── level │ ├── level.go │ └── level_test.go ├── memory │ └── memory.go ├── multi │ ├── multi.go │ └── multi_test.go └── text │ ├── text.go │ └── text_test.go ├── interfaces.go ├── levels.go ├── levels_test.go ├── logger.go ├── logger_test.go ├── objectpools.go └── stack.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bep] -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | pull_request: 5 | name: Test 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go-version: [1.20.x,1.21.x] 11 | platform: [ubuntu-latest, macos-latest, windows-latest] 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Install staticcheck 19 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 20 | shell: bash 21 | - name: Update PATH 22 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 23 | shell: bash 24 | - name: Checkout code 25 | uses: actions/checkout@v1 26 | - name: Fmt 27 | if: matrix.platform != 'windows-latest' # :( 28 | run: "diff <(gofmt -d .) <(printf '')" 29 | shell: bash 30 | - name: Vet 31 | run: go vet ./... 32 | - name: Staticcheck 33 | run: staticcheck ./... 34 | - name: Test 35 | run: go test -race ./... 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Tests on Linux, MacOS and Windows](https://github.com/bep/logg/workflows/Test/badge.svg)](https://github.com/bep/logg/actions?query=workflow:Test) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/bep/logg)](https://goreportcard.com/report/github.com/bep/logg) 4 | [![GoDoc](https://godoc.org/github.com/bep/logg?status.svg)](https://godoc.org/github.com/bep/logg) 5 | 6 | This is a fork of the exellent [Apex Log](https://github.com/apex/log) library. 7 | 8 | Main changes: 9 | 10 | * Trim unneeded dependencies. 11 | * Make `Fields` into a slice to preserve log order. 12 | * Split the old `Interface` in two and remove all but one `Log` method (see below). 13 | * This allows for lazy creation of messages in `Log(fmt.Stringer)` and ignoring fields added in `LevelLogger`s with levels below the `Logger`s. 14 | * The pointer passed to `HandleLog` is not safe to use outside of the current log chain, and needs to be cloned with `Clone` first if that's needed. 15 | * See [Benchmarks](#benchmarks) for more info. 16 | 17 | This is probably the very fastest structured log library when logging is disabled: 18 | 19 | image 20 | 21 | > One can never have enough log libraries! 22 | 23 | ```go 24 | // Logger is the main interface for the logger. 25 | type Logger interface { 26 | // WithLevel returns a new entry with `level` set. 27 | WithLevel(Level) *Entry 28 | } 29 | 30 | // LevelLogger is the logger at a given level. 31 | type LevelLogger interface { 32 | // Log logs a message at the given level using the string from calling s.String(). 33 | // Note that s.String() will not be called if the level is not enabled. 34 | Log(s fmt.Stringer) 35 | 36 | // Logf logs a message at the given level using the format and args from calling fmt.Sprintf(). 37 | // Note that fmt.Sprintf() will not be called if the level is not enabled. 38 | Logf(format string, a ...any) 39 | 40 | // WithLevel returns a new entry with `level` set. 41 | WithLevel(Level) *Entry 42 | 43 | // WithFields returns a new entry with the`fields` in fields set. 44 | // This is a noop if LevelLogger's level is less than Logger's. 45 | WithFields(fields Fielder) *Entry 46 | 47 | // WithLevel returns a new entry with the field f set with value v 48 | // This is a noop if LevelLogger's level is less than Logger's. 49 | WithField(f string, v any) *Entry 50 | 51 | // WithDuration returns a new entry with the "duration" field set 52 | // to the given duration in milliseconds. 53 | // This is a noop if LevelLogger's level is less than Logger's. 54 | WithDuration(time.Duration) *Entry 55 | 56 | // WithError returns a new entry with the "error" set to `err`. 57 | // This is a noop if err is nil or LevelLogger's level is less than Logger's. 58 | WithError(error) *Entry 59 | } 60 | ``` 61 | 62 | ## Benchmarks 63 | 64 | Benchmarks below are borrowed and adapted from [Zap](https://github.com/uber-go/zap/tree/master/benchmarks). 65 | 66 | ### Logging at a disabled level without any structured context 67 | 68 | ``` 69 | name time/op 70 | DisabledWithoutFields/apex/log-10 33.9ns ± 0% 71 | DisabledWithoutFields/bep/logg-10 0.28ns ± 0% 72 | DisabledWithoutFields/sirupsen/logrus-10 6.54ns ± 0% 73 | DisabledWithoutFields/rs/zerolog-10 0.31ns ± 0% 74 | 75 | name alloc/op 76 | DisabledWithoutFields/apex/log-10 112B ± 0% 77 | DisabledWithoutFields/bep/logg-10 0.00B 78 | DisabledWithoutFields/sirupsen/logrus-10 16.0B ± 0% 79 | DisabledWithoutFields/rs/zerolog-10 0.00B 80 | 81 | name allocs/op 82 | DisabledWithoutFields/apex/log-10 1.00 ± 0% 83 | DisabledWithoutFields/bep/logg-10 0.00 84 | DisabledWithoutFields/sirupsen/logrus-10 1.00 ± 0% 85 | DisabledWithoutFields/rs/zerolog-10 0.00 86 | ``` 87 | 88 | 89 | ### Logging at a disabled level with some accumulated context 90 | 91 | ``` 92 | name time/op 93 | DisabledAccumulatedContext/apex/log-10 0.29ns ± 0% 94 | DisabledAccumulatedContext/bep/logg-10 0.27ns ± 0% 95 | DisabledAccumulatedContext/sirupsen/logrus-10 6.61ns ± 0% 96 | DisabledAccumulatedContext/rs/zerolog-10 0.32ns ± 0% 97 | 98 | name alloc/op 99 | DisabledAccumulatedContext/apex/log-10 0.00B 100 | DisabledAccumulatedContext/bep/logg-10 0.00B 101 | DisabledAccumulatedContext/sirupsen/logrus-10 16.0B ± 0% 102 | DisabledAccumulatedContext/rs/zerolog-10 0.00B 103 | 104 | name allocs/op 105 | DisabledAccumulatedContext/apex/log-10 0.00 106 | DisabledAccumulatedContext/bep/logg-10 0.00 107 | DisabledAccumulatedContext/sirupsen/logrus-10 1.00 ± 0% 108 | DisabledAccumulatedContext/rs/zerolog-10 0.00 109 | ``` 110 | 111 | ### Logging at a disabled level, adding context at each log site 112 | 113 | ``` 114 | name time/op 115 | DisabledAddingFields/apex/log-10 328ns ± 0% 116 | DisabledAddingFields/bep/logg-10 0.38ns ± 0% 117 | DisabledAddingFields/sirupsen/logrus-10 610ns ± 0% 118 | DisabledAddingFields/rs/zerolog-10 10.5ns ± 0% 119 | 120 | name alloc/op 121 | DisabledAddingFields/apex/log-10 886B ± 0% 122 | DisabledAddingFields/bep/logg-10 0.00B 123 | DisabledAddingFields/sirupsen/logrus-10 1.52kB ± 0% 124 | DisabledAddingFields/rs/zerolog-10 24.0B ± 0% 125 | 126 | name allocs/op 127 | DisabledAddingFields/apex/log-10 10.0 ± 0% 128 | DisabledAddingFields/bep/logg-10 0.00 129 | DisabledAddingFields/sirupsen/logrus-10 12.0 ± 0% 130 | DisabledAddingFields/rs/zerolog-10 1.00 ± 0% 131 | ``` 132 | 133 | ### Logging without any structured context 134 | 135 | ``` 136 | name time/op 137 | WithoutFields/apex/log-10 964ns ± 0% 138 | WithoutFields/bep/logg-10 100ns ± 0% 139 | WithoutFields/go-kit/kit/log-10 232ns ± 0% 140 | WithoutFields/inconshreveable/log15-10 2.13µs ± 0% 141 | WithoutFields/sirupsen/logrus-10 866ns ± 0% 142 | WithoutFields/stdlib.Println-10 7.08ns ± 0% 143 | WithoutFields/stdlib.Printf-10 56.4ns ± 0% 144 | WithoutFields/rs/zerolog-10 30.9ns ± 0% 145 | WithoutFields/rs/zerolog.Formatting-10 1.33µs ± 0% 146 | WithoutFields/rs/zerolog.Check-10 32.1ns ± 0% 147 | 148 | name alloc/op 149 | WithoutFields/apex/log-10 352B ± 0% 150 | WithoutFields/bep/logg-10 56.0B ± 0% 151 | WithoutFields/go-kit/kit/log-10 520B ± 0% 152 | WithoutFields/inconshreveable/log15-10 1.43kB ± 0% 153 | WithoutFields/sirupsen/logrus-10 1.14kB ± 0% 154 | WithoutFields/stdlib.Println-10 16.0B ± 0% 155 | WithoutFields/stdlib.Printf-10 136B ± 0% 156 | WithoutFields/rs/zerolog-10 0.00B 157 | WithoutFields/rs/zerolog.Formatting-10 1.92kB ± 0% 158 | WithoutFields/rs/zerolog.Check-10 0.00B 159 | 160 | name allocs/op 161 | WithoutFields/apex/log-10 6.00 ± 0% 162 | WithoutFields/bep/logg-10 2.00 ± 0% 163 | WithoutFields/go-kit/kit/log-10 9.00 ± 0% 164 | WithoutFields/inconshreveable/log15-10 20.0 ± 0% 165 | WithoutFields/sirupsen/logrus-10 23.0 ± 0% 166 | WithoutFields/stdlib.Println-10 1.00 ± 0% 167 | WithoutFields/stdlib.Printf-10 6.00 ± 0% 168 | WithoutFields/rs/zerolog-10 0.00 169 | WithoutFields/rs/zerolog.Formatting-10 58.0 ± 0% 170 | WithoutFields/rs/zerolog.Check-10 0.00 171 | ``` 172 | 173 | 174 | ### Logging with some accumulated context 175 | 176 | ``` 177 | name time/op 178 | AccumulatedContext/apex/log-10 12.7µs ± 0% 179 | AccumulatedContext/bep/logg-10 1.52µs ± 0% 180 | AccumulatedContext/go-kit/kit/log-10 2.52µs ± 0% 181 | AccumulatedContext/inconshreveable/log15-10 9.36µs ± 0% 182 | AccumulatedContext/sirupsen/logrus-10 3.41µs ± 0% 183 | AccumulatedContext/rs/zerolog-10 37.9ns ± 0% 184 | AccumulatedContext/rs/zerolog.Check-10 34.0ns ± 0% 185 | AccumulatedContext/rs/zerolog.Formatting-10 1.36µs ± 0% 186 | 187 | name alloc/op 188 | AccumulatedContext/apex/log-10 3.30kB ± 0% 189 | AccumulatedContext/bep/logg-10 1.16kB ± 0% 190 | AccumulatedContext/go-kit/kit/log-10 3.67kB ± 0% 191 | AccumulatedContext/inconshreveable/log15-10 3.31kB ± 0% 192 | AccumulatedContext/sirupsen/logrus-10 4.73kB ± 0% 193 | AccumulatedContext/rs/zerolog-10 0.00B 194 | AccumulatedContext/rs/zerolog.Check-10 0.00B 195 | AccumulatedContext/rs/zerolog.Formatting-10 1.92kB ± 0% 196 | 197 | name allocs/op 198 | AccumulatedContext/apex/log-10 53.0 ± 0% 199 | AccumulatedContext/bep/logg-10 25.0 ± 0% 200 | AccumulatedContext/go-kit/kit/log-10 56.0 ± 0% 201 | AccumulatedContext/inconshreveable/log15-10 70.0 ± 0% 202 | AccumulatedContext/sirupsen/logrus-10 68.0 ± 0% 203 | AccumulatedContext/rs/zerolog-10 0.00 204 | AccumulatedContext/rs/zerolog.Check-10 0.00 205 | AccumulatedContext/rs/zerolog.Formatting-10 58.0 ± 0% 206 | ``` 207 | 208 | 209 | ## Logging with additional context at each log site 210 | 211 | ``` 212 | name time/op 213 | AddingFields/apex/log-10 13.2µs ± 0% 214 | AddingFields/bep/logg-10 1.79µs ± 0% 215 | AddingFields/go-kit/kit/log-10 2.23µs ± 0% 216 | AddingFields/inconshreveable/log15-10 14.3µs ± 0% 217 | AddingFields/sirupsen/logrus-10 4.46µs ± 0% 218 | AddingFields/rs/zerolog-10 398ns ± 0% 219 | AddingFields/rs/zerolog.Check-10 389ns ± 0% 220 | 221 | name alloc/op 222 | AddingFields/apex/log-10 4.19kB ± 0% 223 | AddingFields/bep/logg-10 2.02kB ± 0% 224 | AddingFields/go-kit/kit/log-10 3.31kB ± 0% 225 | AddingFields/inconshreveable/log15-10 6.68kB ± 0% 226 | AddingFields/sirupsen/logrus-10 6.27kB ± 0% 227 | AddingFields/rs/zerolog-10 24.0B ± 0% 228 | AddingFields/rs/zerolog.Check-10 24.0B ± 0% 229 | 230 | name allocs/op 231 | AddingFields/apex/log-10 63.0 ± 0% 232 | AddingFields/bep/logg-10 34.0 ± 0% 233 | AddingFields/go-kit/kit/log-10 57.0 ± 0% 234 | AddingFields/inconshreveable/log15-10 74.0 ± 0% 235 | AddingFields/sirupsen/logrus-10 79.0 ± 0% 236 | AddingFields/rs/zerolog-10 1.00 ± 0% 237 | AddingFields/rs/zerolog.Check-10 1.00 ± 0% 238 | ``` 239 | -------------------------------------------------------------------------------- /benchmarks/apex_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | 26 | "github.com/apex/log" 27 | "github.com/apex/log/handlers/json" 28 | ) 29 | 30 | func newDisabledApexLog() *log.Logger { 31 | return &log.Logger{ 32 | Handler: json.New(io.Discard), 33 | Level: log.ErrorLevel, 34 | } 35 | } 36 | 37 | func newApexLog() *log.Logger { 38 | return &log.Logger{ 39 | Handler: json.New(io.Discard), 40 | Level: log.DebugLevel, 41 | } 42 | } 43 | 44 | func fakeApexFields() log.Fields { 45 | return log.Fields{ 46 | "int": _tenInts[0], 47 | "ints": _tenInts, 48 | "string": _tenStrings[0], 49 | "strings": _tenStrings, 50 | "time": _tenTimes[0], 51 | "times": _tenTimes, 52 | "user1": _oneUser, 53 | "user2": _oneUser, 54 | "users": _tenUsers, 55 | "error": errExample, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /benchmarks/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package benchmarks contains only benchmarks comparing zap to other 22 | // structured logging libraries. 23 | package benchmarks 24 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bep/logg/benchmarks 2 | 3 | go 1.18 4 | 5 | replace github.com/bep/logg => ../ 6 | 7 | require ( 8 | github.com/apex/log v1.9.0 9 | github.com/go-kit/log v0.2.0 10 | github.com/rs/zerolog v1.26.0 11 | github.com/sirupsen/logrus v1.8.1 12 | go.uber.org/multierr v1.7.0 13 | go.uber.org/zap v1.19.1 14 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 15 | ) 16 | 17 | require ( 18 | github.com/benbjohnson/clock v1.2.0 // indirect 19 | github.com/bep/clocks v0.5.0 // indirect 20 | github.com/bep/logg v0.0.0-20220809094309-f3eda2566f97 21 | github.com/go-logfmt/logfmt v0.5.1 // indirect 22 | github.com/go-stack/stack v1.8.1 // indirect 23 | github.com/mattn/go-colorable v0.1.12 // indirect 24 | github.com/mattn/go-isatty v0.0.14 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | go.uber.org/atomic v1.9.0 // indirect 27 | go.uber.org/goleak v1.1.12 // indirect 28 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= 2 | github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= 3 | github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= 4 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 5 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 6 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 7 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 8 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 9 | github.com/benbjohnson/clock v1.2.0 h1:9Re3G2TWxkE06LdMWMpcY6KV81GLXMGiYpPYUPkFAws= 10 | github.com/benbjohnson/clock v1.2.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 11 | github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= 12 | github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= 13 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 18 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw= 21 | github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 22 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 23 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 24 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 25 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 26 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 27 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 31 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 32 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 33 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 34 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 35 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 36 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 37 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 38 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 39 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 41 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 42 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 43 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 44 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 45 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 46 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 47 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 48 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 49 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 50 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 51 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 52 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 53 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 55 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 59 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 60 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 61 | github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= 62 | github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= 63 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 64 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 65 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 66 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 67 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 68 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 72 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 74 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 76 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= 77 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 78 | github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= 79 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 80 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 81 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 82 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 83 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 84 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 85 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 86 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 87 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 88 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 89 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 90 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 91 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 92 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 93 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 94 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 95 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 96 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 97 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 98 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 99 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 100 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 103 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 105 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 106 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= 122 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 124 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 125 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 126 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 127 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 128 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 129 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 130 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 131 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 132 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 139 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 140 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 h1:iiHuQZCNgYPmFQxd3BBN/Nc5+dAwzZuq5y40s20oQw0= 141 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 142 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 143 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 144 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 145 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 146 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 147 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 148 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 150 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 151 | -------------------------------------------------------------------------------- /benchmarks/kit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | 26 | "github.com/go-kit/log" 27 | ) 28 | 29 | func newKitLog(fields ...interface{}) log.Logger { 30 | return log.With(log.NewJSONLogger(io.Discard), fields...) 31 | } 32 | -------------------------------------------------------------------------------- /benchmarks/log15_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | 26 | "gopkg.in/inconshreveable/log15.v2" 27 | ) 28 | 29 | func newLog15() log15.Logger { 30 | logger := log15.New() 31 | logger.SetHandler(log15.StreamHandler(io.Discard, log15.JsonFormat())) 32 | return logger 33 | } 34 | -------------------------------------------------------------------------------- /benchmarks/logn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | 26 | "github.com/bep/logg" 27 | "github.com/bep/logg/handlers/json" 28 | ) 29 | 30 | func newDisabledLoggLog() logg.LevelLogger { 31 | logger := logg.New(logg.Options{ 32 | Handler: json.New(io.Discard), 33 | Level: logg.LevelError, 34 | }) 35 | return logger.WithLevel(logg.LevelInfo) 36 | } 37 | 38 | func newLoggLog() logg.LevelLogger { 39 | logger := logg.New(logg.Options{ 40 | Handler: json.New(io.Discard), 41 | Level: logg.LevelDebug, 42 | }) 43 | 44 | return logger.WithLevel(logg.LevelDebug) 45 | } 46 | 47 | func fakeLognFields() logg.FieldsFunc { 48 | return func() logg.Fields { 49 | return logg.Fields{ 50 | {Name: "int", Value: _tenInts[0]}, 51 | {Name: "ints", Value: _tenInts}, 52 | {Name: "string", Value: _tenStrings[0]}, 53 | {Name: "strings", Value: _tenStrings}, 54 | {Name: "time", Value: _tenTimes[0]}, 55 | {Name: "times", Value: _tenTimes}, 56 | {Name: "user1", Value: _oneUser}, 57 | {Name: "user2", Value: _oneUser}, 58 | {Name: "users", Value: _tenUsers}, 59 | {Name: "error", Value: errExample}, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /benchmarks/logrus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | 26 | "github.com/sirupsen/logrus" 27 | ) 28 | 29 | func newDisabledLogrus() *logrus.Logger { 30 | logger := newLogrus() 31 | logger.Level = logrus.ErrorLevel 32 | return logger 33 | } 34 | 35 | func newLogrus() *logrus.Logger { 36 | return &logrus.Logger{ 37 | Out: io.Discard, 38 | Formatter: new(logrus.JSONFormatter), 39 | Hooks: make(logrus.LevelHooks), 40 | Level: logrus.DebugLevel, 41 | } 42 | } 43 | 44 | func fakeLogrusFields() logrus.Fields { 45 | return logrus.Fields{ 46 | "int": _tenInts[0], 47 | "ints": _tenInts, 48 | "string": _tenStrings[0], 49 | "strings": _tenStrings, 50 | "time": _tenTimes[0], 51 | "times": _tenTimes, 52 | "user1": _oneUser, 53 | "user2": _oneUser, 54 | "users": _tenUsers, 55 | "error": errExample, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /benchmarks/scenario_bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | "log" 26 | "testing" 27 | 28 | "github.com/bep/logg" 29 | ) 30 | 31 | func BenchmarkDisabledWithoutFields(b *testing.B) { 32 | b.Logf("Logging at a disabled level without any structured context.") 33 | b.Run("apex/log", func(b *testing.B) { 34 | logger := newDisabledApexLog() 35 | b.ResetTimer() 36 | b.RunParallel(func(pb *testing.PB) { 37 | for pb.Next() { 38 | logger.Info(getMessage(0)) 39 | } 40 | }) 41 | }) 42 | b.Run("bep/logg", func(b *testing.B) { 43 | logger := newDisabledLoggLog() 44 | b.ResetTimer() 45 | b.RunParallel(func(pb *testing.PB) { 46 | for pb.Next() { 47 | message := logg.StringFunc(func() string { return getMessage(0) }) 48 | logger.Log(message) 49 | } 50 | }) 51 | }) 52 | b.Run("sirupsen/logrus", func(b *testing.B) { 53 | logger := newDisabledLogrus() 54 | b.ResetTimer() 55 | b.RunParallel(func(pb *testing.PB) { 56 | for pb.Next() { 57 | logger.Info(getMessage(0)) 58 | } 59 | }) 60 | }) 61 | b.Run("rs/zerolog", func(b *testing.B) { 62 | logger := newDisabledZerolog() 63 | b.ResetTimer() 64 | b.RunParallel(func(pb *testing.PB) { 65 | for pb.Next() { 66 | logger.Info().Msg(getMessage(0)) 67 | } 68 | }) 69 | }) 70 | } 71 | 72 | func BenchmarkDisabledAccumulatedContext(b *testing.B) { 73 | b.Logf("Logging at a disabled level with some accumulated context.") 74 | b.Run("apex/log", func(b *testing.B) { 75 | logger := newDisabledApexLog().WithFields(fakeApexFields()) 76 | b.ResetTimer() 77 | b.RunParallel(func(pb *testing.PB) { 78 | for pb.Next() { 79 | logger.Info(getMessage(0)) 80 | } 81 | }) 82 | }) 83 | b.Run("bep/logg", func(b *testing.B) { 84 | logger := newDisabledLoggLog().WithFields(fakeLognFields()) 85 | b.ResetTimer() 86 | b.RunParallel(func(pb *testing.PB) { 87 | for pb.Next() { 88 | message := logg.StringFunc(func() string { return getMessage(0) }) 89 | logger.Log(message) 90 | } 91 | }) 92 | }) 93 | b.Run("sirupsen/logrus", func(b *testing.B) { 94 | logger := newDisabledLogrus().WithFields(fakeLogrusFields()) 95 | b.ResetTimer() 96 | b.RunParallel(func(pb *testing.PB) { 97 | for pb.Next() { 98 | logger.Info(getMessage(0)) 99 | } 100 | }) 101 | }) 102 | b.Run("rs/zerolog", func(b *testing.B) { 103 | logger := fakeZerologContext(newDisabledZerolog().With()).Logger() 104 | b.ResetTimer() 105 | b.RunParallel(func(pb *testing.PB) { 106 | for pb.Next() { 107 | logger.Info().Msg(getMessage(0)) 108 | } 109 | }) 110 | }) 111 | } 112 | 113 | func BenchmarkDisabledAddingFields(b *testing.B) { 114 | b.Logf("Logging at a disabled level, adding context at each log site.") 115 | b.Run("apex/log", func(b *testing.B) { 116 | logger := newDisabledApexLog() 117 | b.ResetTimer() 118 | b.RunParallel(func(pb *testing.PB) { 119 | for pb.Next() { 120 | logger.WithFields(fakeApexFields()).Info(getMessage(0)) 121 | } 122 | }) 123 | }) 124 | b.Run("bep/logg", func(b *testing.B) { 125 | logger := newDisabledLoggLog() 126 | b.ResetTimer() 127 | b.RunParallel(func(pb *testing.PB) { 128 | for pb.Next() { 129 | message := logg.StringFunc(func() string { return getMessage(0) }) 130 | logger.WithFields(fakeLognFields()).Log(message) 131 | } 132 | }) 133 | }) 134 | b.Run("sirupsen/logrus", func(b *testing.B) { 135 | logger := newDisabledLogrus() 136 | b.ResetTimer() 137 | b.RunParallel(func(pb *testing.PB) { 138 | for pb.Next() { 139 | logger.WithFields(fakeLogrusFields()).Info(getMessage(0)) 140 | } 141 | }) 142 | }) 143 | b.Run("rs/zerolog", func(b *testing.B) { 144 | logger := newDisabledZerolog() 145 | b.ResetTimer() 146 | b.RunParallel(func(pb *testing.PB) { 147 | for pb.Next() { 148 | fakeZerologFields(logger.Info()).Msg(getMessage(0)) 149 | } 150 | }) 151 | }) 152 | } 153 | 154 | func BenchmarkWithoutFields(b *testing.B) { 155 | b.Logf("Logging without any structured context.") 156 | b.Run("apex/log", func(b *testing.B) { 157 | logger := newApexLog() 158 | b.ResetTimer() 159 | b.RunParallel(func(pb *testing.PB) { 160 | for pb.Next() { 161 | logger.Info(getMessage(0)) 162 | } 163 | }) 164 | }) 165 | b.Run("bep/logg", func(b *testing.B) { 166 | logger := newLoggLog() 167 | b.ResetTimer() 168 | b.RunParallel(func(pb *testing.PB) { 169 | for pb.Next() { 170 | message := logg.StringFunc(func() string { return getMessage(0) }) 171 | logger.Log(message) 172 | } 173 | }) 174 | }) 175 | b.Run("go-kit/kit/log", func(b *testing.B) { 176 | logger := newKitLog() 177 | b.ResetTimer() 178 | b.RunParallel(func(pb *testing.PB) { 179 | for pb.Next() { 180 | logger.Log(getMessage(0), getMessage(1)) 181 | } 182 | }) 183 | }) 184 | b.Run("inconshreveable/log15", func(b *testing.B) { 185 | logger := newLog15() 186 | b.ResetTimer() 187 | b.RunParallel(func(pb *testing.PB) { 188 | for pb.Next() { 189 | logger.Info(getMessage(0)) 190 | } 191 | }) 192 | }) 193 | b.Run("sirupsen/logrus", func(b *testing.B) { 194 | logger := newLogrus() 195 | b.ResetTimer() 196 | b.RunParallel(func(pb *testing.PB) { 197 | for pb.Next() { 198 | logger.Info(getMessage(0)) 199 | } 200 | }) 201 | }) 202 | b.Run("stdlib.Println", func(b *testing.B) { 203 | logger := log.New(io.Discard, "", log.LstdFlags) 204 | b.ResetTimer() 205 | b.RunParallel(func(pb *testing.PB) { 206 | for pb.Next() { 207 | logger.Println(getMessage(0)) 208 | } 209 | }) 210 | }) 211 | b.Run("stdlib.Printf", func(b *testing.B) { 212 | logger := log.New(io.Discard, "", log.LstdFlags) 213 | b.ResetTimer() 214 | b.RunParallel(func(pb *testing.PB) { 215 | for pb.Next() { 216 | logger.Printf("%v %v %v %s %v %v %v %v %v %s\n", fakeFmtArgs()...) 217 | } 218 | }) 219 | }) 220 | b.Run("rs/zerolog", func(b *testing.B) { 221 | logger := newZerolog() 222 | b.ResetTimer() 223 | b.RunParallel(func(pb *testing.PB) { 224 | for pb.Next() { 225 | logger.Info().Msg(getMessage(0)) 226 | } 227 | }) 228 | }) 229 | b.Run("rs/zerolog.Formatting", func(b *testing.B) { 230 | logger := newZerolog() 231 | b.ResetTimer() 232 | b.RunParallel(func(pb *testing.PB) { 233 | for pb.Next() { 234 | logger.Info().Msgf("%v %v %v %s %v %v %v %v %v %s\n", fakeFmtArgs()...) 235 | } 236 | }) 237 | }) 238 | b.Run("rs/zerolog.Check", func(b *testing.B) { 239 | logger := newZerolog() 240 | b.ResetTimer() 241 | b.RunParallel(func(pb *testing.PB) { 242 | for pb.Next() { 243 | if e := logger.Info(); e.Enabled() { 244 | e.Msg(getMessage(0)) 245 | } 246 | } 247 | }) 248 | }) 249 | } 250 | 251 | func BenchmarkAccumulatedContext(b *testing.B) { 252 | b.Logf("Logging with some accumulated context.") 253 | b.Run("apex/log", func(b *testing.B) { 254 | logger := newApexLog().WithFields(fakeApexFields()) 255 | b.ResetTimer() 256 | b.RunParallel(func(pb *testing.PB) { 257 | for pb.Next() { 258 | logger.Info(getMessage(0)) 259 | } 260 | }) 261 | }) 262 | b.Run("bep/logg", func(b *testing.B) { 263 | logger := newLoggLog().WithFields(fakeLognFields()) 264 | b.ResetTimer() 265 | b.RunParallel(func(pb *testing.PB) { 266 | for pb.Next() { 267 | message := logg.StringFunc(func() string { return getMessage(0) }) 268 | logger.Log(message) 269 | } 270 | }) 271 | }) 272 | b.Run("go-kit/kit/log", func(b *testing.B) { 273 | logger := newKitLog(fakeSugarFields()...) 274 | b.ResetTimer() 275 | b.RunParallel(func(pb *testing.PB) { 276 | for pb.Next() { 277 | logger.Log(getMessage(0), getMessage(1)) 278 | } 279 | }) 280 | }) 281 | b.Run("inconshreveable/log15", func(b *testing.B) { 282 | logger := newLog15().New(fakeSugarFields()) 283 | b.ResetTimer() 284 | b.RunParallel(func(pb *testing.PB) { 285 | for pb.Next() { 286 | logger.Info(getMessage(0)) 287 | } 288 | }) 289 | }) 290 | b.Run("sirupsen/logrus", func(b *testing.B) { 291 | logger := newLogrus().WithFields(fakeLogrusFields()) 292 | b.ResetTimer() 293 | b.RunParallel(func(pb *testing.PB) { 294 | for pb.Next() { 295 | logger.Info(getMessage(0)) 296 | } 297 | }) 298 | }) 299 | b.Run("rs/zerolog", func(b *testing.B) { 300 | logger := fakeZerologContext(newZerolog().With()).Logger() 301 | b.ResetTimer() 302 | b.RunParallel(func(pb *testing.PB) { 303 | for pb.Next() { 304 | logger.Info().Msg(getMessage(0)) 305 | } 306 | }) 307 | }) 308 | b.Run("rs/zerolog.Check", func(b *testing.B) { 309 | logger := fakeZerologContext(newZerolog().With()).Logger() 310 | b.ResetTimer() 311 | b.RunParallel(func(pb *testing.PB) { 312 | for pb.Next() { 313 | if e := logger.Info(); e.Enabled() { 314 | e.Msg(getMessage(0)) 315 | } 316 | } 317 | }) 318 | }) 319 | b.Run("rs/zerolog.Formatting", func(b *testing.B) { 320 | logger := fakeZerologContext(newZerolog().With()).Logger() 321 | b.ResetTimer() 322 | b.RunParallel(func(pb *testing.PB) { 323 | for pb.Next() { 324 | logger.Info().Msgf("%v %v %v %s %v %v %v %v %v %s\n", fakeFmtArgs()...) 325 | } 326 | }) 327 | }) 328 | } 329 | 330 | func BenchmarkAddingFields(b *testing.B) { 331 | b.Logf("Logging with additional context at each log site.") 332 | b.Run("apex/log", func(b *testing.B) { 333 | logger := newApexLog() 334 | b.ResetTimer() 335 | b.RunParallel(func(pb *testing.PB) { 336 | for pb.Next() { 337 | logger.WithFields(fakeApexFields()).Info(getMessage(0)) 338 | } 339 | }) 340 | }) 341 | b.Run("bep/logg", func(b *testing.B) { 342 | logger := newLoggLog() 343 | b.ResetTimer() 344 | b.RunParallel(func(pb *testing.PB) { 345 | for pb.Next() { 346 | message := logg.StringFunc(func() string { return getMessage(0) }) 347 | logger.WithFields(fakeLognFields()).Log(message) 348 | } 349 | }) 350 | }) 351 | b.Run("go-kit/kit/log", func(b *testing.B) { 352 | logger := newKitLog() 353 | b.ResetTimer() 354 | b.RunParallel(func(pb *testing.PB) { 355 | for pb.Next() { 356 | logger.Log(fakeSugarFields()...) 357 | } 358 | }) 359 | }) 360 | b.Run("inconshreveable/log15", func(b *testing.B) { 361 | logger := newLog15() 362 | b.ResetTimer() 363 | b.RunParallel(func(pb *testing.PB) { 364 | for pb.Next() { 365 | logger.Info(getMessage(0), fakeSugarFields()...) 366 | } 367 | }) 368 | }) 369 | b.Run("sirupsen/logrus", func(b *testing.B) { 370 | logger := newLogrus() 371 | b.ResetTimer() 372 | b.RunParallel(func(pb *testing.PB) { 373 | for pb.Next() { 374 | logger.WithFields(fakeLogrusFields()).Info(getMessage(0)) 375 | } 376 | }) 377 | }) 378 | b.Run("rs/zerolog", func(b *testing.B) { 379 | logger := newZerolog() 380 | b.ResetTimer() 381 | b.RunParallel(func(pb *testing.PB) { 382 | for pb.Next() { 383 | fakeZerologFields(logger.Info()).Msg(getMessage(0)) 384 | } 385 | }) 386 | }) 387 | b.Run("rs/zerolog.Check", func(b *testing.B) { 388 | logger := newZerolog() 389 | b.ResetTimer() 390 | b.RunParallel(func(pb *testing.PB) { 391 | for pb.Next() { 392 | if e := logger.Info(); e.Enabled() { 393 | fakeZerologFields(e).Msg(getMessage(0)) 394 | } 395 | } 396 | }) 397 | }) 398 | } 399 | -------------------------------------------------------------------------------- /benchmarks/zap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "time" 27 | 28 | "go.uber.org/multierr" 29 | "go.uber.org/zap" 30 | "go.uber.org/zap/zapcore" 31 | ) 32 | 33 | var ( 34 | errExample = errors.New("fail") 35 | 36 | _messages = fakeMessages(1000) 37 | _tenInts = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} 38 | _tenStrings = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} 39 | _tenTimes = []time.Time{ 40 | time.Unix(0, 0), 41 | time.Unix(1, 0), 42 | time.Unix(2, 0), 43 | time.Unix(3, 0), 44 | time.Unix(4, 0), 45 | time.Unix(5, 0), 46 | time.Unix(6, 0), 47 | time.Unix(7, 0), 48 | time.Unix(8, 0), 49 | time.Unix(9, 0), 50 | } 51 | _oneUser = &user{ 52 | Name: "Jane Doe", 53 | Email: "jane@test.com", 54 | CreatedAt: time.Date(1980, 1, 1, 12, 0, 0, 0, time.UTC), 55 | } 56 | _tenUsers = users{ 57 | _oneUser, 58 | _oneUser, 59 | _oneUser, 60 | _oneUser, 61 | _oneUser, 62 | _oneUser, 63 | _oneUser, 64 | _oneUser, 65 | _oneUser, 66 | _oneUser, 67 | } 68 | ) 69 | 70 | func fakeMessages(n int) []string { 71 | messages := make([]string, n) 72 | for i := range messages { 73 | messages[i] = fmt.Sprintf("Test logging, but use a somewhat realistic message length. (#%v)", i) 74 | } 75 | return messages 76 | } 77 | 78 | func getMessage(iter int) string { 79 | return _messages[iter%1000] 80 | } 81 | 82 | type users []*user 83 | 84 | func (uu users) MarshalLogArray(arr zapcore.ArrayEncoder) error { 85 | var err error 86 | for i := range uu { 87 | err = multierr.Append(err, arr.AppendObject(uu[i])) 88 | } 89 | return err 90 | } 91 | 92 | type user struct { 93 | Name string `json:"name"` 94 | Email string `json:"email"` 95 | CreatedAt time.Time `json:"created_at"` 96 | } 97 | 98 | func (u *user) MarshalLogObject(enc zapcore.ObjectEncoder) error { 99 | enc.AddString("name", u.Name) 100 | enc.AddString("email", u.Email) 101 | enc.AddInt64("createdAt", u.CreatedAt.UnixNano()) 102 | return nil 103 | } 104 | 105 | func fakeFields() []zap.Field { 106 | return []zap.Field{ 107 | zap.Int("int", _tenInts[0]), 108 | zap.Ints("ints", _tenInts), 109 | zap.String("string", _tenStrings[0]), 110 | zap.Strings("strings", _tenStrings), 111 | zap.Time("time", _tenTimes[0]), 112 | zap.Times("times", _tenTimes), 113 | zap.Object("user1", _oneUser), 114 | zap.Object("user2", _oneUser), 115 | zap.Array("users", _tenUsers), 116 | zap.Error(errExample), 117 | } 118 | } 119 | 120 | func fakeSugarFields() []interface{} { 121 | return []interface{}{ 122 | "int", _tenInts[0], 123 | "ints", _tenInts, 124 | "string", _tenStrings[0], 125 | "strings", _tenStrings, 126 | "time", _tenTimes[0], 127 | "times", _tenTimes, 128 | "user1", _oneUser, 129 | "user2", _oneUser, 130 | "users", _tenUsers, 131 | "error", errExample, 132 | } 133 | } 134 | 135 | func fakeFmtArgs() []interface{} { 136 | // Need to keep this a function instead of a package-global var so that we 137 | // pay the cast-to-interface{} penalty on each call. 138 | return []interface{}{ 139 | _tenInts[0], 140 | _tenInts, 141 | _tenStrings[0], 142 | _tenStrings, 143 | _tenTimes[0], 144 | _tenTimes, 145 | _oneUser, 146 | _oneUser, 147 | _tenUsers, 148 | errExample, 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /benchmarks/zerolog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package benchmarks 22 | 23 | import ( 24 | "io" 25 | 26 | "github.com/rs/zerolog" 27 | ) 28 | 29 | func newZerolog() zerolog.Logger { 30 | return zerolog.New(io.Discard).With().Timestamp().Logger() 31 | } 32 | 33 | func newDisabledZerolog() zerolog.Logger { 34 | return newZerolog().Level(zerolog.Disabled) 35 | } 36 | 37 | func (u *user) MarshalZerologObject(e *zerolog.Event) { 38 | e.Str("name", u.Name). 39 | Str("email", u.Email). 40 | Int64("createdAt", u.CreatedAt.UnixNano()) 41 | } 42 | 43 | func (uu users) MarshalZerologArray(a *zerolog.Array) { 44 | for _, u := range uu { 45 | a.Object(u) 46 | } 47 | } 48 | 49 | func fakeZerologFields(e *zerolog.Event) *zerolog.Event { 50 | return e. 51 | Int("int", _tenInts[0]). 52 | Ints("ints", _tenInts). 53 | Str("string", _tenStrings[0]). 54 | Strs("strings", _tenStrings). 55 | Time("time", _tenTimes[0]). 56 | Times("times", _tenTimes). 57 | Object("user1", _oneUser). 58 | Object("user2", _oneUser). 59 | Array("users", _tenUsers). 60 | Err(errExample) 61 | } 62 | 63 | func fakeZerologContext(c zerolog.Context) zerolog.Context { 64 | return c. 65 | Int("int", _tenInts[0]). 66 | Ints("ints", _tenInts). 67 | Str("string", _tenStrings[0]). 68 | Strs("strings", _tenStrings). 69 | Time("time", _tenTimes[0]). 70 | Times("times", _tenTimes). 71 | Object("user1", _oneUser). 72 | Object("user2", _oneUser). 73 | Array("users", _tenUsers). 74 | Err(errExample) 75 | } 76 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | package logg implements a simple structured logging API. 3 | */ 4 | package logg 5 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // assert interface compliance. 10 | var ( 11 | _ LevelLogger = (*Entry)(nil) 12 | ) 13 | 14 | // Entry represents a single log entry at a given log level. 15 | type Entry struct { 16 | logger *logger 17 | 18 | Level Level `json:"level"` 19 | Timestamp time.Time `json:"timestamp"` 20 | Fields Fields `json:"fields,omitempty"` 21 | Message string `json:"message"` 22 | 23 | fieldsAddedCounter int 24 | } 25 | 26 | // NewEntry returns a new entry for `log`. 27 | func NewEntry(log *logger) *Entry { 28 | return &Entry{ 29 | logger: log, 30 | } 31 | } 32 | 33 | func (e Entry) WithLevel(level Level) *Entry { 34 | e.Level = level 35 | return &e 36 | } 37 | 38 | func (e *Entry) WithFields(fielder Fielder) *Entry { 39 | if e.isLevelDisabled() { 40 | return e 41 | } 42 | x := *e 43 | fields := fielder.Fields() 44 | x.fieldsAddedCounter += len(fields) 45 | x.Fields = append(x.Fields, fields...) 46 | if x.fieldsAddedCounter > 100 { 47 | // This operation will eventually also be performed on the final entry, 48 | // do it here to avoid the slice to grow indefinitely. 49 | x.mergeFields() 50 | x.fieldsAddedCounter = 0 51 | } 52 | return &x 53 | } 54 | 55 | func (e *Entry) WithField(key string, value any) *Entry { 56 | if e.isLevelDisabled() { 57 | return e 58 | } 59 | return e.WithFields(Fields{{key, value}}) 60 | } 61 | 62 | func (e *Entry) WithDuration(d time.Duration) *Entry { 63 | if e.isLevelDisabled() { 64 | return e 65 | } 66 | return e.WithField("duration", d.Milliseconds()) 67 | } 68 | 69 | // WithError returns a new entry with the "error" set to `err`. 70 | // 71 | // The given error may implement .Fielder, if it does the method 72 | // will add all its `.Fields()` into the returned entry. 73 | func (e *Entry) WithError(err error) *Entry { 74 | if err == nil || e.isLevelDisabled() { 75 | return e 76 | } 77 | 78 | ctx := e.WithField("error", err.Error()) 79 | 80 | if s, ok := err.(stackTracer); ok { 81 | frame := s.StackTrace()[0] 82 | 83 | name := fmt.Sprintf("%n", frame) 84 | file := fmt.Sprintf("%+s", frame) 85 | line := fmt.Sprintf("%d", frame) 86 | 87 | parts := strings.Split(file, "\n\t") 88 | if len(parts) > 1 { 89 | file = parts[1] 90 | } 91 | 92 | ctx = ctx.WithField("source", fmt.Sprintf("%s: %s:%s", name, file, line)) 93 | } 94 | 95 | if f, ok := err.(Fielder); ok { 96 | ctx = ctx.WithFields(f) 97 | } 98 | 99 | return ctx 100 | } 101 | 102 | func (e *Entry) isLevelDisabled() bool { 103 | return e.Level < e.logger.Level 104 | } 105 | 106 | // Log a message at the given level. 107 | func (e *Entry) Log(s fmt.Stringer) { 108 | e.logger.log(e, s) 109 | } 110 | 111 | // Log a message at the given level. 112 | func (e *Entry) Logf(format string, a ...any) { 113 | e.logger.log(e, StringFunc(func() string { 114 | return fmt.Sprintf(format, a...) 115 | })) 116 | 117 | } 118 | 119 | // Clone returns a new Entry with the same fields. 120 | func (e *Entry) Clone() *Entry { 121 | x := *e 122 | x.Fields = make(Fields, len(e.Fields)) 123 | copy(x.Fields, e.Fields) 124 | return &x 125 | } 126 | 127 | func (e *Entry) reset() { 128 | e.logger = nil 129 | e.Level = 0 130 | e.Fields = e.Fields[:0] 131 | e.Message = "" 132 | e.Timestamp = time.Time{} 133 | } 134 | 135 | // Remove any early entries with the same name. 136 | func (e *Entry) mergeFields() { 137 | n := 0 138 | for i, f := range e.Fields { 139 | keep := true 140 | for j := i + 1; j < len(e.Fields); j++ { 141 | if e.Fields[j].Name == f.Name { 142 | keep = false 143 | break 144 | } 145 | } 146 | if keep { 147 | e.Fields[n] = f 148 | n++ 149 | } 150 | } 151 | e.Fields = e.Fields[:n] 152 | } 153 | 154 | // finalize populates dst with Level and Fields merged from e and Message and Timestamp set. 155 | func (e *Entry) finalize(dst *Entry, msg string) { 156 | dst.Message = msg 157 | dst.Timestamp = e.logger.Clock.Now() 158 | dst.Level = e.Level 159 | if cap(dst.Fields) < len(e.Fields) { 160 | dst.Fields = make(Fields, len(e.Fields)) 161 | } else { 162 | dst.Fields = dst.Fields[:len(e.Fields)] 163 | } 164 | copy(dst.Fields, e.Fields) 165 | dst.mergeFields() 166 | } 167 | -------------------------------------------------------------------------------- /entry_test.go: -------------------------------------------------------------------------------- 1 | package logg_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/bep/logg" 9 | "github.com/bep/logg/handlers" 10 | "github.com/bep/logg/handlers/memory" 11 | qt "github.com/frankban/quicktest" 12 | ) 13 | 14 | func TestEntry_WithFields(t *testing.T) { 15 | h := memory.New() 16 | a := logg.New(logg.Options{Handler: h, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 17 | 18 | b := a.WithFields(logg.Fields{{"foo", "bar"}}) 19 | 20 | c := a.WithFields(logg.Fields{{"foo", "hello"}, {"bar", "world"}}) 21 | d := c.WithFields(logg.Fields{{"baz", "jazz"}}) 22 | qt.Assert(t, b.Fields, qt.DeepEquals, logg.Fields{{"foo", "bar"}}) 23 | qt.Assert(t, c.Fields, qt.DeepEquals, logg.Fields{{"foo", "hello"}, {"bar", "world"}}) 24 | qt.Assert(t, d.Fields, qt.DeepEquals, logg.Fields{{"foo", "hello"}, {"bar", "world"}, {"baz", "jazz"}}) 25 | 26 | c.Log(logg.String("upload")) 27 | e := h.Entries[0] 28 | 29 | qt.Assert(t, "upload", qt.Equals, e.Message) 30 | qt.Assert(t, logg.Fields{{"foo", "hello"}, {"bar", "world"}}, qt.DeepEquals, e.Fields) 31 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 32 | qt.Assert(t, time.Now().IsZero(), qt.IsFalse) 33 | } 34 | 35 | func TestEntry_WithManyFieldsWithSameName(t *testing.T) { 36 | h := memory.New() 37 | a := logg.New(logg.Options{Handler: h, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 38 | 39 | b := a.WithFields(logg.Fields{{"foo", "bar"}}) 40 | 41 | for i := 0; i < 100; i++ { 42 | b = b.WithFields(logg.Fields{{"foo", "bar"}}) 43 | } 44 | 45 | b.Log(logg.String("upload")) 46 | e := h.Entries[0] 47 | 48 | qt.Assert(t, "upload", qt.Equals, e.Message) 49 | qt.Assert(t, logg.Fields{{"foo", "bar"}}, qt.DeepEquals, e.Fields) 50 | 51 | } 52 | 53 | func TestEntry_WithField(t *testing.T) { 54 | h := memory.New() 55 | a := logg.New(logg.Options{Handler: h, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 56 | b := a.WithField("foo", "baz").WithField("foo", "bar") 57 | b.Log(logg.String("upload")) 58 | qt.Assert(t, a.Fields, qt.IsNil) 59 | qt.Assert(t, h.Entries[0].Fields, qt.DeepEquals, logg.Fields{{"foo", "bar"}}) 60 | } 61 | 62 | func TestEntry_WithError(t *testing.T) { 63 | a := logg.New(logg.Options{Handler: handlers.Discard, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 64 | b := a.WithError(fmt.Errorf("boom")) 65 | qt.Assert(t, a.Fields, qt.IsNil) 66 | qt.Assert(t, b.Fields, qt.DeepEquals, logg.Fields{{"error", "boom"}}) 67 | } 68 | 69 | func TestEntry_WithError_fields(t *testing.T) { 70 | a := logg.New(logg.Options{Handler: handlers.Discard, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 71 | b := a.WithError(errFields("boom")) 72 | qt.Assert(t, a.Fields, qt.IsNil) 73 | qt.Assert(t, 74 | 75 | b.Fields, qt.DeepEquals, logg.Fields{ 76 | {"error", "boom"}, 77 | {"reason", "timeout"}, 78 | }) 79 | } 80 | 81 | func TestEntry_WithError_nil(t *testing.T) { 82 | a := logg.New(logg.Options{Handler: handlers.Discard, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 83 | b := a.WithError(nil) 84 | qt.Assert(t, a.Fields, qt.IsNil) 85 | qt.Assert(t, b.Fields, qt.IsNil) 86 | } 87 | 88 | func TestEntry_WithDuration(t *testing.T) { 89 | a := logg.New(logg.Options{Handler: handlers.Discard, Level: logg.LevelInfo}).WithLevel(logg.LevelInfo) 90 | b := a.WithDuration(time.Second * 2) 91 | qt.Assert(t, b.Fields, qt.DeepEquals, logg.Fields{{"duration", int64(2000)}}) 92 | } 93 | 94 | type errFields string 95 | 96 | func (ef errFields) Error() string { 97 | return string(ef) 98 | } 99 | 100 | func (ef errFields) Fields() logg.Fields { 101 | return logg.Fields{{"reason", "timeout"}} 102 | } 103 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package logg_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/bep/logg" 10 | "github.com/bep/logg/handlers/text" 11 | ) 12 | 13 | func Example() { 14 | var buff bytes.Buffer 15 | // Create a new logger. 16 | l := logg.New( 17 | logg.Options{ 18 | Level: logg.LevelInfo, 19 | Handler: text.New(&buff, text.Options{Separator: " "}), 20 | }, 21 | ) 22 | // Create a new log context. 23 | infoLogger := l.WithLevel(logg.LevelInfo) 24 | 25 | // Logg some user activity. 26 | userLogger := infoLogger.WithField("user", "foo").WithField("id", "123") 27 | userLogger.Log(logg.String("logged in")) 28 | userLogger.WithField("file", "jokes.txt").Log(logg.String("uploaded")) 29 | userLogger.WithField("file", "morejokes.txt").Log(logg.String("uploaded")) 30 | 31 | fmt.Print(buff.String()) 32 | 33 | // Output: 34 | // INFO logged in user=foo id=123 35 | // INFO uploaded user=foo id=123 file=jokes.txt 36 | // INFO uploaded user=foo id=123 file=morejokes.txt 37 | } 38 | 39 | func Example_lazy_evaluation() { 40 | var buff bytes.Buffer 41 | // Create a new logger. 42 | l := logg.New( 43 | logg.Options{ 44 | Level: logg.LevelError, 45 | Handler: text.New(&buff, text.Options{Separator: "|"}), 46 | }, 47 | ) 48 | 49 | errorLogger := l.WithLevel(logg.LevelError) 50 | 51 | // Info is below the logger's level, so 52 | // nothing will be printed. 53 | infoLogger := l.WithLevel(logg.LevelInfo) 54 | 55 | // Simulate a busy loop. 56 | for i := 0; i < 999; i++ { 57 | ctx := infoLogger.WithFields( 58 | logg.NewFieldsFunc( 59 | // This func will never be invoked with the current logger's level. 60 | func() logg.Fields { 61 | return logg.Fields{ 62 | {"field", strings.Repeat("x", 9999)}, 63 | } 64 | 65 | }), 66 | ) 67 | ctx.Log(logg.StringFunc( 68 | // This func will never be invoked with the current logger's level. 69 | func() string { 70 | return "log message: " + strings.Repeat("x", 9999) 71 | }, 72 | )) 73 | 74 | } 75 | 76 | errorLogger.WithDuration(32 * time.Second).Log(logg.String("something took too long")) 77 | 78 | fmt.Print(buff.String()) 79 | 80 | // Output: 81 | // ERROR|something took too long|duration=32000 82 | 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bep/logg 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bep/clocks v0.5.0 7 | github.com/fatih/color v1.13.0 8 | github.com/frankban/quicktest v1.14.3 9 | github.com/mattn/go-colorable v0.1.12 10 | github.com/pkg/errors v0.9.1 11 | ) 12 | 13 | require ( 14 | github.com/google/go-cmp v0.5.8 // indirect 15 | github.com/kr/pretty v0.3.0 // indirect 16 | github.com/kr/text v0.2.0 // indirect 17 | github.com/mattn/go-isatty v0.0.14 // indirect 18 | github.com/rogpeppe/go-internal v1.8.1 // indirect 19 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= 2 | github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 5 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 6 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 7 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 8 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 9 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 10 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 12 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 13 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 19 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 20 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 21 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 22 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 23 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 24 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 25 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 26 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 28 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 29 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 30 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= 35 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 39 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | // Handler is used to handle log events, outputting them to 4 | // stdio or sending them to remote services. See the "handlers" 5 | // directory for implementations. 6 | // 7 | // It is left up to Handlers to implement thread-safety. 8 | type Handler interface { 9 | // HandleLog is invoked for each log event. 10 | // Note that if the Entry is going to be used after the call to HandleLog 11 | // in the handler chain returns, it must be cloned with Clone(). See 12 | // the memory.Handler implementation for an example. 13 | // 14 | // The Entry can be modified if needed, e.g. when passed down via 15 | // a multi.Handler (e.g. to sanitize the data). 16 | HandleLog(e *Entry) error 17 | } 18 | 19 | // The HandlerFunc type is an adapter to allow the use of ordinary functions as 20 | // log handlers. If f is a function with the appropriate signature, 21 | // HandlerFunc(f) is a Handler object that calls f. 22 | type HandlerFunc func(*Entry) error 23 | 24 | // HandleLog calls f(e). 25 | func (f HandlerFunc) HandleLog(e *Entry) error { 26 | return f(e) 27 | } 28 | -------------------------------------------------------------------------------- /handlers/cli/cli.go: -------------------------------------------------------------------------------- 1 | // Package cli implements a colored text handler suitable for command-line interfaces. 2 | package cli 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "sync" 9 | 10 | "github.com/bep/logg" 11 | "github.com/fatih/color" 12 | colorable "github.com/mattn/go-colorable" 13 | ) 14 | 15 | // Default handler outputting to stderr. 16 | var Default = New(os.Stderr) 17 | 18 | var bold = color.New(color.Bold) 19 | 20 | // Colors mapping. 21 | var Colors = [...]*color.Color{ 22 | logg.LevelTrace: color.New(color.FgWhite), 23 | logg.LevelDebug: color.New(color.FgWhite), 24 | logg.LevelInfo: color.New(color.FgBlue), 25 | logg.LevelWarn: color.New(color.FgYellow), 26 | logg.LevelError: color.New(color.FgRed), 27 | } 28 | 29 | // Strings mapping. 30 | var Strings = [...]string{ 31 | logg.LevelTrace: "•", 32 | logg.LevelDebug: "•", 33 | logg.LevelInfo: "•", 34 | logg.LevelWarn: "•", 35 | logg.LevelError: "⨯", 36 | } 37 | 38 | // Handler implementation. 39 | type Handler struct { 40 | mu sync.Mutex 41 | Writer io.Writer 42 | Padding int 43 | } 44 | 45 | // New handler. 46 | func New(w io.Writer) *Handler { 47 | if f, ok := w.(*os.File); ok { 48 | return &Handler{ 49 | Writer: colorable.NewColorable(f), 50 | Padding: 3, 51 | } 52 | } 53 | 54 | return &Handler{ 55 | Writer: w, 56 | Padding: 3, 57 | } 58 | } 59 | 60 | // HandleLog implements logg.Handler. 61 | func (h *Handler) HandleLog(e *logg.Entry) error { 62 | color := Colors[e.Level] 63 | level := Strings[e.Level] 64 | 65 | h.mu.Lock() 66 | defer h.mu.Unlock() 67 | 68 | color.Fprintf(h.Writer, "%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message) 69 | 70 | for _, field := range e.Fields { 71 | if field.Name == "source" { 72 | continue 73 | } 74 | fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(field.Name), field.Value) 75 | } 76 | 77 | fmt.Fprintln(h.Writer) 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "github.com/bep/logg" 4 | 5 | // Discard is a no-op handler that discards all log messages. 6 | var Discard = logg.HandlerFunc(func(e *logg.Entry) error { 7 | return nil 8 | }) 9 | -------------------------------------------------------------------------------- /handlers/json/json.go: -------------------------------------------------------------------------------- 1 | // Package json implements a JSON handler. 2 | package json 3 | 4 | import ( 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/bep/logg" 9 | ) 10 | 11 | type Handler struct { 12 | w io.Writer 13 | } 14 | 15 | // New Handler implementation for JSON logging. 16 | // Eeach log Entry is written as a single JSON object, no more than one write to w. 17 | // The writer w should be safe for concurrent use by multiple 18 | // goroutines if the returned Handler will be used concurrently. 19 | func New(w io.Writer) *Handler { 20 | return &Handler{ 21 | w, 22 | } 23 | } 24 | 25 | // HandleLog implements logg.Handler. 26 | func (h *Handler) HandleLog(e *logg.Entry) error { 27 | enc := json.NewEncoder(h.w) 28 | enc.SetEscapeHTML(false) 29 | return enc.Encode(e) 30 | } 31 | -------------------------------------------------------------------------------- /handlers/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | 9 | "github.com/bep/clocks" 10 | "github.com/bep/logg" 11 | "github.com/bep/logg/handlers/json" 12 | ) 13 | 14 | func TestJSONHandler(t *testing.T) { 15 | var buf bytes.Buffer 16 | 17 | l := logg.New( 18 | logg.Options{ 19 | Level: logg.LevelInfo, 20 | Handler: json.New(&buf), 21 | Clock: clocks.Fixed(clocks.TimeCupFinalNorway1976), 22 | }) 23 | 24 | info := l.WithLevel(logg.LevelInfo) 25 | 26 | info.WithField("user", "tj").WithField("id", "123").Log(logg.String("hello")) 27 | info.Log(logg.String("world")) 28 | info.WithLevel(logg.LevelError).Log(logg.String("boom")) 29 | 30 | expected := "{\"level\":\"info\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"fields\":[{\"name\":\"user\",\"value\":\"tj\"},{\"name\":\"id\",\"value\":\"123\"}],\"message\":\"hello\"}\n{\"level\":\"info\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"world\"}\n{\"level\":\"error\",\"timestamp\":\"1976-10-24T12:15:02.127686412Z\",\"message\":\"boom\"}\n" 31 | 32 | qt.Assert(t, buf.String(), qt.Equals, expected) 33 | } 34 | -------------------------------------------------------------------------------- /handlers/level/level.go: -------------------------------------------------------------------------------- 1 | // Package level implements a level filter handler. 2 | package level 3 | 4 | import "github.com/bep/logg" 5 | 6 | // Handler implementation. 7 | type Handler struct { 8 | Level logg.Level 9 | Handler logg.Handler 10 | } 11 | 12 | // New handler. 13 | func New(h logg.Handler, level logg.Level) *Handler { 14 | return &Handler{ 15 | Level: level, 16 | Handler: h, 17 | } 18 | } 19 | 20 | // HandleLog implements logg.Handler. 21 | func (h *Handler) HandleLog(e *logg.Entry) error { 22 | if e.Level < h.Level { 23 | return nil 24 | } 25 | 26 | return h.Handler.HandleLog(e) 27 | } 28 | -------------------------------------------------------------------------------- /handlers/level/level_test.go: -------------------------------------------------------------------------------- 1 | package level_test 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | 8 | "github.com/bep/logg" 9 | "github.com/bep/logg/handlers/level" 10 | "github.com/bep/logg/handlers/memory" 11 | ) 12 | 13 | func TestLevel(t *testing.T) { 14 | h := memory.New() 15 | l := logg.New( 16 | logg.Options{Level: logg.LevelError, Handler: level.New(h, logg.LevelError)}, 17 | ) 18 | 19 | info := l.WithLevel(logg.LevelInfo) 20 | info.Log(logg.String("hello")) 21 | info.Log(logg.String("world")) 22 | info.WithLevel(logg.LevelError).Log(logg.String("boom")) 23 | 24 | qt.Assert(t, h.Entries, qt.HasLen, 1) 25 | qt.Assert(t, "boom", qt.Equals, h.Entries[0].Message) 26 | } 27 | -------------------------------------------------------------------------------- /handlers/memory/memory.go: -------------------------------------------------------------------------------- 1 | // Package memory implements an in-memory handler useful for testing, as the 2 | // entries can be accessed after writes. 3 | package memory 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/bep/logg" 9 | ) 10 | 11 | // Handler implementation. 12 | type Handler struct { 13 | mu sync.Mutex 14 | Entries []*logg.Entry 15 | } 16 | 17 | // New handler. 18 | func New() *Handler { 19 | return &Handler{} 20 | } 21 | 22 | // HandleLog implements logg.Handler. 23 | func (h *Handler) HandleLog(e *logg.Entry) error { 24 | h.mu.Lock() 25 | defer h.mu.Unlock() 26 | h.Entries = append(h.Entries, e.Clone()) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /handlers/multi/multi.go: -------------------------------------------------------------------------------- 1 | // Package multi implements a handler which invokes a number of handlers. 2 | package multi 3 | 4 | import ( 5 | "github.com/bep/logg" 6 | ) 7 | 8 | // Handler implementation. 9 | type Handler struct { 10 | Handlers []logg.Handler 11 | } 12 | 13 | // New handler. 14 | func New(h ...logg.Handler) *Handler { 15 | return &Handler{ 16 | Handlers: h, 17 | } 18 | } 19 | 20 | // HandleLog implements logg.Handler. 21 | func (h *Handler) HandleLog(e *logg.Entry) error { 22 | for _, handler := range h.Handlers { 23 | // TODO(tj): maybe just write to stderr here, definitely not ideal 24 | // to miss out logging to a more critical handler if something 25 | // goes wrong 26 | if err := handler.HandleLog(e); err != nil { 27 | return err 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /handlers/multi/multi_test.go: -------------------------------------------------------------------------------- 1 | package multi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bep/logg" 7 | "github.com/bep/logg/handlers/memory" 8 | "github.com/bep/logg/handlers/multi" 9 | qt "github.com/frankban/quicktest" 10 | ) 11 | 12 | func TestMulti(t *testing.T) { 13 | a := memory.New() 14 | b := memory.New() 15 | 16 | l := logg.New(logg.Options{ 17 | Level: logg.LevelInfo, 18 | Handler: multi.New(a, b), 19 | }) 20 | 21 | info := l.WithLevel(logg.LevelInfo) 22 | 23 | info.WithField("user", "tj").WithField("id", "123").Log(logg.String("hello")) 24 | info.Log(logg.String("world")) 25 | info.WithLevel(logg.LevelError).Log(logg.String("boom")) 26 | 27 | qt.Assert(t, a.Entries, qt.HasLen, 3) 28 | qt.Assert(t, b.Entries, qt.HasLen, 3) 29 | } 30 | 31 | func TestMultiModifyEntry(t *testing.T) { 32 | var a logg.HandlerFunc = func(e *logg.Entry) error { 33 | e.Message += "-modified" 34 | e.Fields = append(e.Fields, logg.Field{Name: "added", Value: "value"}) 35 | return nil 36 | } 37 | 38 | b := memory.New() 39 | 40 | l := logg.New( 41 | logg.Options{ 42 | Level: logg.LevelInfo, 43 | Handler: multi.New(a, b), 44 | }) 45 | 46 | l.WithLevel(logg.LevelInfo).WithField("initial", "value").Log(logg.String("text")) 47 | 48 | qt.Assert(t, b.Entries, qt.HasLen, 1) 49 | qt.Assert(t, b.Entries[0].Message, qt.Equals, "text-modified") 50 | qt.Assert(t, b.Entries[0].Fields, qt.HasLen, 2) 51 | qt.Assert(t, b.Entries[0].Fields[0].Name, qt.Equals, "initial") 52 | qt.Assert(t, b.Entries[0].Fields[1].Name, qt.Equals, "added") 53 | } 54 | 55 | func TestStopEntry(t *testing.T) { 56 | var a logg.HandlerFunc = func(e *logg.Entry) error { 57 | if e.Fields[0].Value == "v2" { 58 | return logg.ErrStopLogEntry 59 | } 60 | return nil 61 | } 62 | 63 | b := memory.New() 64 | 65 | l := logg.New( 66 | logg.Options{ 67 | Level: logg.LevelInfo, 68 | Handler: multi.New(a, b), 69 | }) 70 | 71 | l.WithLevel(logg.LevelInfo).WithField("v", "v1").Log(logg.String("text")) 72 | l.WithLevel(logg.LevelInfo).WithField("v", "v2").Log(logg.String("text")) 73 | l.WithLevel(logg.LevelInfo).WithField("v", "v3").Log(logg.String("text")) 74 | 75 | qt.Assert(t, b.Entries, qt.HasLen, 2) 76 | } 77 | -------------------------------------------------------------------------------- /handlers/text/text.go: -------------------------------------------------------------------------------- 1 | // Package text implements a development-friendly textual handler. 2 | package text 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/bep/logg" 11 | ) 12 | 13 | // Default handler outputting to stderr. 14 | var Default = New(os.Stderr, Options{}) 15 | 16 | // Handler implementation. 17 | type Handler struct { 18 | opts Options 19 | w io.Writer 20 | } 21 | 22 | // Options holds options for the text handler. 23 | type Options struct { 24 | // Separator is the separator between fields. 25 | // Default is " ". 26 | Separator string 27 | } 28 | 29 | // New handler. 30 | func New(w io.Writer, opts Options) *Handler { 31 | if opts.Separator == "" { 32 | opts.Separator = " " 33 | } 34 | return &Handler{ 35 | w: w, 36 | opts: opts, 37 | } 38 | } 39 | 40 | // HandleLog implements logg.Handler. 41 | func (h *Handler) HandleLog(e *logg.Entry) error { 42 | fields := make([]string, len(e.Fields)) 43 | for i, f := range e.Fields { 44 | fields[i] = fmt.Sprintf("%s=%v", f.Name, f.Value) 45 | } 46 | 47 | fmt.Fprintf(h.w, "%s%s%s%s%s\n", strings.ToUpper(e.Level.String()), h.opts.Separator, e.Message, h.opts.Separator, strings.Join(fields, h.opts.Separator)) 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /handlers/text/text_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | 9 | "github.com/bep/logg" 10 | "github.com/bep/logg/handlers/text" 11 | ) 12 | 13 | func TestTextHandler(t *testing.T) { 14 | var buf bytes.Buffer 15 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: text.New(&buf, text.Options{Separator: "|"})}) 16 | info := l.WithLevel(logg.LevelInfo) 17 | 18 | info.WithField("user", "tj").WithField("id", "123").Log(logg.String("hello")) 19 | info.WithField("user", "tj").Log(logg.String("world")) 20 | info.WithField("user", "tj").WithLevel(logg.LevelError).Log(logg.String("boom")) 21 | 22 | expected := "INFO|hello|user=tj|id=123\nINFO|world|user=tj\nERROR|boom|user=tj\n" 23 | 24 | qt.Assert(t, buf.String(), qt.Equals, expected) 25 | } 26 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Logger is the main interface for the logger. 9 | type Logger interface { 10 | // WithLevel returns a new entry with `level` set. 11 | WithLevel(Level) *Entry 12 | } 13 | 14 | // LevelLogger is the logger at a given level. 15 | type LevelLogger interface { 16 | // Log logs a message at the given level using the string from calling s.String(). 17 | // Note that s.String() will not be called if the level is not enabled. 18 | Log(s fmt.Stringer) 19 | 20 | // Logf logs a message at the given level using the format and args from calling fmt.Sprintf(). 21 | // Note that fmt.Sprintf() will not be called if the level is not enabled. 22 | Logf(format string, a ...any) 23 | 24 | // WithLevel returns a new entry with `level` set. 25 | WithLevel(Level) *Entry 26 | 27 | // WithFields returns a new entry with the`fields` in fields set. 28 | // This is a noop if LevelLogger's level is less than Logger's. 29 | WithFields(fields Fielder) *Entry 30 | 31 | // WithLevel returns a new entry with the field f set with value v 32 | // This is a noop if LevelLogger's level is less than Logger's. 33 | WithField(f string, v any) *Entry 34 | 35 | // WithDuration returns a new entry with the "duration" field set 36 | // to the given duration in milliseconds. 37 | // This is a noop if LevelLogger's level is less than Logger's. 38 | WithDuration(time.Duration) *Entry 39 | 40 | // WithError returns a new entry with the "error" set to `err`. 41 | // This is a noop if err is nil or LevelLogger's level is less than Logger's. 42 | WithError(error) *Entry 43 | } 44 | -------------------------------------------------------------------------------- /levels.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | // ErrInvalidLevel is returned if the severity level is invalid. 10 | var ErrInvalidLevel = errors.New("invalid level") 11 | 12 | // Level of severity. 13 | type Level int 14 | 15 | // Log levels. 16 | const ( 17 | LevelInvalid Level = iota 18 | LevelTrace 19 | LevelDebug 20 | LevelInfo 21 | LevelWarn 22 | LevelError 23 | ) 24 | 25 | var levelNames = [...]string{ 26 | LevelTrace: "trace", 27 | LevelDebug: "debug", 28 | LevelInfo: "info", 29 | LevelWarn: "warn", 30 | LevelError: "error", 31 | } 32 | 33 | var levelStrings = map[string]Level{ 34 | "trace": LevelTrace, 35 | "debug": LevelDebug, 36 | "info": LevelInfo, 37 | "warn": LevelWarn, 38 | "warning": LevelWarn, 39 | "error": LevelError, 40 | } 41 | 42 | // String implementation. 43 | func (l Level) String() string { 44 | return levelNames[l] 45 | } 46 | 47 | // MarshalJSON implementation. 48 | func (l Level) MarshalJSON() ([]byte, error) { 49 | return []byte(`"` + l.String() + `"`), nil 50 | } 51 | 52 | // UnmarshalJSON implementation. 53 | func (l *Level) UnmarshalJSON(b []byte) error { 54 | v, err := ParseLevel(string(bytes.Trim(b, `"`))) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | *l = v 60 | return nil 61 | } 62 | 63 | // ParseLevel parses level string. 64 | func ParseLevel(s string) (Level, error) { 65 | l, ok := levelStrings[strings.ToLower(s)] 66 | if !ok { 67 | return LevelInvalid, ErrInvalidLevel 68 | } 69 | 70 | return l, nil 71 | } 72 | 73 | // MustParseLevel parses level string or panics. 74 | func MustParseLevel(s string) Level { 75 | l, err := ParseLevel(s) 76 | if err != nil { 77 | panic("invalid log level") 78 | } 79 | 80 | return l 81 | } 82 | -------------------------------------------------------------------------------- /levels_test.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | ) 9 | 10 | func TestParseLevel(t *testing.T) { 11 | cases := []struct { 12 | String string 13 | Level Level 14 | }{ 15 | {"trace", LevelTrace}, 16 | {"debug", LevelDebug}, 17 | {"info", LevelInfo}, 18 | {"warn", LevelWarn}, 19 | {"warning", LevelWarn}, 20 | {"error", LevelError}, 21 | } 22 | 23 | for _, c := range cases { 24 | t.Run(c.String, func(t *testing.T) { 25 | l, err := ParseLevel(c.String) 26 | qt.Assert(t, err, qt.IsNil, qt.Commentf("parse")) 27 | qt.Assert(t, l, qt.Equals, c.Level) 28 | }) 29 | } 30 | 31 | t.Run("invalid", func(t *testing.T) { 32 | l, err := ParseLevel("something") 33 | qt.Assert(t, err, qt.Equals, ErrInvalidLevel) 34 | qt.Assert(t, l, qt.Equals, LevelInvalid) 35 | }) 36 | } 37 | 38 | func TestLevel_MarshalJSON(t *testing.T) { 39 | e := Entry{ 40 | Message: "hello", 41 | Level: LevelInfo, 42 | } 43 | 44 | expect := `{"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` 45 | 46 | b, err := json.Marshal(e) 47 | qt.Assert(t, err, qt.IsNil) 48 | qt.Assert(t, string(b), qt.Equals, expect) 49 | } 50 | 51 | func TestLevel_UnmarshalJSON(t *testing.T) { 52 | s := `{"fields":[],"level":"info","timestamp":"0001-01-01T00:00:00Z","message":"hello"}` 53 | e := new(Entry) 54 | 55 | err := json.Unmarshal([]byte(s), e) 56 | qt.Assert(t, err, qt.IsNil) 57 | qt.Assert(t, e.Level, qt.Equals, LevelInfo) 58 | } 59 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import ( 4 | "fmt" 5 | stdlog "log" 6 | "time" 7 | 8 | "github.com/bep/clocks" 9 | ) 10 | 11 | // assert interface compliance. 12 | var _ Logger = (*logger)(nil) 13 | 14 | // String implements fmt.Stringer and can be used directly in 15 | // the log methods. 16 | type String string 17 | 18 | // StringFunc is a function that returns a string. 19 | // It also implements the fmt.Stringer interface and 20 | // can therefore be used as argument to the log methods. 21 | type StringFunc func() string 22 | 23 | func (f StringFunc) String() string { 24 | return f() 25 | } 26 | 27 | func (s String) String() string { 28 | return string(s) 29 | } 30 | 31 | // Fielder is an interface for providing fields to custom types. 32 | type Fielder interface { 33 | Fields() Fields 34 | } 35 | 36 | func NewFieldsFunc(fn func() Fields) FieldsFunc { 37 | return FieldsFunc(fn) 38 | } 39 | 40 | type FieldsFunc func() Fields 41 | 42 | func (f FieldsFunc) Fields() Fields { 43 | return f() 44 | } 45 | 46 | // Field holds a named value. 47 | type Field struct { 48 | Name string `json:"name"` 49 | Value any `json:"value"` 50 | } 51 | 52 | // Fields represents a slice of entry level data used for structured logging. 53 | type Fields []Field 54 | 55 | // Fields implements Fielder. 56 | func (f Fields) Fields() Fields { 57 | return f 58 | } 59 | 60 | // Options is the set of options used to configure a logger. 61 | type Options struct { 62 | // Level is the minimum level to log at. 63 | // If not set, defaults to InfoLevel. 64 | Level Level 65 | 66 | // Handler is the log handler to use. 67 | Handler Handler 68 | 69 | // Clock is the clock to use for timestamps. 70 | // If not set, the system clock is used. 71 | Clock Clock 72 | } 73 | 74 | // New returns a new logger. 75 | func New(cfg Options) Logger { 76 | if cfg.Handler == nil { 77 | panic("handler cannot be nil") 78 | } 79 | 80 | if cfg.Level <= 0 || cfg.Level > LevelError { 81 | panic("log level is out of range") 82 | } 83 | 84 | if cfg.Clock == nil { 85 | cfg.Clock = clocks.System() 86 | } 87 | 88 | if cfg.Level == 0 { 89 | cfg.Level = LevelInfo 90 | } 91 | 92 | return &logger{ 93 | Handler: cfg.Handler, 94 | Level: cfg.Level, 95 | Clock: cfg.Clock, 96 | } 97 | } 98 | 99 | // logger represents a logger with configurable Level and Handler. 100 | type logger struct { 101 | Handler Handler 102 | Level Level 103 | Clock Clock 104 | } 105 | 106 | // Clock provides the current time. 107 | type Clock interface { 108 | Now() time.Time 109 | } 110 | 111 | // WithLevel returns a new entry with `level` set. 112 | func (l *logger) WithLevel(level Level) *Entry { 113 | return NewEntry(l).WithLevel(level) 114 | } 115 | 116 | // WithFields returns a new entry with `fields` set. 117 | func (l *logger) WithFields(fields Fielder) *Entry { 118 | return NewEntry(l).WithFields(fields.Fields()) 119 | } 120 | 121 | // WithField returns a new entry with the `key` and `value` set. 122 | // 123 | // Note that the `key` should not have spaces in it - use camel 124 | // case or underscores 125 | func (l *logger) WithField(key string, value any) *Entry { 126 | return NewEntry(l).WithField(key, value) 127 | } 128 | 129 | // WithDuration returns a new entry with the "duration" field set 130 | // to the given duration in milliseconds. 131 | func (l *logger) WithDuration(d time.Duration) *Entry { 132 | return NewEntry(l).WithDuration(d) 133 | } 134 | 135 | // WithError returns a new entry with the "error" set to `err`. 136 | func (l *logger) WithError(err error) *Entry { 137 | return NewEntry(l).WithError(err) 138 | } 139 | 140 | // ErrStopLogEntry is a sentinel error that can be returned from a 141 | // handler to stop the entry from being passed to the next handler. 142 | var ErrStopLogEntry = fmt.Errorf("stop log entry") 143 | 144 | // log the message, invoking the handler. 145 | func (l *logger) log(e *Entry, s fmt.Stringer) { 146 | if e.Level < l.Level { 147 | return 148 | } 149 | 150 | finalized := objectPools.GetEntry() 151 | defer objectPools.PutEntry(finalized) 152 | e.finalize(finalized, s.String()) 153 | 154 | if err := l.Handler.HandleLog(finalized); err != nil { 155 | if err != ErrStopLogEntry { 156 | stdlog.Printf("error logging: %s", err) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package logg_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/bep/logg" 9 | "github.com/bep/logg/handlers" 10 | "github.com/bep/logg/handlers/memory" 11 | qt "github.com/frankban/quicktest" 12 | ) 13 | 14 | func TestLogger_Log(t *testing.T) { 15 | h := memory.New() 16 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: h}) 17 | a := l.WithLevel(logg.LevelInfo) 18 | 19 | a.Log(logg.String("logged in Tobi")) 20 | 21 | e := h.Entries[0] 22 | qt.Assert(t, "logged in Tobi", qt.Equals, e.Message) 23 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 24 | } 25 | 26 | func TestLogger_Logf(t *testing.T) { 27 | h := memory.New() 28 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: h}) 29 | a := l.WithLevel(logg.LevelInfo) 30 | 31 | a.Logf("logged in %s", "Tobi") 32 | 33 | e := h.Entries[0] 34 | qt.Assert(t, "logged in Tobi", qt.Equals, e.Message) 35 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 36 | } 37 | 38 | func TestLogger_levels(t *testing.T) { 39 | h := memory.New() 40 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: h}) 41 | 42 | l.WithLevel(logg.LevelTrace).Log(logg.String("uploading")) 43 | l.WithLevel(logg.LevelDebug).Log(logg.String("uploading")) 44 | l.WithLevel(logg.LevelInfo).Log(logg.String("upload complete")) 45 | 46 | qt.Assert(t, len(h.Entries), qt.Equals, 1) 47 | 48 | e := h.Entries[0] 49 | qt.Assert(t, "upload complete", qt.Equals, e.Message) 50 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 51 | } 52 | 53 | func TestLogger_WithFields(t *testing.T) { 54 | h := memory.New() 55 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: h}) 56 | 57 | info := l.WithLevel(logg.LevelInfo).WithFields(logg.Fields{{"file", "sloth.png"}}) 58 | info.WithLevel(logg.LevelDebug).Log(logg.String("uploading")) 59 | info.Log(logg.String("upload complete")) 60 | 61 | qt.Assert(t, len(h.Entries), qt.Equals, 1) 62 | 63 | e := h.Entries[0] 64 | qt.Assert(t, "upload complete", qt.Equals, e.Message) 65 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 66 | qt.Assert(t, e.Fields, qt.DeepEquals, logg.Fields{{"file", "sloth.png"}}) 67 | } 68 | 69 | func TestLogger_WithField(t *testing.T) { 70 | h := memory.New() 71 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: h}) 72 | 73 | info := l.WithLevel(logg.LevelInfo).WithField("file", "sloth.png").WithField("user", "Tobi") 74 | info.WithLevel(logg.LevelDebug).Log(logg.String("uploading")) 75 | info.Log(logg.String("upload complete")) 76 | 77 | qt.Assert(t, len(h.Entries), qt.Equals, 1) 78 | 79 | e := h.Entries[0] 80 | qt.Assert(t, "upload complete", qt.Equals, e.Message) 81 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 82 | qt.Assert(t, e.Fields, qt.DeepEquals, logg.Fields{{"file", "sloth.png"}, {"user", "Tobi"}}) 83 | } 84 | 85 | func TestLogger_HandlerFunc(t *testing.T) { 86 | h := memory.New() 87 | f := func(e *logg.Entry) error { 88 | return h.HandleLog(e) 89 | } 90 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: logg.HandlerFunc(f)}) 91 | info := l.WithLevel(logg.LevelInfo) 92 | 93 | info.Log(logg.String("logged in Tobi")) 94 | 95 | e := h.Entries[0] 96 | qt.Assert(t, "logged in Tobi", qt.Equals, e.Message) 97 | qt.Assert(t, logg.LevelInfo, qt.Equals, e.Level) 98 | } 99 | 100 | func BenchmarkLogger_small(b *testing.B) { 101 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 102 | info := l.WithLevel(logg.LevelInfo) 103 | 104 | for i := 0; i < b.N; i++ { 105 | info.Log(logg.String("login")) 106 | } 107 | } 108 | 109 | func BenchmarkLogger_medium(b *testing.B) { 110 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 111 | info := l.WithLevel(logg.LevelInfo) 112 | 113 | for i := 0; i < b.N; i++ { 114 | info.WithFields(logg.Fields{ 115 | {"file", "sloth.png"}, 116 | {"type", "image/png"}, 117 | {"size", 1 << 20}, 118 | }).Log(logg.String("upload")) 119 | } 120 | } 121 | 122 | func BenchmarkLogger_large(b *testing.B) { 123 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 124 | info := l.WithLevel(logg.LevelInfo) 125 | 126 | err := fmt.Errorf("boom") 127 | 128 | for i := 0; i < b.N; i++ { 129 | info.WithFields(logg.Fields{ 130 | {"file", "sloth.png"}, 131 | {"type", "image/png"}, 132 | {"size", 1 << 20}, 133 | }). 134 | WithFields(logg.Fields{ 135 | {"some", "more"}, 136 | {"data", "here"}, 137 | {"whatever", "blah blah"}, 138 | {"more", "stuff"}, 139 | {"context", "such useful"}, 140 | {"much", "fun"}, 141 | }). 142 | WithError(err).Log(logg.String("upload failed")) 143 | } 144 | } 145 | 146 | func BenchmarkLogger_common_context(b *testing.B) { 147 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 148 | info := l.WithLevel(logg.LevelInfo) 149 | for i := 0; i < 3; i++ { 150 | info = info.WithField(fmt.Sprintf("context%d", i), "value") 151 | } 152 | b.ResetTimer() 153 | for i := 0; i < b.N; i++ { 154 | info.Log(logg.String("upload")) 155 | } 156 | } 157 | 158 | func BenchmarkLogger_common_context_many_fields(b *testing.B) { 159 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 160 | info := l.WithLevel(logg.LevelInfo) 161 | for i := 0; i < 42; i++ { 162 | info = info.WithField(fmt.Sprintf("context%d", i), "value") 163 | } 164 | b.ResetTimer() 165 | for i := 0; i < b.N; i++ { 166 | info.Log(logg.String("upload")) 167 | } 168 | } 169 | 170 | func BenchmarkLogger_context_many_fields_duplicate_names_with_field(b *testing.B) { 171 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 172 | 173 | b.ResetTimer() 174 | for i := 0; i < b.N; i++ { 175 | info := l.WithLevel(logg.LevelInfo) 176 | for i := 0; i < 9999; i++ { 177 | info = info.WithField("name", "value") 178 | } 179 | info.Log(logg.String("upload")) 180 | } 181 | } 182 | 183 | func BenchmarkLogger_context_many_fields_duplicate_names_with_fields(b *testing.B) { 184 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 185 | 186 | b.ResetTimer() 187 | for i := 0; i < b.N; i++ { 188 | info := l.WithLevel(logg.LevelInfo) 189 | for i := 0; i < 3333; i++ { 190 | info = info.WithFields(logg.Fields{{"name", "value"}, {"name", "value"}, {"name", "value"}}) 191 | } 192 | info.Log(logg.String("upload")) 193 | } 194 | } 195 | 196 | func BenchmarkLogger_levels(b *testing.B) { 197 | doWork := func(l logg.LevelLogger) { 198 | for i := 0; i < 10; i++ { 199 | l.Log(logg.StringFunc( 200 | func() string { 201 | return fmt.Sprintf("loging value %s and %s.", "value1", strings.Repeat("value2", i+1)) 202 | }, 203 | )) 204 | } 205 | } 206 | 207 | b.Run("level not met", func(b *testing.B) { 208 | for i := 0; i < b.N; i++ { 209 | l := logg.New(logg.Options{Level: logg.LevelError, Handler: handlers.Discard}) 210 | error := l.WithLevel(logg.LevelInfo) 211 | doWork(error) 212 | } 213 | }) 214 | 215 | b.Run("level not met, one field", func(b *testing.B) { 216 | for i := 0; i < b.N; i++ { 217 | l := logg.New(logg.Options{Level: logg.LevelError, Handler: handlers.Discard}) 218 | info := l.WithLevel(logg.LevelInfo) 219 | info = info.WithField("file", "sloth.png") 220 | doWork(info) 221 | } 222 | }) 223 | 224 | b.Run("level not met, many fields", func(b *testing.B) { 225 | for i := 0; i < b.N; i++ { 226 | l := logg.New(logg.Options{Level: logg.LevelError, Handler: handlers.Discard}) 227 | info := l.WithLevel(logg.LevelInfo) 228 | info = info.WithField("file", "sloth.png") 229 | for i := 0; i < 32; i++ { 230 | info = info.WithField(fmt.Sprintf("field%d", i), "value") 231 | } 232 | doWork(info) 233 | } 234 | }) 235 | 236 | b.Run("level met", func(b *testing.B) { 237 | for i := 0; i < b.N; i++ { 238 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 239 | info := l.WithLevel(logg.LevelInfo) 240 | for j := 0; j < 10; j++ { 241 | doWork(info) 242 | } 243 | } 244 | }) 245 | 246 | b.Run("level met, one field", func(b *testing.B) { 247 | for i := 0; i < b.N; i++ { 248 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 249 | info := l.WithLevel(logg.LevelInfo) 250 | info = info.WithField("file", "sloth.png") 251 | doWork(info) 252 | } 253 | }) 254 | 255 | b.Run("level met, many fields", func(b *testing.B) { 256 | for i := 0; i < b.N; i++ { 257 | l := logg.New(logg.Options{Level: logg.LevelInfo, Handler: handlers.Discard}) 258 | info := l.WithLevel(logg.LevelInfo) 259 | info = info.WithField("file", "sloth.png") 260 | for i := 0; i < 32; i++ { 261 | info = info.WithField(fmt.Sprintf("field%d", i), "value") 262 | } 263 | doWork(info) 264 | } 265 | }) 266 | } 267 | -------------------------------------------------------------------------------- /objectpools.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import "sync" 4 | 5 | var objectPools = &objectPoolsHolder{ 6 | entryPool: &sync.Pool{ 7 | New: func() any { 8 | return &Entry{} 9 | }, 10 | }, 11 | } 12 | 13 | type objectPoolsHolder struct { 14 | // This is only used for the event copy passed to HandleLog. 15 | entryPool *sync.Pool 16 | } 17 | 18 | func (h *objectPoolsHolder) GetEntry() *Entry { 19 | return h.entryPool.Get().(*Entry) 20 | } 21 | 22 | func (h *objectPoolsHolder) PutEntry(e *Entry) { 23 | e.reset() 24 | h.entryPool.Put(e) 25 | } 26 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package logg 2 | 3 | import "github.com/pkg/errors" 4 | 5 | // stackTracer interface. 6 | type stackTracer interface { 7 | StackTrace() errors.StackTrace 8 | } 9 | --------------------------------------------------------------------------------