├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmark_test.go ├── concurrency_test.go ├── doc.go ├── example_test.go ├── go.mod ├── go.sum ├── json_logger.go ├── json_logger_test.go ├── level ├── benchmark_test.go ├── doc.go ├── example_test.go ├── level.go └── level_test.go ├── log.go ├── log_test.go ├── logfmt_logger.go ├── logfmt_logger_test.go ├── nop_logger.go ├── nop_logger_test.go ├── staticcheck.conf ├── stdlib.go ├── stdlib_test.go ├── sync.go ├── sync_test.go ├── syslog ├── example_test.go ├── syslog.go └── syslog_test.go ├── term ├── LICENSE ├── colorlogger.go ├── colorlogger_test.go ├── colorwriter_others.go ├── colorwriter_windows.go ├── example_test.go ├── term.go ├── terminal_darwin.go ├── terminal_freebsd.go ├── terminal_linux.go ├── terminal_notwindows.go ├── terminal_openbsd.go ├── terminal_stub.go ├── terminal_windows.go └── terminal_windows_test.go ├── value.go └── value_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | types: [synchronize] 5 | name: Test 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | go-version: [1.19.x, 1.20.x] 12 | platform: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.platform }} 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Install Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Fmt 22 | if: matrix.platform != 'windows-latest' # :( 23 | run: "diff <(gofmt -d .) <(printf '')" 24 | shell: bash 25 | - name: Vet 26 | run: go vet ./... 27 | - name: Staticcheck 28 | uses: dominikh/staticcheck-action@v1 29 | with: 30 | install-go: false 31 | cache-key: ${{ matrix.go-version }} 32 | - name: Test 33 | run: go test -race ./... 34 | - name: Test coverage 35 | run: go test -coverprofile="cover.out" ./... # quotes needed for powershell 36 | - name: Send coverage 37 | uses: shogo82148/actions-goveralls@v1 38 | with: 39 | path-to-profile: cover.out 40 | flag-name: go${{ matrix.go-version }}-${{ matrix.os }} 41 | parallel: true 42 | # notifies that all test jobs are finished. 43 | finish: 44 | needs: test 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: shogo82148/actions-goveralls@v1 48 | with: 49 | parallel-finished: true 50 | -------------------------------------------------------------------------------- /.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/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Go kit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # package log 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-kit/log.svg)](https://pkg.go.dev/github.com/go-kit/log) 4 | [![Go Report Card](https://goreportcard.com/badge/go-kit/log)](https://goreportcard.com/report/go-kit/log) 5 | [![GitHub Actions](https://github.com/go-kit/log/actions/workflows/test.yml/badge.svg)](https://github.com/go-kit/log/actions/workflows/test.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/go-kit/log/badge.svg?branch=main)](https://coveralls.io/github/go-kit/log?branch=main) 7 | 8 | `package log` provides a minimal interface for structured logging in services. 9 | It may be wrapped to encode conventions, enforce type-safety, provide leveled 10 | logging, and so on. It can be used for both typical application log events, 11 | and log-structured data streams. 12 | 13 | ## Structured logging 14 | 15 | Structured logging is, basically, conceding to the reality that logs are 16 | _data_, and warrant some level of schematic rigor. Using a stricter, 17 | key/value-oriented message format for our logs, containing contextual and 18 | semantic information, makes it much easier to get insight into the 19 | operational activity of the systems we build. Consequently, `package log` is 20 | of the strong belief that structured logging offers great benefits in exchange 21 | for minimal effort. 22 | 23 | Migrating from unstructured to structured logging is probably a lot easier 24 | than you'd expect. 25 | 26 | ```go 27 | // Unstructured 28 | log.Printf("HTTP server listening on %s", addr) 29 | 30 | // Structured 31 | logger.Log("transport", "HTTP", "addr", addr, "msg", "listening") 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Typical application logging 37 | 38 | ```go 39 | w := log.NewSyncWriter(os.Stderr) 40 | logger := log.NewLogfmtLogger(w) 41 | logger.Log("question", "what is the meaning of life?", "answer", 42) 42 | 43 | // Output: 44 | // question="what is the meaning of life?" answer=42 45 | ``` 46 | 47 | ### Contextual Loggers 48 | 49 | ```go 50 | func main() { 51 | var logger log.Logger 52 | logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 53 | logger = log.With(logger, "instance_id", 123) 54 | 55 | logger.Log("msg", "starting") 56 | NewWorker(log.With(logger, "component", "worker")).Run() 57 | NewSlacker(log.With(logger, "component", "slacker")).Run() 58 | } 59 | 60 | // Output: 61 | // instance_id=123 msg=starting 62 | // instance_id=123 component=worker msg=running 63 | // instance_id=123 component=slacker msg=running 64 | ``` 65 | 66 | ### Interact with stdlib logger 67 | 68 | Redirect stdlib logger to Go kit logger. 69 | 70 | ```go 71 | import ( 72 | "os" 73 | stdlog "log" 74 | kitlog "github.com/go-kit/log" 75 | ) 76 | 77 | func main() { 78 | logger := kitlog.NewJSONLogger(kitlog.NewSyncWriter(os.Stdout)) 79 | stdlog.SetOutput(kitlog.NewStdlibAdapter(logger)) 80 | stdlog.Print("I sure like pie") 81 | } 82 | 83 | // Output: 84 | // {"msg":"I sure like pie","ts":"2016/01/01 12:34:56"} 85 | ``` 86 | 87 | Or, if, for legacy reasons, you need to pipe all of your logging through the 88 | stdlib log package, you can redirect Go kit logger to the stdlib logger. 89 | 90 | ```go 91 | logger := kitlog.NewLogfmtLogger(kitlog.StdlibWriter{}) 92 | logger.Log("legacy", true, "msg", "at least it's something") 93 | 94 | // Output: 95 | // 2016/01/01 12:34:56 legacy=true msg="at least it's something" 96 | ``` 97 | 98 | ### Timestamps and callers 99 | 100 | ```go 101 | var logger log.Logger 102 | logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 103 | logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) 104 | 105 | logger.Log("msg", "hello") 106 | 107 | // Output: 108 | // ts=2016-01-01T12:34:56Z caller=main.go:15 msg=hello 109 | ``` 110 | 111 | ## Levels 112 | 113 | Log levels are supported via the [level package](https://godoc.org/github.com/go-kit/log/level). 114 | 115 | ## Supported output formats 116 | 117 | - [Logfmt](https://brandur.org/logfmt) ([see also](https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write)) 118 | - JSON 119 | 120 | ## Enhancements 121 | 122 | `package log` is centered on the one-method Logger interface. 123 | 124 | ```go 125 | type Logger interface { 126 | Log(keyvals ...interface{}) error 127 | } 128 | ``` 129 | 130 | This interface, and its supporting code like is the product of much iteration 131 | and evaluation. For more details on the evolution of the Logger interface, 132 | see [The Hunt for a Logger Interface](http://go-talks.appspot.com/github.com/ChrisHines/talks/structured-logging/structured-logging.slide#1), 133 | a talk by [Chris Hines](https://github.com/ChrisHines). 134 | Also, please see 135 | [#63](https://github.com/go-kit/kit/issues/63), 136 | [#76](https://github.com/go-kit/kit/pull/76), 137 | [#131](https://github.com/go-kit/kit/issues/131), 138 | [#157](https://github.com/go-kit/kit/pull/157), 139 | [#164](https://github.com/go-kit/kit/issues/164), and 140 | [#252](https://github.com/go-kit/kit/pull/252) 141 | to review historical conversations about package log and the Logger interface. 142 | 143 | Value-add packages and suggestions, 144 | like improvements to [the leveled logger](https://godoc.org/github.com/go-kit/log/level), 145 | are of course welcome. Good proposals should 146 | 147 | - Be composable with [contextual loggers](https://godoc.org/github.com/go-kit/log#With), 148 | - Not break the behavior of [log.Caller](https://godoc.org/github.com/go-kit/log#Caller) in any wrapped contextual loggers, and 149 | - Be friendly to packages that accept only an unadorned log.Logger. 150 | 151 | ## Benchmarks & comparisons 152 | 153 | There are a few Go logging benchmarks and comparisons that include Go kit's package log. 154 | 155 | - [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench) includes kit/log 156 | - [uber-common/zap](https://github.com/uber-common/zap), a zero-alloc logging library, includes a comparison with kit/log 157 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) { 10 | lc := log.With(logger, "common_key", "common_value") 11 | b.ReportAllocs() 12 | b.ResetTimer() 13 | for i := 0; i < b.N; i++ { 14 | f(lc) 15 | } 16 | } 17 | 18 | var ( 19 | baseMessage = func(logger log.Logger) { logger.Log("foo_key", "foo_value") } 20 | withMessage = func(logger log.Logger) { log.With(logger, "a", "b").Log("c", "d") } 21 | ) 22 | -------------------------------------------------------------------------------- /concurrency_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/go-kit/log" 8 | ) 9 | 10 | // These test are designed to be run with the race detector. 11 | 12 | func testConcurrency(t *testing.T, logger log.Logger, total int) { 13 | n := int(math.Sqrt(float64(total))) 14 | share := total / n 15 | 16 | errC := make(chan error, n) 17 | 18 | for i := 0; i < n; i++ { 19 | go func() { 20 | errC <- spam(logger, share) 21 | }() 22 | } 23 | 24 | for i := 0; i < n; i++ { 25 | err := <-errC 26 | if err != nil { 27 | t.Fatalf("concurrent logging error: %v", err) 28 | } 29 | } 30 | } 31 | 32 | func spam(logger log.Logger, count int) error { 33 | for i := 0; i < count; i++ { 34 | err := logger.Log("key", i) 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package log provides a structured logger. 2 | // 3 | // Structured logging produces logs easily consumed later by humans or 4 | // machines. Humans might be interested in debugging errors, or tracing 5 | // specific requests. Machines might be interested in counting interesting 6 | // events, or aggregating information for off-line processing. In both cases, 7 | // it is important that the log messages are structured and actionable. 8 | // Package log is designed to encourage both of these best practices. 9 | // 10 | // # Basic Usage 11 | // 12 | // The fundamental interface is Logger. Loggers create log events from 13 | // key/value data. The Logger interface has a single method, Log, which 14 | // accepts a sequence of alternating key/value pairs, which this package names 15 | // keyvals. 16 | // 17 | // type Logger interface { 18 | // Log(keyvals ...interface{}) error 19 | // } 20 | // 21 | // Here is an example of a function using a Logger to create log events. 22 | // 23 | // func RunTask(task Task, logger log.Logger) string { 24 | // logger.Log("taskID", task.ID, "event", "starting task") 25 | // ... 26 | // logger.Log("taskID", task.ID, "event", "task complete") 27 | // } 28 | // 29 | // The keys in the above example are "taskID" and "event". The values are 30 | // task.ID, "starting task", and "task complete". Every key is followed 31 | // immediately by its value. 32 | // 33 | // Keys are usually plain strings. Values may be any type that has a sensible 34 | // encoding in the chosen log format. With structured logging it is a good 35 | // idea to log simple values without formatting them. This practice allows 36 | // the chosen logger to encode values in the most appropriate way. 37 | // 38 | // # Contextual Loggers 39 | // 40 | // A contextual logger stores keyvals that it includes in all log events. 41 | // Building appropriate contextual loggers reduces repetition and aids 42 | // consistency in the resulting log output. With, WithPrefix, and WithSuffix 43 | // add context to a logger. We can use With to improve the RunTask example. 44 | // 45 | // func RunTask(task Task, logger log.Logger) string { 46 | // logger = log.With(logger, "taskID", task.ID) 47 | // logger.Log("event", "starting task") 48 | // ... 49 | // taskHelper(task.Cmd, logger) 50 | // ... 51 | // logger.Log("event", "task complete") 52 | // } 53 | // 54 | // The improved version emits the same log events as the original for the 55 | // first and last calls to Log. Passing the contextual logger to taskHelper 56 | // enables each log event created by taskHelper to include the task.ID even 57 | // though taskHelper does not have access to that value. Using contextual 58 | // loggers this way simplifies producing log output that enables tracing the 59 | // life cycle of individual tasks. (See the Contextual example for the full 60 | // code of the above snippet.) 61 | // 62 | // # Dynamic Contextual Values 63 | // 64 | // A Valuer function stored in a contextual logger generates a new value each 65 | // time an event is logged. The Valuer example demonstrates how this feature 66 | // works. 67 | // 68 | // Valuers provide the basis for consistently logging timestamps and source 69 | // code location. The log package defines several valuers for that purpose. 70 | // See Timestamp, DefaultTimestamp, DefaultTimestampUTC, Caller, and 71 | // DefaultCaller. A common logger initialization sequence that ensures all log 72 | // entries contain a timestamp and source location looks like this: 73 | // 74 | // logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) 75 | // logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) 76 | // 77 | // # Concurrent Safety 78 | // 79 | // Applications with multiple goroutines want each log event written to the 80 | // same logger to remain separate from other log events. Package log provides 81 | // two simple solutions for concurrent safe logging. 82 | // 83 | // NewSyncWriter wraps an io.Writer and serializes each call to its Write 84 | // method. Using a SyncWriter has the benefit that the smallest practical 85 | // portion of the logging logic is performed within a mutex, but it requires 86 | // the formatting Logger to make only one call to Write per log event. 87 | // 88 | // NewSyncLogger wraps any Logger and serializes each call to its Log method. 89 | // Using a SyncLogger has the benefit that it guarantees each log event is 90 | // handled atomically within the wrapped logger, but it typically serializes 91 | // both the formatting and output logic. Use a SyncLogger if the formatting 92 | // logger may perform multiple writes per log event. 93 | // 94 | // # Error Handling 95 | // 96 | // This package relies on the practice of wrapping or decorating loggers with 97 | // other loggers to provide composable pieces of functionality. It also means 98 | // that Logger.Log must return an error because some 99 | // implementations—especially those that output log data to an io.Writer—may 100 | // encounter errors that cannot be handled locally. This in turn means that 101 | // Loggers that wrap other loggers should return errors from the wrapped 102 | // logger up the stack. 103 | // 104 | // Fortunately, the decorator pattern also provides a way to avoid the 105 | // necessity to check for errors every time an application calls Logger.Log. 106 | // An application required to panic whenever its Logger encounters 107 | // an error could initialize its logger as follows. 108 | // 109 | // fmtlogger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) 110 | // logger := log.LoggerFunc(func(keyvals ...interface{}) error { 111 | // if err := fmtlogger.Log(keyvals...); err != nil { 112 | // panic(err) 113 | // } 114 | // return nil 115 | // }) 116 | package log 117 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "sync" 7 | "time" 8 | 9 | "github.com/go-kit/log" 10 | ) 11 | 12 | func Example_basic() { 13 | logger := log.NewLogfmtLogger(os.Stdout) 14 | 15 | type Task struct { 16 | ID int 17 | } 18 | 19 | RunTask := func(task Task, logger log.Logger) { 20 | logger.Log("taskID", task.ID, "event", "starting task") 21 | 22 | logger.Log("taskID", task.ID, "event", "task complete") 23 | } 24 | 25 | RunTask(Task{ID: 1}, logger) 26 | 27 | // Output: 28 | // taskID=1 event="starting task" 29 | // taskID=1 event="task complete" 30 | } 31 | 32 | func Example_contextual() { 33 | logger := log.NewLogfmtLogger(os.Stdout) 34 | 35 | type Task struct { 36 | ID int 37 | Cmd string 38 | } 39 | 40 | taskHelper := func(cmd string, logger log.Logger) { 41 | // execute(cmd) 42 | logger.Log("cmd", cmd, "dur", 42*time.Millisecond) 43 | } 44 | 45 | RunTask := func(task Task, logger log.Logger) { 46 | logger = log.With(logger, "taskID", task.ID) 47 | logger.Log("event", "starting task") 48 | 49 | taskHelper(task.Cmd, logger) 50 | 51 | logger.Log("event", "task complete") 52 | } 53 | 54 | RunTask(Task{ID: 1, Cmd: "echo Hello, world!"}, logger) 55 | 56 | // Output: 57 | // taskID=1 event="starting task" 58 | // taskID=1 cmd="echo Hello, world!" dur=42ms 59 | // taskID=1 event="task complete" 60 | } 61 | 62 | func Example_valuer() { 63 | logger := log.NewLogfmtLogger(os.Stdout) 64 | 65 | count := 0 66 | counter := func() interface{} { 67 | count++ 68 | return count 69 | } 70 | 71 | logger = log.With(logger, "count", log.Valuer(counter)) 72 | 73 | logger.Log("call", "first") 74 | logger.Log("call", "second") 75 | 76 | // Output: 77 | // count=1 call=first 78 | // count=2 call=second 79 | } 80 | 81 | func Example_debugInfo() { 82 | logger := log.NewLogfmtLogger(os.Stdout) 83 | 84 | // make time predictable for this test 85 | baseTime := time.Date(2015, time.February, 3, 10, 0, 0, 0, time.UTC) 86 | mockTime := func() time.Time { 87 | baseTime = baseTime.Add(time.Second) 88 | return baseTime 89 | } 90 | 91 | logger = log.With(logger, "time", log.Timestamp(mockTime), "caller", log.DefaultCaller) 92 | 93 | logger.Log("call", "first") 94 | logger.Log("call", "second") 95 | 96 | // ... 97 | 98 | logger.Log("call", "third") 99 | 100 | // Output: 101 | // time=2015-02-03T10:00:01Z caller=example_test.go:93 call=first 102 | // time=2015-02-03T10:00:02Z caller=example_test.go:94 call=second 103 | // time=2015-02-03T10:00:03Z caller=example_test.go:98 call=third 104 | } 105 | 106 | func Example_syncWriter() { 107 | w := log.NewSyncWriter(os.Stdout) 108 | logger := log.NewLogfmtLogger(w) 109 | 110 | type Task struct { 111 | ID int 112 | } 113 | 114 | var wg sync.WaitGroup 115 | 116 | RunTask := func(task Task, logger log.Logger) { 117 | logger.Log("taskID", task.ID, "event", "starting task") 118 | 119 | time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) 120 | 121 | logger.Log("taskID", task.ID, "event", "task complete") 122 | wg.Done() 123 | } 124 | 125 | wg.Add(2) 126 | 127 | go RunTask(Task{ID: 1}, logger) 128 | go RunTask(Task{ID: 2}, logger) 129 | 130 | wg.Wait() 131 | 132 | // Unordered output: 133 | // taskID=1 event="starting task" 134 | // taskID=2 event="starting task" 135 | // taskID=1 event="task complete" 136 | // taskID=2 event="task complete" 137 | } 138 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-kit/log 2 | 3 | go 1.17 4 | 5 | require github.com/go-logfmt/logfmt v0.5.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 2 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 3 | -------------------------------------------------------------------------------- /json_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | ) 10 | 11 | type jsonLogger struct { 12 | io.Writer 13 | } 14 | 15 | // NewJSONLogger returns a Logger that encodes keyvals to the Writer as a 16 | // single JSON object. Each log event produces no more than one call to 17 | // w.Write. The passed Writer must be safe for concurrent use by multiple 18 | // goroutines if the returned Logger will be used concurrently. 19 | func NewJSONLogger(w io.Writer) Logger { 20 | return &jsonLogger{w} 21 | } 22 | 23 | func (l *jsonLogger) Log(keyvals ...interface{}) error { 24 | n := (len(keyvals) + 1) / 2 // +1 to handle case when len is odd 25 | m := make(map[string]interface{}, n) 26 | for i := 0; i < len(keyvals); i += 2 { 27 | k := keyvals[i] 28 | var v interface{} = ErrMissingValue 29 | if i+1 < len(keyvals) { 30 | v = keyvals[i+1] 31 | } 32 | merge(m, k, v) 33 | } 34 | enc := json.NewEncoder(l.Writer) 35 | enc.SetEscapeHTML(false) 36 | return enc.Encode(m) 37 | } 38 | 39 | func merge(dst map[string]interface{}, k, v interface{}) { 40 | var key string 41 | switch x := k.(type) { 42 | case string: 43 | key = x 44 | case fmt.Stringer: 45 | key = safeString(x) 46 | default: 47 | key = fmt.Sprint(x) 48 | } 49 | 50 | // We want json.Marshaler and encoding.TextMarshaller to take priority over 51 | // err.Error() and v.String(). But json.Marshall (called later) does that by 52 | // default so we force a no-op if it's one of those 2 case. 53 | switch x := v.(type) { 54 | case json.Marshaler: 55 | case encoding.TextMarshaler: 56 | case error: 57 | v = safeError(x) 58 | case fmt.Stringer: 59 | v = safeString(x) 60 | } 61 | 62 | dst[key] = v 63 | } 64 | 65 | func safeString(str fmt.Stringer) (s string) { 66 | defer func() { 67 | if panicVal := recover(); panicVal != nil { 68 | if v := reflect.ValueOf(str); v.Kind() == reflect.Ptr && v.IsNil() { 69 | s = "NULL" 70 | } else { 71 | s = fmt.Sprintf("PANIC in String method: %v", panicVal) 72 | } 73 | } 74 | }() 75 | s = str.String() 76 | return 77 | } 78 | 79 | func safeError(err error) (s interface{}) { 80 | defer func() { 81 | if panicVal := recover(); panicVal != nil { 82 | if v := reflect.ValueOf(err); v.Kind() == reflect.Ptr && v.IsNil() { 83 | s = nil 84 | } else { 85 | s = fmt.Sprintf("PANIC in Error method: %v", panicVal) 86 | } 87 | } 88 | }() 89 | s = err.Error() 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /json_logger_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/go-kit/log" 10 | ) 11 | 12 | func TestJSONLoggerCaller(t *testing.T) { 13 | t.Parallel() 14 | buf := &bytes.Buffer{} 15 | logger := log.NewJSONLogger(buf) 16 | logger = log.With(logger, "caller", log.DefaultCaller) 17 | 18 | if err := logger.Log(); err != nil { 19 | t.Fatal(err) 20 | } 21 | if want, have := `{"caller":"json_logger_test.go:18"}`+"\n", buf.String(); want != have { 22 | t.Errorf("\nwant %#v\nhave %#v", want, have) 23 | } 24 | } 25 | 26 | func TestJSONLogger(t *testing.T) { 27 | t.Parallel() 28 | buf := &bytes.Buffer{} 29 | logger := log.NewJSONLogger(buf) 30 | if err := logger.Log("err", errors.New("err"), "m", map[string]int{"0": 0}, "a", []int{1, 2, 3}); err != nil { 31 | t.Fatal(err) 32 | } 33 | if want, have := `{"a":[1,2,3],"err":"err","m":{"0":0}}`+"\n", buf.String(); want != have { 34 | t.Errorf("\nwant %#v\nhave %#v", want, have) 35 | } 36 | } 37 | 38 | func TestJSONLoggerMissingValue(t *testing.T) { 39 | t.Parallel() 40 | buf := &bytes.Buffer{} 41 | logger := log.NewJSONLogger(buf) 42 | if err := logger.Log("k"); err != nil { 43 | t.Fatal(err) 44 | } 45 | if want, have := `{"k":"(MISSING)"}`+"\n", buf.String(); want != have { 46 | t.Errorf("\nwant %#v\nhave %#v", want, have) 47 | } 48 | } 49 | 50 | func TestJSONLoggerNilStringerKey(t *testing.T) { 51 | t.Parallel() 52 | 53 | buf := &bytes.Buffer{} 54 | logger := log.NewJSONLogger(buf) 55 | if err := logger.Log((*stringer)(nil), "v"); err != nil { 56 | t.Fatal(err) 57 | } 58 | if want, have := `{"NULL":"v"}`+"\n", buf.String(); want != have { 59 | t.Errorf("\nwant %#v\nhave %#v", want, have) 60 | } 61 | } 62 | 63 | func TestJSONLoggerPanicStringerValue(t *testing.T) { 64 | t.Parallel() 65 | 66 | buf := &bytes.Buffer{} 67 | logger := log.NewJSONLogger(buf) 68 | if err := logger.Log("k", unsafeStringer{}); err != nil { 69 | t.Fatal(err) 70 | } 71 | if want, have := `{"k":"PANIC in String method: error"}`+"\n", buf.String(); want != have { 72 | t.Errorf("\nwant %#v\nhave %#v", want, have) 73 | } 74 | } 75 | 76 | func TestJSONLoggerNilErrorValue(t *testing.T) { 77 | t.Parallel() 78 | 79 | buf := &bytes.Buffer{} 80 | logger := log.NewJSONLogger(buf) 81 | if err := logger.Log("err", (*stringError)(nil)); err != nil { 82 | t.Fatal(err) 83 | } 84 | if want, have := `{"err":null}`+"\n", buf.String(); want != have { 85 | t.Errorf("\nwant %#v\nhave %#v", want, have) 86 | } 87 | } 88 | 89 | func TestJSONLoggerPanicErrorValue(t *testing.T) { 90 | t.Parallel() 91 | 92 | buf := &bytes.Buffer{} 93 | logger := log.NewJSONLogger(buf) 94 | if err := logger.Log("err", unsafeError{}); err != nil { 95 | t.Fatal(err) 96 | } 97 | if want, have := `{"err":"PANIC in Error method: error"}`+"\n", buf.String(); want != have { 98 | t.Errorf("\nwant %#v\nhave %#v", want, have) 99 | } 100 | } 101 | 102 | func TestJSONLoggerNoHTMLEscape(t *testing.T) { 103 | t.Parallel() 104 | buf := &bytes.Buffer{} 105 | logger := log.NewJSONLogger(buf) 106 | if err := logger.Log("k", "<&>"); err != nil { 107 | t.Fatal(err) 108 | } 109 | if want, have := `{"k":"<&>"}`+"\n", buf.String(); want != have { 110 | t.Errorf("\nwant %#v\nhave%#v", want, have) 111 | } 112 | } 113 | 114 | // aller implements json.Marshaler, encoding.TextMarshaler, and fmt.Stringer. 115 | type aller struct{} 116 | 117 | func (aller) MarshalJSON() ([]byte, error) { 118 | return []byte("\"json\""), nil 119 | } 120 | 121 | func (aller) MarshalText() ([]byte, error) { 122 | return []byte("text"), nil 123 | } 124 | 125 | func (aller) String() string { 126 | return "string" 127 | } 128 | 129 | func (aller) Error() string { 130 | return "error" 131 | } 132 | 133 | // textstringer implements encoding.TextMarshaler and fmt.Stringer. 134 | type textstringer struct{} 135 | 136 | func (textstringer) MarshalText() ([]byte, error) { 137 | return []byte("text"), nil 138 | } 139 | 140 | func (textstringer) String() string { 141 | return "string" 142 | } 143 | 144 | func TestJSONLoggerStringValue(t *testing.T) { 145 | t.Parallel() 146 | tests := []struct { 147 | v interface{} 148 | expected string 149 | }{ 150 | { 151 | v: aller{}, 152 | expected: `{"v":"json"}`, 153 | }, 154 | { 155 | v: textstringer{}, 156 | expected: `{"v":"text"}`, 157 | }, 158 | { 159 | v: stringer("string"), 160 | expected: `{"v":"string"}`, 161 | }, 162 | } 163 | 164 | for _, test := range tests { 165 | buf := &bytes.Buffer{} 166 | logger := log.NewJSONLogger(buf) 167 | if err := logger.Log("v", test.v); err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | if want, have := test.expected+"\n", buf.String(); want != have { 172 | t.Errorf("\nwant %#v\nhave %#v", want, have) 173 | } 174 | } 175 | } 176 | 177 | type stringer string 178 | 179 | func (s stringer) String() string { 180 | return string(s) 181 | } 182 | 183 | type stringError string 184 | 185 | func (s stringError) Error() string { 186 | return string(s) 187 | } 188 | 189 | type unsafeStringer struct{} 190 | 191 | func (s unsafeStringer) String() string { 192 | panic("error") 193 | } 194 | 195 | type unsafeError struct{} 196 | 197 | func (s unsafeError) Error() string { 198 | panic("error") 199 | } 200 | 201 | func BenchmarkJSONLoggerSimple(b *testing.B) { 202 | benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard), baseMessage) 203 | } 204 | 205 | func BenchmarkJSONLoggerContextual(b *testing.B) { 206 | benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard), withMessage) 207 | } 208 | 209 | func TestJSONLoggerConcurrency(t *testing.T) { 210 | t.Parallel() 211 | testConcurrency(t, log.NewJSONLogger(ioutil.Discard), 10000) 212 | } 213 | -------------------------------------------------------------------------------- /level/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package level_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/go-kit/log" 8 | "github.com/go-kit/log/level" 9 | ) 10 | 11 | func Benchmark(b *testing.B) { 12 | contexts := []struct { 13 | name string 14 | context func(log.Logger) log.Logger 15 | }{ 16 | {"NoContext", func(l log.Logger) log.Logger { 17 | return l 18 | }}, 19 | {"TimeContext", func(l log.Logger) log.Logger { 20 | return log.With(l, "time", log.DefaultTimestampUTC) 21 | }}, 22 | {"CallerContext", func(l log.Logger) log.Logger { 23 | return log.With(l, "caller", log.DefaultCaller) 24 | }}, 25 | {"TimeCallerReqIDContext", func(l log.Logger) log.Logger { 26 | return log.With(l, "time", log.DefaultTimestampUTC, "caller", log.DefaultCaller, "reqID", 29) 27 | }}, 28 | } 29 | 30 | loggers := []struct { 31 | name string 32 | logger log.Logger 33 | }{ 34 | {"Nop", log.NewNopLogger()}, 35 | {"Logfmt", log.NewLogfmtLogger(ioutil.Discard)}, 36 | {"JSON", log.NewJSONLogger(ioutil.Discard)}, 37 | } 38 | 39 | filters := []struct { 40 | name string 41 | filter func(log.Logger) log.Logger 42 | }{ 43 | {"Baseline", func(l log.Logger) log.Logger { 44 | return l 45 | }}, 46 | {"DisallowedLevel", func(l log.Logger) log.Logger { 47 | return level.NewFilter(l, level.AllowInfo()) 48 | }}, 49 | {"AllowedLevel", func(l log.Logger) log.Logger { 50 | return level.NewFilter(l, level.AllowAll()) 51 | }}, 52 | } 53 | 54 | for _, c := range contexts { 55 | b.Run(c.name, func(b *testing.B) { 56 | for _, f := range filters { 57 | b.Run(f.name, func(b *testing.B) { 58 | for _, l := range loggers { 59 | b.Run(l.name, func(b *testing.B) { 60 | logger := c.context(f.filter(l.logger)) 61 | b.ResetTimer() 62 | b.ReportAllocs() 63 | for i := 0; i < b.N; i++ { 64 | level.Debug(logger).Log("foo", "bar") 65 | } 66 | }) 67 | } 68 | }) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /level/doc.go: -------------------------------------------------------------------------------- 1 | // Package level implements leveled logging on top of Go kit's log package. To 2 | // use the level package, create a logger as per normal in your func main, and 3 | // wrap it with level.NewFilter. 4 | // 5 | // var logger log.Logger 6 | // logger = log.NewLogfmtLogger(os.Stderr) 7 | // logger = level.NewFilter(logger, level.AllowInfo()) // <-- 8 | // logger = log.With(logger, "ts", log.DefaultTimestampUTC) 9 | // 10 | // It's also possible to configure log level from a string. For instance from 11 | // a flag, environment variable or configuration file. 12 | // 13 | // fs := flag.NewFlagSet("myprogram") 14 | // lvl := fs.String("log", "info", "debug, info, warn, error") 15 | // 16 | // var logger log.Logger 17 | // logger = log.NewLogfmtLogger(os.Stderr) 18 | // logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.InfoValue()))) // <-- 19 | // logger = log.With(logger, "ts", log.DefaultTimestampUTC) 20 | // 21 | // Then, at the callsites, use one of the level.Debug, Info, Warn, or Error 22 | // helper methods to emit leveled log events. 23 | // 24 | // logger.Log("foo", "bar") // as normal, no level 25 | // level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get()) 26 | // if value > 100 { 27 | // level.Error(logger).Log("value", value) 28 | // } 29 | // 30 | // NewFilter allows precise control over what happens when a log event is 31 | // emitted without a level key, or if a squelched level is used. Check the 32 | // Option functions for details. 33 | package level 34 | -------------------------------------------------------------------------------- /level/example_test.go: -------------------------------------------------------------------------------- 1 | package level_test 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "os" 7 | 8 | "github.com/go-kit/log" 9 | "github.com/go-kit/log/level" 10 | ) 11 | 12 | func Example_basic() { 13 | logger := log.NewLogfmtLogger(os.Stdout) 14 | level.Debug(logger).Log("msg", "this message is at the debug level") 15 | level.Info(logger).Log("msg", "this message is at the info level") 16 | level.Warn(logger).Log("msg", "this message is at the warn level") 17 | level.Error(logger).Log("msg", "this message is at the error level") 18 | 19 | // Output: 20 | // level=debug msg="this message is at the debug level" 21 | // level=info msg="this message is at the info level" 22 | // level=warn msg="this message is at the warn level" 23 | // level=error msg="this message is at the error level" 24 | } 25 | 26 | func Example_filtered() { 27 | // Set up logger with level filter. 28 | logger := log.NewLogfmtLogger(os.Stdout) 29 | logger = level.NewFilter(logger, level.AllowInfo()) 30 | logger = log.With(logger, "caller", log.DefaultCaller) 31 | 32 | // Use level helpers to log at different levels. 33 | level.Error(logger).Log("err", errors.New("bad data")) 34 | level.Info(logger).Log("event", "data saved") 35 | level.Debug(logger).Log("next item", 17) // filtered 36 | 37 | // Output: 38 | // level=error caller=example_test.go:33 err="bad data" 39 | // level=info caller=example_test.go:34 event="data saved" 40 | } 41 | 42 | func Example_parsed() { 43 | fs := flag.NewFlagSet("example", flag.ExitOnError) 44 | lvl := fs.String("log-level", "", `"debug", "info", "warn" or "error"`) 45 | fs.Parse([]string{"-log-level", "info"}) 46 | 47 | // Set up logger with level filter. 48 | logger := log.NewLogfmtLogger(os.Stdout) 49 | logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.DebugValue()))) 50 | logger = log.With(logger, "caller", log.DefaultCaller) 51 | 52 | // Use level helpers to log at different levels. 53 | level.Error(logger).Log("err", errors.New("bad data")) 54 | level.Info(logger).Log("event", "data saved") 55 | level.Debug(logger).Log("next item", 17) // filtered 56 | 57 | // Output: 58 | // level=error caller=example_test.go:53 err="bad data" 59 | // level=info caller=example_test.go:54 event="data saved" 60 | } 61 | -------------------------------------------------------------------------------- /level/level.go: -------------------------------------------------------------------------------- 1 | package level 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/go-kit/log" 8 | ) 9 | 10 | // ErrInvalidLevelString is returned whenever an invalid string is passed to Parse. 11 | var ErrInvalidLevelString = errors.New("invalid level string") 12 | 13 | // Error returns a logger that includes a Key/ErrorValue pair. 14 | func Error(logger log.Logger) log.Logger { 15 | return log.WithPrefix(logger, Key(), ErrorValue()) 16 | } 17 | 18 | // Warn returns a logger that includes a Key/WarnValue pair. 19 | func Warn(logger log.Logger) log.Logger { 20 | return log.WithPrefix(logger, Key(), WarnValue()) 21 | } 22 | 23 | // Info returns a logger that includes a Key/InfoValue pair. 24 | func Info(logger log.Logger) log.Logger { 25 | return log.WithPrefix(logger, Key(), InfoValue()) 26 | } 27 | 28 | // Debug returns a logger that includes a Key/DebugValue pair. 29 | func Debug(logger log.Logger) log.Logger { 30 | return log.WithPrefix(logger, Key(), DebugValue()) 31 | } 32 | 33 | // NewFilter wraps next and implements level filtering. See the commentary on 34 | // the Option functions for a detailed description of how to configure levels. 35 | // If no options are provided, all leveled log events created with Debug, 36 | // Info, Warn or Error helper methods are squelched and non-leveled log 37 | // events are passed to next unmodified. 38 | func NewFilter(next log.Logger, options ...Option) log.Logger { 39 | l := &logger{ 40 | next: next, 41 | } 42 | for _, option := range options { 43 | option(l) 44 | } 45 | return l 46 | } 47 | 48 | type logger struct { 49 | next log.Logger 50 | allowed level 51 | squelchNoLevel bool 52 | errNotAllowed error 53 | errNoLevel error 54 | } 55 | 56 | func (l *logger) Log(keyvals ...interface{}) error { 57 | var hasLevel, levelAllowed bool 58 | for i := 1; i < len(keyvals); i += 2 { 59 | if v, ok := keyvals[i].(*levelValue); ok { 60 | hasLevel = true 61 | levelAllowed = l.allowed&v.level != 0 62 | break 63 | } 64 | } 65 | if !hasLevel && l.squelchNoLevel { 66 | return l.errNoLevel 67 | } 68 | if hasLevel && !levelAllowed { 69 | return l.errNotAllowed 70 | } 71 | return l.next.Log(keyvals...) 72 | } 73 | 74 | // Option sets a parameter for the leveled logger. 75 | type Option func(*logger) 76 | 77 | // Allow the provided log level to pass. 78 | func Allow(v Value) Option { 79 | switch v { 80 | case debugValue: 81 | return AllowDebug() 82 | case infoValue: 83 | return AllowInfo() 84 | case warnValue: 85 | return AllowWarn() 86 | case errorValue: 87 | return AllowError() 88 | default: 89 | return AllowNone() 90 | } 91 | } 92 | 93 | // AllowAll is an alias for AllowDebug. 94 | func AllowAll() Option { 95 | return AllowDebug() 96 | } 97 | 98 | // AllowDebug allows error, warn, info and debug level log events to pass. 99 | func AllowDebug() Option { 100 | return allowed(levelError | levelWarn | levelInfo | levelDebug) 101 | } 102 | 103 | // AllowInfo allows error, warn and info level log events to pass. 104 | func AllowInfo() Option { 105 | return allowed(levelError | levelWarn | levelInfo) 106 | } 107 | 108 | // AllowWarn allows error and warn level log events to pass. 109 | func AllowWarn() Option { 110 | return allowed(levelError | levelWarn) 111 | } 112 | 113 | // AllowError allows only error level log events to pass. 114 | func AllowError() Option { 115 | return allowed(levelError) 116 | } 117 | 118 | // AllowNone allows no leveled log events to pass. 119 | func AllowNone() Option { 120 | return allowed(0) 121 | } 122 | 123 | func allowed(allowed level) Option { 124 | return func(l *logger) { l.allowed = allowed } 125 | } 126 | 127 | // Parse a string to its corresponding level value. Valid strings are "debug", 128 | // "info", "warn", and "error". Strings are normalized via strings.TrimSpace and 129 | // strings.ToLower. 130 | func Parse(level string) (Value, error) { 131 | switch strings.TrimSpace(strings.ToLower(level)) { 132 | case debugValue.name: 133 | return debugValue, nil 134 | case infoValue.name: 135 | return infoValue, nil 136 | case warnValue.name: 137 | return warnValue, nil 138 | case errorValue.name: 139 | return errorValue, nil 140 | default: 141 | return nil, ErrInvalidLevelString 142 | } 143 | } 144 | 145 | // ParseDefault calls Parse and returns the default Value on error. 146 | func ParseDefault(level string, def Value) Value { 147 | v, err := Parse(level) 148 | if err != nil { 149 | return def 150 | } 151 | return v 152 | } 153 | 154 | // ErrNotAllowed sets the error to return from Log when it squelches a log 155 | // event disallowed by the configured Allow[Level] option. By default, 156 | // ErrNotAllowed is nil; in this case the log event is squelched with no 157 | // error. 158 | func ErrNotAllowed(err error) Option { 159 | return func(l *logger) { l.errNotAllowed = err } 160 | } 161 | 162 | // SquelchNoLevel instructs Log to squelch log events with no level, so that 163 | // they don't proceed through to the wrapped logger. If SquelchNoLevel is set 164 | // to true and a log event is squelched in this way, the error value 165 | // configured with ErrNoLevel is returned to the caller. 166 | func SquelchNoLevel(squelch bool) Option { 167 | return func(l *logger) { l.squelchNoLevel = squelch } 168 | } 169 | 170 | // ErrNoLevel sets the error to return from Log when it squelches a log event 171 | // with no level. By default, ErrNoLevel is nil; in this case the log event is 172 | // squelched with no error. 173 | func ErrNoLevel(err error) Option { 174 | return func(l *logger) { l.errNoLevel = err } 175 | } 176 | 177 | // NewInjector wraps next and returns a logger that adds a Key/level pair to 178 | // the beginning of log events that don't already contain a level. In effect, 179 | // this gives a default level to logs without a level. 180 | func NewInjector(next log.Logger, level Value) log.Logger { 181 | return &injector{ 182 | next: next, 183 | level: level, 184 | } 185 | } 186 | 187 | type injector struct { 188 | next log.Logger 189 | level interface{} 190 | } 191 | 192 | func (l *injector) Log(keyvals ...interface{}) error { 193 | for i := 1; i < len(keyvals); i += 2 { 194 | if _, ok := keyvals[i].(*levelValue); ok { 195 | return l.next.Log(keyvals...) 196 | } 197 | } 198 | kvs := make([]interface{}, len(keyvals)+2) 199 | kvs[0], kvs[1] = key, l.level 200 | copy(kvs[2:], keyvals) 201 | return l.next.Log(kvs...) 202 | } 203 | 204 | // Value is the interface that each of the canonical level values implement. 205 | // It contains unexported methods that prevent types from other packages from 206 | // implementing it and guaranteeing that NewFilter can distinguish the levels 207 | // defined in this package from all other values. 208 | type Value interface { 209 | String() string 210 | levelVal() 211 | } 212 | 213 | // Key returns the unique key added to log events by the loggers in this 214 | // package. 215 | func Key() interface{} { return key } 216 | 217 | // ErrorValue returns the unique value added to log events by Error. 218 | func ErrorValue() Value { return errorValue } 219 | 220 | // WarnValue returns the unique value added to log events by Warn. 221 | func WarnValue() Value { return warnValue } 222 | 223 | // InfoValue returns the unique value added to log events by Info. 224 | func InfoValue() Value { return infoValue } 225 | 226 | // DebugValue returns the unique value added to log events by Debug. 227 | func DebugValue() Value { return debugValue } 228 | 229 | var ( 230 | // key is of type interface{} so that it allocates once during package 231 | // initialization and avoids allocating every time the value is added to a 232 | // []interface{} later. 233 | key interface{} = "level" 234 | 235 | errorValue = &levelValue{level: levelError, name: "error"} 236 | warnValue = &levelValue{level: levelWarn, name: "warn"} 237 | infoValue = &levelValue{level: levelInfo, name: "info"} 238 | debugValue = &levelValue{level: levelDebug, name: "debug"} 239 | ) 240 | 241 | type level byte 242 | 243 | const ( 244 | levelDebug level = 1 << iota 245 | levelInfo 246 | levelWarn 247 | levelError 248 | ) 249 | 250 | type levelValue struct { 251 | name string 252 | level 253 | } 254 | 255 | func (v *levelValue) String() string { return v.name } 256 | func (v *levelValue) levelVal() {} 257 | -------------------------------------------------------------------------------- /level/level_test.go: -------------------------------------------------------------------------------- 1 | package level_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/go-kit/log" 11 | "github.com/go-kit/log/level" 12 | ) 13 | 14 | func TestVariousLevels(t *testing.T) { 15 | testCases := []struct { 16 | name string 17 | allowed level.Option 18 | want string 19 | }{ 20 | { 21 | "Allow(DebugValue)", 22 | level.Allow(level.DebugValue()), 23 | strings.Join([]string{ 24 | `{"level":"debug","this is":"debug log"}`, 25 | `{"level":"info","this is":"info log"}`, 26 | `{"level":"warn","this is":"warn log"}`, 27 | `{"level":"error","this is":"error log"}`, 28 | }, "\n"), 29 | }, 30 | { 31 | "Allow(InfoValue)", 32 | level.Allow(level.InfoValue()), 33 | strings.Join([]string{ 34 | `{"level":"info","this is":"info log"}`, 35 | `{"level":"warn","this is":"warn log"}`, 36 | `{"level":"error","this is":"error log"}`, 37 | }, "\n"), 38 | }, 39 | { 40 | "Allow(WarnValue)", 41 | level.Allow(level.WarnValue()), 42 | strings.Join([]string{ 43 | `{"level":"warn","this is":"warn log"}`, 44 | `{"level":"error","this is":"error log"}`, 45 | }, "\n"), 46 | }, 47 | { 48 | "Allow(ErrorValue)", 49 | level.Allow(level.ErrorValue()), 50 | strings.Join([]string{ 51 | `{"level":"error","this is":"error log"}`, 52 | }, "\n"), 53 | }, 54 | { 55 | "Allow(nil)", 56 | level.Allow(nil), 57 | strings.Join([]string{}, "\n"), 58 | }, 59 | { 60 | "AllowAll", 61 | level.AllowAll(), 62 | strings.Join([]string{ 63 | `{"level":"debug","this is":"debug log"}`, 64 | `{"level":"info","this is":"info log"}`, 65 | `{"level":"warn","this is":"warn log"}`, 66 | `{"level":"error","this is":"error log"}`, 67 | }, "\n"), 68 | }, 69 | { 70 | "AllowDebug", 71 | level.AllowDebug(), 72 | strings.Join([]string{ 73 | `{"level":"debug","this is":"debug log"}`, 74 | `{"level":"info","this is":"info log"}`, 75 | `{"level":"warn","this is":"warn log"}`, 76 | `{"level":"error","this is":"error log"}`, 77 | }, "\n"), 78 | }, 79 | { 80 | "AllowInfo", 81 | level.AllowInfo(), 82 | strings.Join([]string{ 83 | `{"level":"info","this is":"info log"}`, 84 | `{"level":"warn","this is":"warn log"}`, 85 | `{"level":"error","this is":"error log"}`, 86 | }, "\n"), 87 | }, 88 | { 89 | "AllowWarn", 90 | level.AllowWarn(), 91 | strings.Join([]string{ 92 | `{"level":"warn","this is":"warn log"}`, 93 | `{"level":"error","this is":"error log"}`, 94 | }, "\n"), 95 | }, 96 | { 97 | "AllowError", 98 | level.AllowError(), 99 | strings.Join([]string{ 100 | `{"level":"error","this is":"error log"}`, 101 | }, "\n"), 102 | }, 103 | { 104 | "AllowNone", 105 | level.AllowNone(), 106 | ``, 107 | }, 108 | } 109 | 110 | for _, tc := range testCases { 111 | t.Run(tc.name, func(t *testing.T) { 112 | var buf bytes.Buffer 113 | logger := level.NewFilter(log.NewJSONLogger(&buf), tc.allowed) 114 | 115 | level.Debug(logger).Log("this is", "debug log") 116 | level.Info(logger).Log("this is", "info log") 117 | level.Warn(logger).Log("this is", "warn log") 118 | level.Error(logger).Log("this is", "error log") 119 | 120 | if want, have := tc.want, strings.TrimSpace(buf.String()); want != have { 121 | t.Errorf("\nwant:\n%s\nhave:\n%s", want, have) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func TestErrNotAllowed(t *testing.T) { 128 | myError := errors.New("squelched!") 129 | opts := []level.Option{ 130 | level.AllowWarn(), 131 | level.ErrNotAllowed(myError), 132 | } 133 | logger := level.NewFilter(log.NewNopLogger(), opts...) 134 | 135 | if want, have := myError, level.Info(logger).Log("foo", "bar"); want != have { 136 | t.Errorf("want %#+v, have %#+v", want, have) 137 | } 138 | 139 | if want, have := error(nil), level.Warn(logger).Log("foo", "bar"); want != have { 140 | t.Errorf("want %#+v, have %#+v", want, have) 141 | } 142 | } 143 | 144 | func TestErrNoLevel(t *testing.T) { 145 | myError := errors.New("no level specified") 146 | 147 | var buf bytes.Buffer 148 | opts := []level.Option{ 149 | level.SquelchNoLevel(true), 150 | level.ErrNoLevel(myError), 151 | } 152 | logger := level.NewFilter(log.NewJSONLogger(&buf), opts...) 153 | 154 | if want, have := myError, logger.Log("foo", "bar"); want != have { 155 | t.Errorf("want %v, have %v", want, have) 156 | } 157 | if want, have := ``, strings.TrimSpace(buf.String()); want != have { 158 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) 159 | } 160 | } 161 | 162 | func TestAllowNoLevel(t *testing.T) { 163 | var buf bytes.Buffer 164 | opts := []level.Option{ 165 | level.SquelchNoLevel(false), 166 | level.ErrNoLevel(errors.New("I should never be returned!")), 167 | } 168 | logger := level.NewFilter(log.NewJSONLogger(&buf), opts...) 169 | 170 | if want, have := error(nil), logger.Log("foo", "bar"); want != have { 171 | t.Errorf("want %v, have %v", want, have) 172 | } 173 | if want, have := `{"foo":"bar"}`, strings.TrimSpace(buf.String()); want != have { 174 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) 175 | } 176 | } 177 | 178 | func TestLevelContext(t *testing.T) { 179 | var buf bytes.Buffer 180 | 181 | // Wrapping the level logger with a context allows users to use 182 | // log.DefaultCaller as per normal. 183 | var logger log.Logger 184 | logger = log.NewLogfmtLogger(&buf) 185 | logger = level.NewFilter(logger, level.AllowAll()) 186 | logger = log.With(logger, "caller", log.DefaultCaller) 187 | 188 | level.Info(logger).Log("foo", "bar") 189 | if want, have := `level=info caller=level_test.go:188 foo=bar`, strings.TrimSpace(buf.String()); want != have { 190 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) 191 | } 192 | } 193 | 194 | func TestContextLevel(t *testing.T) { 195 | var buf bytes.Buffer 196 | 197 | // Wrapping a context with the level logger still works, but requires users 198 | // to specify a higher callstack depth value. 199 | var logger log.Logger 200 | logger = log.NewLogfmtLogger(&buf) 201 | logger = log.With(logger, "caller", log.Caller(5)) 202 | logger = level.NewFilter(logger, level.AllowAll()) 203 | 204 | level.Info(logger).Log("foo", "bar") 205 | if want, have := `caller=level_test.go:204 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have { 206 | t.Errorf("\nwant '%s'\nhave '%s'", want, have) 207 | } 208 | } 209 | 210 | func TestLevelFormatting(t *testing.T) { 211 | testCases := []struct { 212 | name string 213 | format func(io.Writer) log.Logger 214 | output string 215 | }{ 216 | { 217 | name: "logfmt", 218 | format: log.NewLogfmtLogger, 219 | output: `level=info foo=bar`, 220 | }, 221 | { 222 | name: "JSON", 223 | format: log.NewJSONLogger, 224 | output: `{"foo":"bar","level":"info"}`, 225 | }, 226 | } 227 | 228 | for _, tc := range testCases { 229 | t.Run(tc.name, func(t *testing.T) { 230 | var buf bytes.Buffer 231 | 232 | logger := tc.format(&buf) 233 | level.Info(logger).Log("foo", "bar") 234 | if want, have := tc.output, strings.TrimSpace(buf.String()); want != have { 235 | t.Errorf("\nwant: '%s'\nhave '%s'", want, have) 236 | } 237 | }) 238 | } 239 | } 240 | 241 | func TestInjector(t *testing.T) { 242 | var ( 243 | output []interface{} 244 | logger log.Logger 245 | ) 246 | 247 | logger = log.LoggerFunc(func(keyvals ...interface{}) error { 248 | output = keyvals 249 | return nil 250 | }) 251 | logger = level.NewInjector(logger, level.InfoValue()) 252 | 253 | logger.Log("foo", "bar") 254 | if got, want := len(output), 4; got != want { 255 | t.Errorf("missing level not injected: got len==%d, want len==%d", got, want) 256 | } 257 | if got, want := output[0], level.Key(); got != want { 258 | t.Errorf("wrong level key: got %#v, want %#v", got, want) 259 | } 260 | if got, want := output[1], level.InfoValue(); got != want { 261 | t.Errorf("wrong level value: got %#v, want %#v", got, want) 262 | } 263 | 264 | level.Error(logger).Log("foo", "bar") 265 | if got, want := len(output), 4; got != want { 266 | t.Errorf("leveled record modified: got len==%d, want len==%d", got, want) 267 | } 268 | if got, want := output[0], level.Key(); got != want { 269 | t.Errorf("wrong level key: got %#v, want %#v", got, want) 270 | } 271 | if got, want := output[1], level.ErrorValue(); got != want { 272 | t.Errorf("wrong level value: got %#v, want %#v", got, want) 273 | } 274 | } 275 | 276 | func TestParse(t *testing.T) { 277 | testCases := []struct { 278 | name string 279 | level string 280 | want level.Value 281 | wantErr error 282 | }{ 283 | { 284 | name: "Debug", 285 | level: "debug", 286 | want: level.DebugValue(), 287 | wantErr: nil, 288 | }, 289 | { 290 | name: "Info", 291 | level: "info", 292 | want: level.InfoValue(), 293 | wantErr: nil, 294 | }, 295 | { 296 | name: "Warn", 297 | level: "warn", 298 | want: level.WarnValue(), 299 | wantErr: nil, 300 | }, 301 | { 302 | name: "Error", 303 | level: "error", 304 | want: level.ErrorValue(), 305 | wantErr: nil, 306 | }, 307 | { 308 | name: "Case Insensitive", 309 | level: "ErRoR", 310 | want: level.ErrorValue(), 311 | wantErr: nil, 312 | }, 313 | { 314 | name: "Trimmed", 315 | level: " Error ", 316 | want: level.ErrorValue(), 317 | wantErr: nil, 318 | }, 319 | { 320 | name: "Invalid", 321 | level: "invalid", 322 | want: nil, 323 | wantErr: level.ErrInvalidLevelString, 324 | }, 325 | } 326 | 327 | for _, tc := range testCases { 328 | t.Run(tc.name, func(t *testing.T) { 329 | got, err := level.Parse(tc.level) 330 | if err != tc.wantErr { 331 | t.Errorf("got unexpected error %#v", err) 332 | } 333 | 334 | if got != tc.want { 335 | t.Errorf("wrong value: got=%#v, want=%#v", got, tc.want) 336 | } 337 | }) 338 | } 339 | } 340 | 341 | func TestParseDefault(t *testing.T) { 342 | testCases := []struct { 343 | name string 344 | level string 345 | want level.Value 346 | }{ 347 | { 348 | name: "Debug", 349 | level: "debug", 350 | want: level.DebugValue(), 351 | }, 352 | { 353 | name: "Info", 354 | level: "info", 355 | want: level.InfoValue(), 356 | }, 357 | { 358 | name: "Warn", 359 | level: "warn", 360 | want: level.WarnValue(), 361 | }, 362 | { 363 | name: "Error", 364 | level: "error", 365 | want: level.ErrorValue(), 366 | }, 367 | { 368 | name: "Case Insensitive", 369 | level: "ErRoR", 370 | want: level.ErrorValue(), 371 | }, 372 | { 373 | name: "Trimmed", 374 | level: " Error ", 375 | want: level.ErrorValue(), 376 | }, 377 | { 378 | name: "Invalid", 379 | level: "invalid", 380 | want: level.InfoValue(), 381 | }, 382 | } 383 | 384 | for _, tc := range testCases { 385 | t.Run(tc.name, func(t *testing.T) { 386 | got := level.ParseDefault(tc.level, level.InfoValue()) 387 | 388 | if got != tc.want { 389 | t.Errorf("wrong value: got=%#v, want=%#v", got, tc.want) 390 | } 391 | }) 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "errors" 4 | 5 | // Logger is the fundamental interface for all log operations. Log creates a 6 | // log event from keyvals, a variadic sequence of alternating keys and values. 7 | // Implementations must be safe for concurrent use by multiple goroutines. In 8 | // particular, any implementation of Logger that appends to keyvals or 9 | // modifies or retains any of its elements must make a copy first. 10 | type Logger interface { 11 | Log(keyvals ...interface{}) error 12 | } 13 | 14 | // ErrMissingValue is appended to keyvals slices with odd length to substitute 15 | // the missing value. 16 | var ErrMissingValue = errors.New("(MISSING)") 17 | 18 | // With returns a new contextual logger with keyvals prepended to those passed 19 | // to calls to Log. If logger is also a contextual logger created by With, 20 | // WithPrefix, or WithSuffix, keyvals is appended to the existing context. 21 | // 22 | // The returned Logger replaces all value elements (odd indexes) containing a 23 | // Valuer with their generated value for each call to its Log method. 24 | func With(logger Logger, keyvals ...interface{}) Logger { 25 | if len(keyvals) == 0 { 26 | return logger 27 | } 28 | l := newContext(logger) 29 | kvs := append(l.keyvals, keyvals...) 30 | if len(kvs)%2 != 0 { 31 | kvs = append(kvs, ErrMissingValue) 32 | } 33 | return &context{ 34 | logger: l.logger, 35 | // Limiting the capacity of the stored keyvals ensures that a new 36 | // backing array is created if the slice must grow in Log or With. 37 | // Using the extra capacity without copying risks a data race that 38 | // would violate the Logger interface contract. 39 | keyvals: kvs[:len(kvs):len(kvs)], 40 | hasValuer: l.hasValuer || containsValuer(keyvals), 41 | sKeyvals: l.sKeyvals, 42 | sHasValuer: l.sHasValuer, 43 | } 44 | } 45 | 46 | // WithPrefix returns a new contextual logger with keyvals prepended to those 47 | // passed to calls to Log. If logger is also a contextual logger created by 48 | // With, WithPrefix, or WithSuffix, keyvals is prepended to the existing context. 49 | // 50 | // The returned Logger replaces all value elements (odd indexes) containing a 51 | // Valuer with their generated value for each call to its Log method. 52 | func WithPrefix(logger Logger, keyvals ...interface{}) Logger { 53 | if len(keyvals) == 0 { 54 | return logger 55 | } 56 | l := newContext(logger) 57 | // Limiting the capacity of the stored keyvals ensures that a new 58 | // backing array is created if the slice must grow in Log or With. 59 | // Using the extra capacity without copying risks a data race that 60 | // would violate the Logger interface contract. 61 | n := len(l.keyvals) + len(keyvals) 62 | if len(keyvals)%2 != 0 { 63 | n++ 64 | } 65 | kvs := make([]interface{}, 0, n) 66 | kvs = append(kvs, keyvals...) 67 | if len(kvs)%2 != 0 { 68 | kvs = append(kvs, ErrMissingValue) 69 | } 70 | kvs = append(kvs, l.keyvals...) 71 | return &context{ 72 | logger: l.logger, 73 | keyvals: kvs, 74 | hasValuer: l.hasValuer || containsValuer(keyvals), 75 | sKeyvals: l.sKeyvals, 76 | sHasValuer: l.sHasValuer, 77 | } 78 | } 79 | 80 | // WithSuffix returns a new contextual logger with keyvals appended to those 81 | // passed to calls to Log. If logger is also a contextual logger created by 82 | // With, WithPrefix, or WithSuffix, keyvals is appended to the existing context. 83 | // 84 | // The returned Logger replaces all value elements (odd indexes) containing a 85 | // Valuer with their generated value for each call to its Log method. 86 | func WithSuffix(logger Logger, keyvals ...interface{}) Logger { 87 | if len(keyvals) == 0 { 88 | return logger 89 | } 90 | l := newContext(logger) 91 | // Limiting the capacity of the stored keyvals ensures that a new 92 | // backing array is created if the slice must grow in Log or With. 93 | // Using the extra capacity without copying risks a data race that 94 | // would violate the Logger interface contract. 95 | n := len(l.sKeyvals) + len(keyvals) 96 | if len(keyvals)%2 != 0 { 97 | n++ 98 | } 99 | kvs := make([]interface{}, 0, n) 100 | kvs = append(kvs, keyvals...) 101 | if len(kvs)%2 != 0 { 102 | kvs = append(kvs, ErrMissingValue) 103 | } 104 | kvs = append(l.sKeyvals, kvs...) 105 | return &context{ 106 | logger: l.logger, 107 | keyvals: l.keyvals, 108 | hasValuer: l.hasValuer, 109 | sKeyvals: kvs, 110 | sHasValuer: l.sHasValuer || containsValuer(keyvals), 111 | } 112 | } 113 | 114 | // context is the Logger implementation returned by With, WithPrefix, and 115 | // WithSuffix. It wraps a Logger and holds keyvals that it includes in all 116 | // log events. Its Log method calls bindValues to generate values for each 117 | // Valuer in the context keyvals. 118 | // 119 | // A context must always have the same number of stack frames between calls to 120 | // its Log method and the eventual binding of Valuers to their value. This 121 | // requirement comes from the functional requirement to allow a context to 122 | // resolve application call site information for a Caller stored in the 123 | // context. To do this we must be able to predict the number of logging 124 | // functions on the stack when bindValues is called. 125 | // 126 | // Two implementation details provide the needed stack depth consistency. 127 | // 128 | // 1. newContext avoids introducing an additional layer when asked to 129 | // wrap another context. 130 | // 2. With, WithPrefix, and WithSuffix avoid introducing an additional 131 | // layer by returning a newly constructed context with a merged keyvals 132 | // rather than simply wrapping the existing context. 133 | type context struct { 134 | logger Logger 135 | keyvals []interface{} 136 | sKeyvals []interface{} // suffixes 137 | hasValuer bool 138 | sHasValuer bool 139 | } 140 | 141 | func newContext(logger Logger) *context { 142 | if c, ok := logger.(*context); ok { 143 | return c 144 | } 145 | return &context{logger: logger} 146 | } 147 | 148 | // Log replaces all value elements (odd indexes) containing a Valuer in the 149 | // stored context with their generated value, appends keyvals, and passes the 150 | // result to the wrapped Logger. 151 | func (l *context) Log(keyvals ...interface{}) error { 152 | kvs := append(l.keyvals, keyvals...) 153 | if len(kvs)%2 != 0 { 154 | kvs = append(kvs, ErrMissingValue) 155 | } 156 | if l.hasValuer { 157 | // If no keyvals were appended above then we must copy l.keyvals so 158 | // that future log events will reevaluate the stored Valuers. 159 | if len(keyvals) == 0 { 160 | kvs = append([]interface{}{}, l.keyvals...) 161 | } 162 | bindValues(kvs[:(len(l.keyvals))]) 163 | } 164 | kvs = append(kvs, l.sKeyvals...) 165 | if l.sHasValuer { 166 | bindValues(kvs[len(kvs)-len(l.sKeyvals):]) 167 | } 168 | return l.logger.Log(kvs...) 169 | } 170 | 171 | // LoggerFunc is an adapter to allow use of ordinary functions as Loggers. If 172 | // f is a function with the appropriate signature, LoggerFunc(f) is a Logger 173 | // object that calls f. 174 | type LoggerFunc func(...interface{}) error 175 | 176 | // Log implements Logger by calling f(keyvals...). 177 | func (f LoggerFunc) Log(keyvals ...interface{}) error { 178 | return f(keyvals...) 179 | } 180 | -------------------------------------------------------------------------------- /log_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "bytes" 5 | "runtime" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/go-kit/log" 10 | ) 11 | 12 | func TestContext(t *testing.T) { 13 | t.Parallel() 14 | buf := &bytes.Buffer{} 15 | logger := log.NewLogfmtLogger(buf) 16 | 17 | kvs := []interface{}{"a", 123} 18 | lc := log.With(logger, kvs...) 19 | kvs[1] = 0 // With should copy its key values 20 | 21 | lc = log.With(lc, "b", "c") // With should stack 22 | if err := lc.Log("msg", "message"); err != nil { 23 | t.Fatal(err) 24 | } 25 | if want, have := "a=123 b=c msg=message\n", buf.String(); want != have { 26 | t.Errorf("\nwant: %shave: %s", want, have) 27 | } 28 | 29 | buf.Reset() 30 | lc = log.WithPrefix(lc, "p", "first") 31 | if err := lc.Log("msg", "message"); err != nil { 32 | t.Fatal(err) 33 | } 34 | if want, have := "p=first a=123 b=c msg=message\n", buf.String(); want != have { 35 | t.Errorf("\nwant: %shave: %s", want, have) 36 | } 37 | } 38 | 39 | func TestContextMissingValue(t *testing.T) { 40 | t.Parallel() 41 | var output []interface{} 42 | logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error { 43 | output = keyvals 44 | return nil 45 | })) 46 | 47 | log.WithPrefix(log.With(logger, "k1"), "k0").Log("k2") 48 | if want, have := 6, len(output); want != have { 49 | t.Errorf("want len(output) == %v, have %v", want, have) 50 | } 51 | for i := 1; i < 6; i += 2 { 52 | if want, have := log.ErrMissingValue, output[i]; want != have { 53 | t.Errorf("want output[%d] == %#v, have %#v", i, want, have) 54 | } 55 | } 56 | } 57 | 58 | func TestWithPrefixAndSuffix(t *testing.T) { 59 | t.Parallel() 60 | var output []interface{} 61 | logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error { 62 | output = keyvals 63 | return nil 64 | })) 65 | 66 | lc := log.WithPrefix(logger, "a", "first") 67 | lc = log.WithSuffix(lc, "z", "last") 68 | if err := lc.Log("msg", "message"); err != nil { 69 | t.Fatal(err) 70 | } 71 | if want, have := 6, len(output); want != have { 72 | t.Errorf("want len(output) == %v, have %v", want, have) 73 | } 74 | want := []string{"a", "first", "msg", "message", "z", "last"} 75 | for i := 0; i < 6; i++ { 76 | if want, have := want[i], output[i]; want != have { 77 | t.Errorf("want output[%d] == %#v, have %#v", i, want, have) 78 | } 79 | } 80 | 81 | lc = log.With(logger, "b", "second") 82 | lc = log.WithPrefix(lc, "a", "first") 83 | lc = log.With(lc, "c", "third") 84 | lc = log.WithSuffix(lc, "z", "last") 85 | lc = log.WithSuffix(lc, "aa", "sequel") 86 | if err := lc.Log("msg", "message"); err != nil { 87 | t.Fatal(err) 88 | } 89 | if want, have := 12, len(output); want != have { 90 | t.Errorf("want len(output) == %v, have %v", want, have) 91 | } 92 | want = []string{ 93 | "a", "first", 94 | "b", "second", 95 | "c", "third", 96 | "msg", "message", 97 | "z", "last", 98 | "aa", "sequel", 99 | } 100 | for i := 0; i < 12; i++ { 101 | if want, have := want[i], output[i]; want != have { 102 | t.Errorf("want output[%d] == %#v, have %#v", i, want, have) 103 | } 104 | } 105 | } 106 | 107 | // Test that context.Log has a consistent function stack depth when binding 108 | // Valuers, regardless of how many times With has been called. 109 | func TestContextStackDepth(t *testing.T) { 110 | t.Parallel() 111 | fn := callingFunctions()[0] 112 | 113 | var output []interface{} 114 | 115 | logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error { 116 | output = keyvals 117 | return nil 118 | })) 119 | 120 | stackValuer := log.Valuer(func() interface{} { 121 | for i, f := range callingFunctions() { 122 | if f == fn { 123 | return i 124 | } 125 | } 126 | t.Fatal("Test function not found in stack trace.") 127 | return nil 128 | }) 129 | 130 | logger = log.With(logger, "stack", stackValuer) 131 | 132 | // Call through interface to get baseline. 133 | logger.Log("k", "v") 134 | want := output[1].(int) 135 | 136 | for len(output) < 10 { 137 | logger.Log("k", "v") 138 | if have := output[1]; have != want { 139 | t.Errorf("%d Withs: have %v, want %v", len(output)/2-1, have, want) 140 | } 141 | 142 | wrapped := log.With(logger) 143 | wrapped.Log("k", "v") 144 | if have := output[1]; have != want { 145 | t.Errorf("%d Withs: have %v, want %v", len(output)/2-1, have, want) 146 | } 147 | 148 | logger = log.With(logger, "k", "v") 149 | } 150 | } 151 | 152 | // callingFunctions returns the names of the functions on the call stack for the 153 | // current goroutine with element 0 identifying the calling function. 154 | func callingFunctions() []string { 155 | pcs := make([]uintptr, 10) 156 | n := runtime.Callers(2, pcs) 157 | if n == 0 { 158 | return nil 159 | } 160 | 161 | frames := runtime.CallersFrames(pcs[:n]) 162 | funcs := make([]string, 0, n) 163 | 164 | for { 165 | frame, more := frames.Next() 166 | funcs = append(funcs, frame.Function) 167 | if !more { 168 | break 169 | } 170 | } 171 | 172 | return funcs 173 | } 174 | 175 | // Test that With returns a Logger safe for concurrent use. This test 176 | // validates that the stored logging context does not get corrupted when 177 | // multiple clients concurrently log additional keyvals. 178 | // 179 | // This test must be run with go test -cpu 2 (or more) to achieve its goal. 180 | func TestWithConcurrent(t *testing.T) { 181 | // Create some buckets to count how many events each goroutine logs. 182 | const goroutines = 8 183 | counts := [goroutines]int{} 184 | 185 | // This logger extracts a goroutine id from the last value field and 186 | // increments the referenced bucket. 187 | logger := log.LoggerFunc(func(kv ...interface{}) error { 188 | goroutine := kv[len(kv)-1].(int) 189 | counts[goroutine]++ 190 | return nil 191 | }) 192 | 193 | // With must be careful about handling slices that can grow without 194 | // copying the underlying array, so give it a challenge. 195 | l := log.With(logger, make([]interface{}, 0, 2)...) 196 | 197 | // Start logging concurrently. Each goroutine logs its id so the logger 198 | // can bucket the event counts. 199 | var wg sync.WaitGroup 200 | wg.Add(goroutines) 201 | const n = 10000 202 | for i := 0; i < goroutines; i++ { 203 | go func(idx int) { 204 | defer wg.Done() 205 | for j := 0; j < n; j++ { 206 | l.Log("goroutineIdx", idx) 207 | } 208 | }(i) 209 | } 210 | wg.Wait() 211 | 212 | for bucket, have := range counts { 213 | if want := n; want != have { 214 | t.Errorf("bucket %d: want %d, have %d", bucket, want, have) // note Errorf 215 | } 216 | } 217 | } 218 | 219 | func TestLogCopiesValuers(t *testing.T) { 220 | t.Parallel() 221 | var output []interface{} 222 | logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error { 223 | output = keyvals 224 | return nil 225 | })) 226 | 227 | valuerCallCount := 0 228 | counterValuer := log.Valuer(func() interface{} { 229 | valuerCallCount++ 230 | return valuerCallCount 231 | }) 232 | lc := log.WithPrefix(logger, "a", counterValuer) 233 | lc = log.WithSuffix(lc, "z", counterValuer) 234 | 235 | if err := lc.Log(); err != nil { 236 | t.Fatal(err) 237 | } 238 | want := []interface{}{"a", 1, "z", 2} 239 | for i := 0; i < 4; i++ { 240 | if want, have := want[i], output[i]; want != have { 241 | t.Errorf("want output[%d] == %#v, have %#v", i, want, have) 242 | } 243 | } 244 | 245 | if err := lc.Log(); err != nil { 246 | t.Fatal(err) 247 | } 248 | want = []interface{}{"a", 3, "z", 4} 249 | for i := 0; i < 4; i++ { 250 | if want, have := want[i], output[i]; want != have { 251 | t.Errorf("want output[%d] == %#v, have %#v", i, want, have) 252 | } 253 | } 254 | } 255 | 256 | func BenchmarkDiscard(b *testing.B) { 257 | logger := log.NewNopLogger() 258 | b.ReportAllocs() 259 | b.ResetTimer() 260 | for i := 0; i < b.N; i++ { 261 | logger.Log("k", "v") 262 | } 263 | } 264 | 265 | func BenchmarkOneWith(b *testing.B) { 266 | logger := log.NewNopLogger() 267 | lc := log.With(logger, "k", "v") 268 | b.ReportAllocs() 269 | b.ResetTimer() 270 | for i := 0; i < b.N; i++ { 271 | lc.Log("k", "v") 272 | } 273 | } 274 | 275 | func BenchmarkTwoWith(b *testing.B) { 276 | logger := log.NewNopLogger() 277 | lc := log.With(logger, "k", "v") 278 | for i := 1; i < 2; i++ { 279 | lc = log.With(lc, "k", "v") 280 | } 281 | b.ReportAllocs() 282 | b.ResetTimer() 283 | for i := 0; i < b.N; i++ { 284 | lc.Log("k", "v") 285 | } 286 | } 287 | 288 | func BenchmarkTenWith(b *testing.B) { 289 | logger := log.NewNopLogger() 290 | lc := log.With(logger, "k", "v") 291 | for i := 1; i < 10; i++ { 292 | lc = log.With(lc, "k", "v") 293 | } 294 | b.ReportAllocs() 295 | b.ResetTimer() 296 | for i := 0; i < b.N; i++ { 297 | lc.Log("k", "v") 298 | } 299 | } 300 | 301 | func BenchmarkOneWithPrefix(b *testing.B) { 302 | logger := log.NewNopLogger() 303 | lc := log.WithPrefix(logger, "a", "first") 304 | b.ReportAllocs() 305 | b.ResetTimer() 306 | for i := 0; i < b.N; i++ { 307 | lc.Log("k", "v") 308 | } 309 | } 310 | 311 | func BenchmarkTenWithPrefix(b *testing.B) { 312 | logger := log.NewNopLogger() 313 | lc := log.WithPrefix(logger, "a", "first") 314 | for i := 1; i < 10; i++ { 315 | lc = log.WithPrefix(lc, "a", "first") 316 | } 317 | b.ReportAllocs() 318 | b.ResetTimer() 319 | for i := 0; i < b.N; i++ { 320 | lc.Log("k", "v") 321 | } 322 | } 323 | 324 | func BenchmarkOneWithSuffix(b *testing.B) { 325 | logger := log.NewNopLogger() 326 | lc := log.WithSuffix(logger, "z", "last") 327 | b.ReportAllocs() 328 | b.ResetTimer() 329 | for i := 0; i < b.N; i++ { 330 | lc.Log("k", "v") 331 | } 332 | } 333 | 334 | func BenchmarkTenWithSuffix(b *testing.B) { 335 | logger := log.NewNopLogger() 336 | lc := log.WithSuffix(logger, "z", "last") 337 | for i := 1; i < 10; i++ { 338 | lc = log.WithSuffix(lc, "z", "last") 339 | } 340 | b.ReportAllocs() 341 | b.ResetTimer() 342 | for i := 0; i < b.N; i++ { 343 | lc.Log("k", "v") 344 | } 345 | } 346 | 347 | func BenchmarkOneWithPrefixSuffix(b *testing.B) { 348 | logger := log.NewNopLogger() 349 | lc := log.WithSuffix(logger, "a", "first") 350 | lc = log.WithSuffix(lc, "z", "last") 351 | b.ReportAllocs() 352 | b.ResetTimer() 353 | for i := 0; i < b.N; i++ { 354 | lc.Log("k", "v") 355 | } 356 | } 357 | 358 | func BenchmarkTenWithPrefixSuffix(b *testing.B) { 359 | logger := log.NewNopLogger() 360 | lc := log.WithPrefix(logger, "a", "first") 361 | lc = log.WithSuffix(lc, "z", "last") 362 | for i := 1; i < 10; i++ { 363 | lc = log.WithPrefix(lc, "a", "first") 364 | lc = log.WithSuffix(lc, "z", "last") 365 | } 366 | b.ReportAllocs() 367 | b.ResetTimer() 368 | for i := 0; i < b.N; i++ { 369 | lc.Log("k", "v") 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /logfmt_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | 8 | "github.com/go-logfmt/logfmt" 9 | ) 10 | 11 | type logfmtEncoder struct { 12 | *logfmt.Encoder 13 | buf bytes.Buffer 14 | } 15 | 16 | func (l *logfmtEncoder) Reset() { 17 | l.Encoder.Reset() 18 | l.buf.Reset() 19 | } 20 | 21 | var logfmtEncoderPool = sync.Pool{ 22 | New: func() interface{} { 23 | var enc logfmtEncoder 24 | enc.Encoder = logfmt.NewEncoder(&enc.buf) 25 | return &enc 26 | }, 27 | } 28 | 29 | type logfmtLogger struct { 30 | w io.Writer 31 | } 32 | 33 | // NewLogfmtLogger returns a logger that encodes keyvals to the Writer in 34 | // logfmt format. Each log event produces no more than one call to w.Write. 35 | // The passed Writer must be safe for concurrent use by multiple goroutines if 36 | // the returned Logger will be used concurrently. 37 | func NewLogfmtLogger(w io.Writer) Logger { 38 | return &logfmtLogger{w} 39 | } 40 | 41 | func (l logfmtLogger) Log(keyvals ...interface{}) error { 42 | enc := logfmtEncoderPool.Get().(*logfmtEncoder) 43 | enc.Reset() 44 | defer logfmtEncoderPool.Put(enc) 45 | 46 | if err := enc.EncodeKeyvals(keyvals...); err != nil { 47 | return err 48 | } 49 | 50 | // Add newline to the end of the buffer 51 | if err := enc.EndRecord(); err != nil { 52 | return err 53 | } 54 | 55 | // The Logger interface requires implementations to be safe for concurrent 56 | // use by multiple goroutines. For this implementation that means making 57 | // only one call to l.w.Write() for each call to Log. 58 | if _, err := l.w.Write(enc.buf.Bytes()); err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /logfmt_logger_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/go-kit/log" 10 | "github.com/go-logfmt/logfmt" 11 | ) 12 | 13 | func TestLogfmtLogger(t *testing.T) { 14 | t.Parallel() 15 | buf := &bytes.Buffer{} 16 | logger := log.NewLogfmtLogger(buf) 17 | 18 | if err := logger.Log("hello", "world"); err != nil { 19 | t.Fatal(err) 20 | } 21 | if want, have := "hello=world\n", buf.String(); want != have { 22 | t.Errorf("want %#v, have %#v", want, have) 23 | } 24 | 25 | buf.Reset() 26 | if err := logger.Log("a", 1, "err", errors.New("error")); err != nil { 27 | t.Fatal(err) 28 | } 29 | if want, have := "a=1 err=error\n", buf.String(); want != have { 30 | t.Errorf("want %#v, have %#v", want, have) 31 | } 32 | 33 | buf.Reset() 34 | if err := logger.Log("std_map", map[int]int{1: 2}, "my_map", mymap{0: 0}); err != nil { 35 | t.Fatal(err) 36 | } 37 | if want, have := "std_map=\""+logfmt.ErrUnsupportedValueType.Error()+"\" my_map=special_behavior\n", buf.String(); want != have { 38 | t.Errorf("want %#v, have %#v", want, have) 39 | } 40 | } 41 | 42 | func BenchmarkLogfmtLoggerSimple(b *testing.B) { 43 | benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard), baseMessage) 44 | } 45 | 46 | func BenchmarkLogfmtLoggerContextual(b *testing.B) { 47 | benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard), withMessage) 48 | } 49 | 50 | func TestLogfmtLoggerConcurrency(t *testing.T) { 51 | t.Parallel() 52 | testConcurrency(t, log.NewLogfmtLogger(ioutil.Discard), 10000) 53 | } 54 | 55 | type mymap map[int]int 56 | 57 | func (m mymap) String() string { return "special_behavior" } 58 | -------------------------------------------------------------------------------- /nop_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | type nopLogger struct{} 4 | 5 | // NewNopLogger returns a logger that doesn't do anything. 6 | func NewNopLogger() Logger { return nopLogger{} } 7 | 8 | func (nopLogger) Log(...interface{}) error { return nil } 9 | -------------------------------------------------------------------------------- /nop_logger_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-kit/log" 7 | ) 8 | 9 | func TestNopLogger(t *testing.T) { 10 | t.Parallel() 11 | logger := log.NewNopLogger() 12 | if err := logger.Log("abc", 123); err != nil { 13 | t.Error(err) 14 | } 15 | if err := log.With(logger, "def", "ghi").Log(); err != nil { 16 | t.Error(err) 17 | } 18 | } 19 | 20 | func BenchmarkNopLoggerSimple(b *testing.B) { 21 | benchmarkRunner(b, log.NewNopLogger(), baseMessage) 22 | } 23 | 24 | func BenchmarkNopLoggerContextual(b *testing.B) { 25 | benchmarkRunner(b, log.NewNopLogger(), withMessage) 26 | } 27 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all"] 2 | -------------------------------------------------------------------------------- /stdlib.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // StdlibWriter implements io.Writer by invoking the stdlib log.Print. It's 12 | // designed to be passed to a Go kit logger as the writer, for cases where 13 | // it's necessary to redirect all Go kit log output to the stdlib logger. 14 | // 15 | // If you have any choice in the matter, you shouldn't use this. Prefer to 16 | // redirect the stdlib log to the Go kit logger via NewStdlibAdapter. 17 | type StdlibWriter struct{} 18 | 19 | // Write implements io.Writer. 20 | func (w StdlibWriter) Write(p []byte) (int, error) { 21 | log.Print(strings.TrimSpace(string(p))) 22 | return len(p), nil 23 | } 24 | 25 | // StdlibAdapter wraps a Logger and allows it to be passed to the stdlib 26 | // logger's SetOutput. It will extract date/timestamps, filenames, and 27 | // messages, and place them under relevant keys. 28 | type StdlibAdapter struct { 29 | Logger 30 | timestampKey string 31 | fileKey string 32 | messageKey string 33 | prefix string 34 | joinPrefixToMsg bool 35 | logRegexp *regexp.Regexp 36 | } 37 | 38 | // StdlibAdapterOption sets a parameter for the StdlibAdapter. 39 | type StdlibAdapterOption func(*StdlibAdapter) 40 | 41 | // TimestampKey sets the key for the timestamp field. By default, it's "ts". 42 | func TimestampKey(key string) StdlibAdapterOption { 43 | return func(a *StdlibAdapter) { a.timestampKey = key } 44 | } 45 | 46 | // FileKey sets the key for the file and line field. By default, it's "caller". 47 | func FileKey(key string) StdlibAdapterOption { 48 | return func(a *StdlibAdapter) { a.fileKey = key } 49 | } 50 | 51 | // MessageKey sets the key for the actual log message. By default, it's "msg". 52 | func MessageKey(key string) StdlibAdapterOption { 53 | return func(a *StdlibAdapter) { a.messageKey = key } 54 | } 55 | 56 | // StdlibRegexp sets the regular expression used to parse stdlib log messages. 57 | // 58 | // The regexp is expected to contain specific named capture groups. The "date" 59 | // and "time" capture groups are combined for the timestamp. The "file" capture 60 | // group (which usually also includes line) is used for the caller. The "msg" 61 | // capture group is used for the actual log message. 62 | // 63 | // Nil regexps are ignored and will return options that are no-ops. The default 64 | // value is StdlibRegexpFull. 65 | func StdlibRegexp(re *regexp.Regexp) StdlibAdapterOption { 66 | if re == nil { 67 | return func(a *StdlibAdapter) {} 68 | } 69 | return func(a *StdlibAdapter) { a.logRegexp = re } 70 | } 71 | 72 | // Prefix configures the adapter to parse a prefix from stdlib log events. If 73 | // you provide a non-empty prefix to the stdlib logger, then your should provide 74 | // that same prefix to the adapter via this option. 75 | // 76 | // By default, the prefix isn't included in the msg key. Set joinPrefixToMsg to 77 | // true if you want to include the parsed prefix in the msg. 78 | func Prefix(prefix string, joinPrefixToMsg bool) StdlibAdapterOption { 79 | return func(a *StdlibAdapter) { a.prefix = prefix; a.joinPrefixToMsg = joinPrefixToMsg } 80 | } 81 | 82 | // NewStdlibAdapter returns a new StdlibAdapter wrapper around the passed 83 | // logger. It's designed to be passed to log.SetOutput. 84 | func NewStdlibAdapter(logger Logger, options ...StdlibAdapterOption) io.Writer { 85 | a := StdlibAdapter{ 86 | Logger: logger, 87 | timestampKey: "ts", 88 | fileKey: "caller", 89 | messageKey: "msg", 90 | logRegexp: StdlibRegexpFull, 91 | } 92 | for _, option := range options { 93 | option(&a) 94 | } 95 | 96 | return a 97 | } 98 | 99 | func (a StdlibAdapter) Write(p []byte) (int, error) { 100 | p = a.handlePrefix(p) 101 | 102 | result := a.subexps(p) 103 | keyvals := []interface{}{} 104 | var timestamp string 105 | if date, ok := result["date"]; ok && date != "" { 106 | timestamp = date 107 | } 108 | if time, ok := result["time"]; ok && time != "" { 109 | if timestamp != "" { 110 | timestamp += " " 111 | } 112 | timestamp += time 113 | } 114 | if timestamp != "" { 115 | keyvals = append(keyvals, a.timestampKey, timestamp) 116 | } 117 | if file, ok := result["file"]; ok && file != "" { 118 | keyvals = append(keyvals, a.fileKey, file) 119 | } 120 | if msg, ok := result["msg"]; ok { 121 | msg = a.handleMessagePrefix(msg) 122 | keyvals = append(keyvals, a.messageKey, msg) 123 | } 124 | if err := a.Logger.Log(keyvals...); err != nil { 125 | return 0, err 126 | } 127 | return len(p), nil 128 | } 129 | 130 | func (a StdlibAdapter) handlePrefix(p []byte) []byte { 131 | if a.prefix != "" { 132 | p = bytes.TrimPrefix(p, []byte(a.prefix)) 133 | } 134 | return p 135 | } 136 | 137 | func (a StdlibAdapter) handleMessagePrefix(msg string) string { 138 | if a.prefix == "" { 139 | return msg 140 | } 141 | 142 | msg = strings.TrimPrefix(msg, a.prefix) 143 | if a.joinPrefixToMsg { 144 | msg = a.prefix + msg 145 | } 146 | return msg 147 | } 148 | 149 | const ( 150 | stdlibRegexpPatternDate = `(?P[0-9]{4}/[0-9]{2}/[0-9]{2})?[ ]?` 151 | stdlibRegexpPatternTime = `(?P