├── .github ├── CODEOWNERS └── workflows │ └── test.yml ├── .gitignore ├── .go-version ├── .golangci.yml ├── LICENSE ├── README.md ├── colorize_unix.go ├── colorize_windows.go ├── context.go ├── context_test.go ├── exclude.go ├── exclude_test.go ├── global.go ├── go.mod ├── go.sum ├── hclogvet ├── .gitignore ├── Makefile ├── README.md ├── example │ ├── go.mod │ ├── go.sum │ └── log.go ├── go.mod ├── go.sum └── main.go ├── interceptlogger.go ├── interceptlogger_test.go ├── intlogger.go ├── logger.go ├── logger_loc_test.go ├── logger_test.go ├── nulllogger.go ├── nulllogger_test.go ├── stacktrace.go ├── stdlog.go ├── stdlog_test.go └── writer.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: go-hclog 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | go-version: 15 | - 'oldstable' 16 | - 'stable' 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | steps: 19 | - name: Install Go 20 | uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | - name: Checkout 24 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 25 | - name: Cache GolangCI-Lint 26 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 27 | with: 28 | path: ~/.cache/golangci-lint 29 | key: golangci-lint-${{ runner.os }}-${{ hashFiles('**/go.sum') }} 30 | restore-keys: golangci-lint-${{ runner.os }}- 31 | - name: Cache Go Modules 32 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 33 | with: 34 | path: | 35 | ~/go/pkg/mod 36 | ~/.cache/go-build 37 | key: go-mod-${{ runner.os }}-${{ hashFiles('**/go.sum') }} 38 | restore-keys: go-mod-${{ runner.os }}- 39 | - name: Run golangci-lint 40 | uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 41 | with: 42 | version: latest 43 | args: --timeout=5m --verbose 44 | only-new-issues: true 45 | - name: Run Tests with coverage 46 | run: go test -v -coverprofile="coverage.out" ./... 47 | - name: Upload coverage report 48 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 49 | with: 50 | path: coverage.out 51 | name: Coverage-report-${{ matrix.os }}-${{ matrix.go-version }} 52 | - name: Display coverage report # displaying only for linux & macOS 53 | if: ${{runner.os != 'Windows'}} 54 | run: go tool cover -func=coverage.out 55 | - name: Build Go 56 | run: | 57 | cd hclogvet 58 | go build ./... 59 | cd example 60 | go build ./... 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea* -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.23 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MIT 3 | 4 | linters: 5 | enable: 6 | - errcheck 7 | - gosimple 8 | output_format: colored-line-number 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 HashiCorp, 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 all 11 | 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 THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-hclog 2 | 3 | [![Go Documentation](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godocs] 4 | 5 | [godocs]: https://godoc.org/github.com/hashicorp/go-hclog 6 | 7 | `go-hclog` is a package for Go that provides a simple key/value logging 8 | interface for use in development and production environments. 9 | 10 | It provides logging levels that provide decreased output based upon the 11 | desired amount of output, unlike the standard library `log` package. 12 | 13 | It provides `Printf` style logging of values via `hclog.Fmt()`. 14 | 15 | It provides a human readable output mode for use in development as well as 16 | JSON output mode for production. 17 | 18 | ## Stability Note 19 | 20 | This library has reached 1.0 stability. Its API can be considered solidified 21 | and promised through future versions. 22 | 23 | ## Installation and Docs 24 | 25 | Install using `go get github.com/hashicorp/go-hclog`. 26 | 27 | Full documentation is available at 28 | http://godoc.org/github.com/hashicorp/go-hclog 29 | 30 | ## Usage 31 | 32 | ### Use the global logger 33 | 34 | ```go 35 | hclog.Default().Info("hello world") 36 | ``` 37 | 38 | ```text 39 | 2017-07-05T16:15:55.167-0700 [INFO ] hello world 40 | ``` 41 | 42 | (Note timestamps are removed in future examples for brevity.) 43 | 44 | ### Create a new logger 45 | 46 | ```go 47 | appLogger := hclog.New(&hclog.LoggerOptions{ 48 | Name: "my-app", 49 | Level: hclog.LevelFromString("DEBUG"), 50 | }) 51 | ``` 52 | 53 | ### Emit an Info level message with 2 key/value pairs 54 | 55 | ```go 56 | input := "5.5" 57 | _, err := strconv.ParseInt(input, 10, 32) 58 | if err != nil { 59 | appLogger.Info("Invalid input for ParseInt", "input", input, "error", err) 60 | } 61 | ``` 62 | 63 | ```text 64 | ... [INFO ] my-app: Invalid input for ParseInt: input=5.5 error="strconv.ParseInt: parsing "5.5": invalid syntax" 65 | ``` 66 | 67 | ### Create a new Logger for a major subsystem 68 | 69 | ```go 70 | subsystemLogger := appLogger.Named("transport") 71 | subsystemLogger.Info("we are transporting something") 72 | ``` 73 | 74 | ```text 75 | ... [INFO ] my-app.transport: we are transporting something 76 | ``` 77 | 78 | Notice that logs emitted by `subsystemLogger` contain `my-app.transport`, 79 | reflecting both the application and subsystem names. 80 | 81 | ### Create a new Logger with fixed key/value pairs 82 | 83 | Using `With()` will include a specific key-value pair in all messages emitted 84 | by that logger. 85 | 86 | ```go 87 | requestID := "5fb446b6-6eba-821d-df1b-cd7501b6a363" 88 | requestLogger := subsystemLogger.With("request", requestID) 89 | requestLogger.Info("we are transporting a request") 90 | ``` 91 | 92 | ```text 93 | ... [INFO ] my-app.transport: we are transporting a request: request=5fb446b6-6eba-821d-df1b-cd7501b6a363 94 | ``` 95 | 96 | This allows sub Loggers to be context specific without having to thread that 97 | into all the callers. 98 | 99 | ### Using `hclog.Fmt()` 100 | 101 | ```go 102 | totalBandwidth := 200 103 | appLogger.Info("total bandwidth exceeded", "bandwidth", hclog.Fmt("%d GB/s", totalBandwidth)) 104 | ``` 105 | 106 | ```text 107 | ... [INFO ] my-app: total bandwidth exceeded: bandwidth="200 GB/s" 108 | ``` 109 | 110 | ### Use this with code that uses the standard library logger 111 | 112 | If you want to use the standard library's `log.Logger` interface you can wrap 113 | `hclog.Logger` by calling the `StandardLogger()` method. This allows you to use 114 | it with the familiar `Println()`, `Printf()`, etc. For example: 115 | 116 | ```go 117 | stdLogger := appLogger.StandardLogger(&hclog.StandardLoggerOptions{ 118 | InferLevels: true, 119 | }) 120 | // Printf() is provided by stdlib log.Logger interface, not hclog.Logger 121 | stdLogger.Printf("[DEBUG] %+v", stdLogger) 122 | ``` 123 | 124 | ```text 125 | ... [DEBUG] my-app: &{mu:{state:0 sema:0} prefix: flag:0 out:0xc42000a0a0 buf:[]} 126 | ``` 127 | 128 | Alternatively, you may configure the system-wide logger: 129 | 130 | ```go 131 | // log the standard logger from 'import "log"' 132 | log.SetOutput(appLogger.StandardWriter(&hclog.StandardLoggerOptions{InferLevels: true})) 133 | log.SetPrefix("") 134 | log.SetFlags(0) 135 | 136 | log.Printf("[DEBUG] %d", 42) 137 | ``` 138 | 139 | ```text 140 | ... [DEBUG] my-app: 42 141 | ``` 142 | 143 | Notice that if `appLogger` is initialized with the `INFO` log level, _and_ you 144 | specify `InferLevels: true`, you will not see any output here. You must change 145 | `appLogger` to `DEBUG` to see output. See the docs for more information. 146 | 147 | If the log lines start with a timestamp you can use the 148 | `InferLevelsWithTimestamp` option to try and ignore them. Please note that in order 149 | for `InferLevelsWithTimestamp` to be relevant, `InferLevels` must be set to `true`. 150 | -------------------------------------------------------------------------------- /colorize_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package hclog 8 | 9 | import ( 10 | "github.com/mattn/go-isatty" 11 | ) 12 | 13 | // hasFD is used to check if the writer has an Fd value to check 14 | // if it's a terminal. 15 | type hasFD interface { 16 | Fd() uintptr 17 | } 18 | 19 | // setColorization will mutate the values of this logger 20 | // to appropriately configure colorization options. It provides 21 | // a wrapper to the output stream on Windows systems. 22 | func (l *intLogger) setColorization(opts *LoggerOptions) { 23 | if opts.Color != AutoColor { 24 | return 25 | } 26 | 27 | if sc, ok := l.writer.w.(SupportsColor); ok { 28 | if !sc.SupportsColor() { 29 | l.headerColor = ColorOff 30 | l.writer.color = ColorOff 31 | } 32 | return 33 | } 34 | 35 | fi, ok := l.writer.w.(hasFD) 36 | if !ok { 37 | return 38 | } 39 | 40 | if !isatty.IsTerminal(fi.Fd()) { 41 | l.headerColor = ColorOff 42 | l.writer.color = ColorOff 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /colorize_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package hclog 8 | 9 | import ( 10 | "os" 11 | 12 | colorable "github.com/mattn/go-colorable" 13 | ) 14 | 15 | // setColorization will mutate the values of this logger 16 | // to appropriately configure colorization options. It provides 17 | // a wrapper to the output stream on Windows systems. 18 | func (l *intLogger) setColorization(opts *LoggerOptions) { 19 | if opts.Color == ColorOff { 20 | return 21 | } 22 | 23 | fi, ok := l.writer.w.(*os.File) 24 | if !ok { 25 | l.writer.color = ColorOff 26 | l.headerColor = ColorOff 27 | return 28 | } 29 | 30 | cfi := colorable.NewColorable(fi) 31 | 32 | // NewColorable detects if color is possible and if it's not, then it 33 | // returns the original value. So we can test if we got the original 34 | // value back to know if color is possible. 35 | if cfi == fi { 36 | l.writer.color = ColorOff 37 | l.headerColor = ColorOff 38 | } else { 39 | l.writer.w = cfi 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "context" 8 | ) 9 | 10 | // WithContext inserts a logger into the context and is retrievable 11 | // with FromContext. The optional args can be set with the same syntax as 12 | // Logger.With to set fields on the inserted logger. This will not modify 13 | // the logger argument in-place. 14 | func WithContext(ctx context.Context, logger Logger, args ...interface{}) context.Context { 15 | // While we could call logger.With even with zero args, we have this 16 | // check to avoid unnecessary allocations around creating a copy of a 17 | // logger. 18 | if len(args) > 0 { 19 | logger = logger.With(args...) 20 | } 21 | 22 | return context.WithValue(ctx, contextKey, logger) 23 | } 24 | 25 | // FromContext returns a logger from the context. This will return L() 26 | // (the default logger) if no logger is found in the context. Therefore, 27 | // this will never return a nil value. 28 | func FromContext(ctx context.Context) Logger { 29 | logger, _ := ctx.Value(contextKey).(Logger) 30 | if logger == nil { 31 | return L() 32 | } 33 | 34 | return logger 35 | } 36 | 37 | // Unexported new type so that our context key never collides with another. 38 | type contextKeyType struct{} 39 | 40 | // contextKey is the key used for the context to store the logger. 41 | var contextKey = contextKeyType{} 42 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestContext_simpleLogger(t *testing.T) { 15 | l := L() 16 | ctx := WithContext(context.Background(), l) 17 | require.Equal(t, l, FromContext(ctx)) 18 | } 19 | 20 | func TestContext_empty(t *testing.T) { 21 | require.Equal(t, L(), FromContext(context.Background())) 22 | } 23 | 24 | func TestContext_fields(t *testing.T) { 25 | var buf bytes.Buffer 26 | l := New(&LoggerOptions{ 27 | Level: Debug, 28 | Output: &buf, 29 | }) 30 | 31 | // Insert the logger with fields 32 | ctx := WithContext(context.Background(), l, "hello", "world") 33 | l = FromContext(ctx) 34 | require.NotNil(t, l) 35 | 36 | // Log something so we can test the output that the field is there 37 | l.Debug("test") 38 | require.Contains(t, buf.String(), "hello") 39 | } 40 | -------------------------------------------------------------------------------- /exclude.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // ExcludeByMessage provides a simple way to build a list of log messages that 12 | // can be queried and matched. This is meant to be used with the Exclude 13 | // option on Options to suppress log messages. This does not hold any mutexs 14 | // within itself, so normal usage would be to Add entries at setup and none after 15 | // Exclude is going to be called. Exclude is called with a mutex held within 16 | // the Logger, so that doesn't need to use a mutex. Example usage: 17 | // 18 | // f := new(ExcludeByMessage) 19 | // f.Add("Noisy log message text") 20 | // appLogger.Exclude = f.Exclude 21 | type ExcludeByMessage struct { 22 | messages map[string]struct{} 23 | } 24 | 25 | // Add a message to be filtered. Do not call this after Exclude is to be called 26 | // due to concurrency issues. 27 | func (f *ExcludeByMessage) Add(msg string) { 28 | if f.messages == nil { 29 | f.messages = make(map[string]struct{}) 30 | } 31 | 32 | f.messages[msg] = struct{}{} 33 | } 34 | 35 | // Return true if the given message should be included 36 | func (f *ExcludeByMessage) Exclude(level Level, msg string, args ...interface{}) bool { 37 | _, ok := f.messages[msg] 38 | return ok 39 | } 40 | 41 | // ExcludeByPrefix is a simple type to match a message string that has a common prefix. 42 | type ExcludeByPrefix string 43 | 44 | // Matches an message that starts with the prefix. 45 | func (p ExcludeByPrefix) Exclude(level Level, msg string, args ...interface{}) bool { 46 | return strings.HasPrefix(msg, string(p)) 47 | } 48 | 49 | // ExcludeByRegexp takes a regexp and uses it to match a log message string. If it matches 50 | // the log entry is excluded. 51 | type ExcludeByRegexp struct { 52 | Regexp *regexp.Regexp 53 | } 54 | 55 | // Exclude the log message if the message string matches the regexp 56 | func (e ExcludeByRegexp) Exclude(level Level, msg string, args ...interface{}) bool { 57 | return e.Regexp.MatchString(msg) 58 | } 59 | 60 | // ExcludeFuncs is a slice of functions that will called to see if a log entry 61 | // should be filtered or not. It stops calling functions once at least one returns 62 | // true. 63 | type ExcludeFuncs []func(level Level, msg string, args ...interface{}) bool 64 | 65 | // Calls each function until one of them returns true 66 | func (ff ExcludeFuncs) Exclude(level Level, msg string, args ...interface{}) bool { 67 | for _, f := range ff { 68 | if f(level, msg, args...) { 69 | return true 70 | } 71 | } 72 | 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /exclude_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestExclude(t *testing.T) { 14 | t.Run("excludes by message", func(t *testing.T) { 15 | var em ExcludeByMessage 16 | em.Add("foo") 17 | em.Add("bar") 18 | 19 | assert.True(t, em.Exclude(Info, "foo")) 20 | assert.True(t, em.Exclude(Info, "bar")) 21 | assert.False(t, em.Exclude(Info, "qux")) 22 | assert.False(t, em.Exclude(Info, "foo qux")) 23 | assert.False(t, em.Exclude(Info, "qux bar")) 24 | }) 25 | 26 | t.Run("excludes by prefix", func(t *testing.T) { 27 | ebp := ExcludeByPrefix("foo: ") 28 | 29 | assert.True(t, ebp.Exclude(Info, "foo: rocks")) 30 | assert.False(t, ebp.Exclude(Info, "foo")) 31 | assert.False(t, ebp.Exclude(Info, "qux foo: bar")) 32 | }) 33 | 34 | t.Run("exclude by regexp", func(t *testing.T) { 35 | ebr := &ExcludeByRegexp{ 36 | Regexp: regexp.MustCompile("(foo|bar)"), 37 | } 38 | 39 | assert.True(t, ebr.Exclude(Info, "foo")) 40 | assert.True(t, ebr.Exclude(Info, "bar")) 41 | assert.True(t, ebr.Exclude(Info, "foo qux")) 42 | assert.True(t, ebr.Exclude(Info, "qux bar")) 43 | assert.False(t, ebr.Exclude(Info, "qux")) 44 | }) 45 | 46 | t.Run("excludes many funcs", func(t *testing.T) { 47 | ef := ExcludeFuncs{ 48 | ExcludeByPrefix("foo: ").Exclude, 49 | ExcludeByPrefix("bar: ").Exclude, 50 | } 51 | 52 | assert.True(t, ef.Exclude(Info, "foo: rocks")) 53 | assert.True(t, ef.Exclude(Info, "bar: rocks")) 54 | assert.False(t, ef.Exclude(Info, "foo")) 55 | assert.False(t, ef.Exclude(Info, "qux foo: bar")) 56 | 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var ( 12 | protect sync.Once 13 | def Logger 14 | 15 | // DefaultOptions is used to create the Default logger. These are read 16 | // only when the Default logger is created, so set them as soon as the 17 | // process starts. 18 | DefaultOptions = &LoggerOptions{ 19 | Level: DefaultLevel, 20 | Output: DefaultOutput, 21 | TimeFn: time.Now, 22 | } 23 | ) 24 | 25 | // Default returns a globally held logger. This can be a good starting 26 | // place, and then you can use .With() and .Named() to create sub-loggers 27 | // to be used in more specific contexts. 28 | // The value of the Default logger can be set via SetDefault() or by 29 | // changing the options in DefaultOptions. 30 | // 31 | // This method is goroutine safe, returning a global from memory, but 32 | // care should be used if SetDefault() is called it random times 33 | // in the program as that may result in race conditions and an unexpected 34 | // Logger being returned. 35 | func Default() Logger { 36 | protect.Do(func() { 37 | // If SetDefault was used before Default() was called, we need to 38 | // detect that here. 39 | if def == nil { 40 | def = New(DefaultOptions) 41 | } 42 | }) 43 | 44 | return def 45 | } 46 | 47 | // L is a short alias for Default(). 48 | func L() Logger { 49 | return Default() 50 | } 51 | 52 | // SetDefault changes the logger to be returned by Default()and L() 53 | // to the one given. This allows packages to use the default logger 54 | // and have higher level packages change it to match the execution 55 | // environment. It returns any old default if there is one. 56 | // 57 | // NOTE: This is expected to be called early in the program to setup 58 | // a default logger. As such, it does not attempt to make itself 59 | // not racy with regard to the value of the default logger. Ergo 60 | // if it is called in goroutines, you may experience race conditions 61 | // with other goroutines retrieving the default logger. Basically, 62 | // don't do that. 63 | func SetDefault(log Logger) Logger { 64 | old := def 65 | def = log 66 | return old 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-hclog 2 | 3 | require ( 4 | github.com/fatih/color v1.18.0 5 | github.com/mattn/go-colorable v0.1.14 6 | github.com/mattn/go-isatty v0.0.20 7 | github.com/stretchr/testify v1.7.2 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | golang.org/x/sys v0.30.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | 17 | go 1.23 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 5 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 6 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 7 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 8 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 9 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 14 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 15 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 17 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /hclogvet/.gitignore: -------------------------------------------------------------------------------- 1 | /hclogvet 2 | -------------------------------------------------------------------------------- /hclogvet/Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | PROGRAM_NAME := hclogvet 4 | 5 | .PHONY: bin 6 | bin: $(PROGRAM_NAME) 7 | $(PROGRAM_NAME): *.go go.mod go.sum 8 | @go build 9 | 10 | .PHONY: install 11 | install: 12 | @go install 13 | -------------------------------------------------------------------------------- /hclogvet/README.md: -------------------------------------------------------------------------------- 1 | # hclogvet 2 | 3 | `hclogvet` is a `go vet` tool for checking that the Trace/Debug/Info/Warn/Error 4 | methods on `hclog.Logger` are used correctly. 5 | 6 | ## Usage 7 | 8 | This may be used in two ways. It may be invoked directly: 9 | 10 | $ hclogvet . 11 | /full/path/to/project/log.go:25:8: invalid number of log arguments to Info (1 valid pair only) 12 | 13 | Or via `go vet`: 14 | 15 | $ go vet -vettool=$(which hclogvet) 16 | # full/module/path 17 | ./log.go:25:8: invalid number of log arguments to Info (1 valid pair only) 18 | 19 | ## Details 20 | 21 | These methods expect an odd number of arguments as in: 22 | 23 | logger.Info("valid login", "account", accountID, "ip", addr) 24 | 25 | The leading argument is a message string, and then the remainder of the 26 | arguments are key/value pairs of additional structured data to log to provide 27 | context. 28 | 29 | `hclogvet` will detect unfortunate errors like: 30 | 31 | logger.Error("raft request failed: %v", err) 32 | logger.Error("error opening file", err) 33 | logger.Debug("too many connections", numConnections, "ip", ipAddr) 34 | 35 | So that the author can correct them: 36 | 37 | logger.Error("raft request failed", "error", err) 38 | logger.Error("error opening file", "error", err) 39 | logger.Debug("too many connections", "connections", numConnections, "ip", ipAddr) 40 | -------------------------------------------------------------------------------- /hclogvet/example/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23 4 | 5 | require github.com/hashicorp/go-hclog v1.6.3 6 | 7 | require ( 8 | github.com/fatih/color v1.18.0 // indirect 9 | github.com/mattn/go-colorable v0.1.14 // indirect 10 | github.com/mattn/go-isatty v0.0.20 // indirect 11 | golang.org/x/sys v0.30.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /hclogvet/example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 5 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 6 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 7 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 8 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 9 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 10 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 11 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 12 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 13 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 14 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 15 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 16 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 21 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 22 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 29 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /hclogvet/example/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package testdata 5 | 6 | import ( 7 | "io" 8 | 9 | hclog "github.com/hashicorp/go-hclog" 10 | ) 11 | 12 | func badHCLog() { 13 | l := hclog.L() 14 | il := hclog.NewInterceptLogger(&hclog.LoggerOptions{}) 15 | 16 | var ( 17 | err = io.EOF 18 | numConnections = 5 19 | ipAddr = "10.40.40.10" 20 | ) 21 | 22 | // good 23 | l.Info("ok", "key", "val") 24 | l.Error("raft request failed", "error", err) 25 | l.Error("error opening file", "error", err) 26 | l.Debug("too many connections", "connections", numConnections, "ip", ipAddr) 27 | 28 | il.Info("ok", "key", "val") 29 | il.Error("raft request failed", "error", err) 30 | il.Error("error opening file", "error", err) 31 | il.Debug("too many connections", "connections", numConnections, "ip", ipAddr) 32 | 33 | // bad 34 | l.Info("bad", "key") 35 | l.Error("raft request failed: %v", err) 36 | l.Error("error opening file", err) 37 | l.Debug("too many connections", numConnections, "ip", ipAddr) 38 | il.Info("bad", "key") 39 | il.Error("raft request failed: %v", err) 40 | il.Error("error opening file", err) 41 | il.Debug("too many connections", numConnections, "ip", ipAddr) 42 | } 43 | -------------------------------------------------------------------------------- /hclogvet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-hclog/hclogvet 2 | 3 | go 1.23 4 | 5 | toolchain go1.24.0 6 | 7 | require golang.org/x/tools v0.30.0 8 | 9 | require ( 10 | golang.org/x/mod v0.23.0 // indirect 11 | golang.org/x/sync v0.11.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /hclogvet/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 4 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 5 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 6 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 7 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 8 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 9 | -------------------------------------------------------------------------------- /hclogvet/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "go/token" 9 | "go/types" 10 | "strings" 11 | 12 | "golang.org/x/tools/go/analysis" 13 | "golang.org/x/tools/go/analysis/passes/inspect" 14 | "golang.org/x/tools/go/analysis/singlechecker" 15 | ) 16 | 17 | func main() { 18 | singlechecker.Main(Analyzer) 19 | } 20 | 21 | var Analyzer = &analysis.Analyzer{ 22 | Name: "hclogvet", 23 | Doc: "check hclog invocations", 24 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 25 | Run: run, 26 | } 27 | 28 | var checkHCLogFunc = map[string]bool{ 29 | "Trace": true, 30 | "Debug": true, 31 | "Info": true, 32 | "Warn": true, 33 | "Error": true, 34 | } 35 | 36 | func run(pass *analysis.Pass) (interface{}, error) { 37 | for _, f := range pass.Files { 38 | ast.Inspect(f, func(n ast.Node) bool { 39 | call, ok := n.(*ast.CallExpr) 40 | if !ok { 41 | return true 42 | } 43 | 44 | fun, ok := call.Fun.(*ast.SelectorExpr) 45 | if !ok { 46 | return true 47 | } 48 | 49 | typ := pass.TypesInfo.Types[fun] 50 | sig, ok := typ.Type.(*types.Signature) 51 | if !ok { 52 | return true 53 | } else if sig == nil { 54 | return true // the call is not on of the form x.f() 55 | } 56 | 57 | recv := pass.TypesInfo.Types[fun.X] 58 | if recv.Type == nil { 59 | return true 60 | } 61 | 62 | if !isNamedType(recv.Type, "github.com/hashicorp/go-hclog", "Logger") && 63 | !isNamedType(recv.Type, "github.com/hashicorp/go-hclog", "InterceptLogger") { 64 | return true 65 | } 66 | 67 | if _, ok := checkHCLogFunc[fun.Sel.Name]; !ok { 68 | return true 69 | } 70 | 71 | if call.Ellipsis != token.NoPos { 72 | // this is a variadic function, we don't know how many args at this level 73 | return true 74 | } 75 | 76 | // arity should be odd, with the log message being first and then followed by K/V pairs 77 | if numArgs := len(call.Args); numArgs%2 != 1 { 78 | pairs := numArgs / 2 79 | noun := "pairs" 80 | if pairs == 1 { 81 | noun = "pair" 82 | } 83 | pass.Reportf(call.Lparen, "invalid number of log arguments to %s (%d valid %s only)", fun.Sel.Name, pairs, noun) 84 | } 85 | 86 | return true 87 | }) 88 | } 89 | 90 | return nil, nil 91 | } 92 | 93 | // isNamedType reports whether t is the named type path.name. 94 | func isNamedType(t types.Type, path, name string) bool { 95 | n, ok := t.(*types.Named) 96 | if !ok { 97 | return false 98 | } 99 | obj := n.Obj() 100 | return obj.Name() == name && isPackage(obj.Pkg(), path) 101 | } 102 | 103 | // isPackage reports whether pkg has path as the canonical path, 104 | // taking into account vendoring effects 105 | func isPackage(pkg *types.Package, path string) bool { 106 | if pkg == nil { 107 | return false 108 | } 109 | 110 | return pkg.Path() == path || 111 | strings.HasSuffix(pkg.Path(), "/vendor/"+path) 112 | } 113 | -------------------------------------------------------------------------------- /interceptlogger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "io" 8 | "log" 9 | "sync" 10 | "sync/atomic" 11 | ) 12 | 13 | var _ Logger = &interceptLogger{} 14 | 15 | type interceptLogger struct { 16 | Logger 17 | 18 | mu *sync.Mutex 19 | sinkCount *int32 20 | Sinks map[SinkAdapter]struct{} 21 | } 22 | 23 | func NewInterceptLogger(opts *LoggerOptions) InterceptLogger { 24 | l := newLogger(opts) 25 | if l.callerOffset > 0 { 26 | // extra frames for interceptLogger.{Warn,Info,Log,etc...}, and interceptLogger.log 27 | l.callerOffset += 2 28 | } 29 | intercept := &interceptLogger{ 30 | Logger: l, 31 | mu: new(sync.Mutex), 32 | sinkCount: new(int32), 33 | Sinks: make(map[SinkAdapter]struct{}), 34 | } 35 | 36 | atomic.StoreInt32(intercept.sinkCount, 0) 37 | 38 | return intercept 39 | } 40 | 41 | func (i *interceptLogger) Log(level Level, msg string, args ...interface{}) { 42 | i.log(level, msg, args...) 43 | } 44 | 45 | // log is used to make the caller stack frame lookup consistent. If Warn,Info,etc 46 | // all called Log then direct calls to Log would have a different stack frame 47 | // depth. By having all the methods call the same helper we ensure the stack 48 | // frame depth is the same. 49 | func (i *interceptLogger) log(level Level, msg string, args ...interface{}) { 50 | i.Logger.Log(level, msg, args...) 51 | if atomic.LoadInt32(i.sinkCount) == 0 { 52 | return 53 | } 54 | 55 | i.mu.Lock() 56 | defer i.mu.Unlock() 57 | for s := range i.Sinks { 58 | s.Accept(i.Name(), level, msg, i.retrieveImplied(args...)...) 59 | } 60 | } 61 | 62 | // Emit the message and args at TRACE level to log and sinks 63 | func (i *interceptLogger) Trace(msg string, args ...interface{}) { 64 | i.log(Trace, msg, args...) 65 | } 66 | 67 | // Emit the message and args at DEBUG level to log and sinks 68 | func (i *interceptLogger) Debug(msg string, args ...interface{}) { 69 | i.log(Debug, msg, args...) 70 | } 71 | 72 | // Emit the message and args at INFO level to log and sinks 73 | func (i *interceptLogger) Info(msg string, args ...interface{}) { 74 | i.log(Info, msg, args...) 75 | } 76 | 77 | // Emit the message and args at WARN level to log and sinks 78 | func (i *interceptLogger) Warn(msg string, args ...interface{}) { 79 | i.log(Warn, msg, args...) 80 | } 81 | 82 | // Emit the message and args at ERROR level to log and sinks 83 | func (i *interceptLogger) Error(msg string, args ...interface{}) { 84 | i.log(Error, msg, args...) 85 | } 86 | 87 | func (i *interceptLogger) retrieveImplied(args ...interface{}) []interface{} { 88 | top := i.Logger.ImpliedArgs() 89 | 90 | cp := make([]interface{}, len(top)+len(args)) 91 | copy(cp, top) 92 | copy(cp[len(top):], args) 93 | 94 | return cp 95 | } 96 | 97 | // Create a new sub-Logger that a name descending from the current name. 98 | // This is used to create a subsystem specific Logger. 99 | // Registered sinks will subscribe to these messages as well. 100 | func (i *interceptLogger) Named(name string) Logger { 101 | return i.NamedIntercept(name) 102 | } 103 | 104 | // Create a new sub-Logger with an explicit name. This ignores the current 105 | // name. This is used to create a standalone logger that doesn't fall 106 | // within the normal hierarchy. Registered sinks will subscribe 107 | // to these messages as well. 108 | func (i *interceptLogger) ResetNamed(name string) Logger { 109 | return i.ResetNamedIntercept(name) 110 | } 111 | 112 | // Create a new sub-Logger that a name decending from the current name. 113 | // This is used to create a subsystem specific Logger. 114 | // Registered sinks will subscribe to these messages as well. 115 | func (i *interceptLogger) NamedIntercept(name string) InterceptLogger { 116 | var sub interceptLogger = *i 117 | sub.Logger = i.Logger.Named(name) 118 | return &sub 119 | } 120 | 121 | // Create a new sub-Logger with an explicit name. This ignores the current 122 | // name. This is used to create a standalone logger that doesn't fall 123 | // within the normal hierarchy. Registered sinks will subscribe 124 | // to these messages as well. 125 | func (i *interceptLogger) ResetNamedIntercept(name string) InterceptLogger { 126 | var sub interceptLogger = *i 127 | sub.Logger = i.Logger.ResetNamed(name) 128 | return &sub 129 | } 130 | 131 | // Return a sub-Logger for which every emitted log message will contain 132 | // the given key/value pairs. This is used to create a context specific 133 | // Logger. 134 | func (i *interceptLogger) With(args ...interface{}) Logger { 135 | var sub interceptLogger = *i 136 | 137 | sub.Logger = i.Logger.With(args...) 138 | 139 | return &sub 140 | } 141 | 142 | // RegisterSink attaches a SinkAdapter to interceptLoggers sinks. 143 | func (i *interceptLogger) RegisterSink(sink SinkAdapter) { 144 | i.mu.Lock() 145 | defer i.mu.Unlock() 146 | 147 | i.Sinks[sink] = struct{}{} 148 | 149 | atomic.AddInt32(i.sinkCount, 1) 150 | } 151 | 152 | // DeregisterSink removes a SinkAdapter from interceptLoggers sinks. 153 | func (i *interceptLogger) DeregisterSink(sink SinkAdapter) { 154 | i.mu.Lock() 155 | defer i.mu.Unlock() 156 | 157 | delete(i.Sinks, sink) 158 | 159 | atomic.AddInt32(i.sinkCount, -1) 160 | } 161 | 162 | func (i *interceptLogger) StandardLoggerIntercept(opts *StandardLoggerOptions) *log.Logger { 163 | return i.StandardLogger(opts) 164 | } 165 | 166 | func (i *interceptLogger) StandardLogger(opts *StandardLoggerOptions) *log.Logger { 167 | if opts == nil { 168 | opts = &StandardLoggerOptions{} 169 | } 170 | 171 | return log.New(i.StandardWriter(opts), "", 0) 172 | } 173 | 174 | func (i *interceptLogger) StandardWriterIntercept(opts *StandardLoggerOptions) io.Writer { 175 | return i.StandardWriter(opts) 176 | } 177 | 178 | func (i *interceptLogger) StandardWriter(opts *StandardLoggerOptions) io.Writer { 179 | return &stdlogAdapter{ 180 | log: i, 181 | inferLevels: opts.InferLevels, 182 | inferLevelsWithTimestamp: opts.InferLevelsWithTimestamp, 183 | forceLevel: opts.ForceLevel, 184 | } 185 | } 186 | 187 | func (i *interceptLogger) ResetOutput(opts *LoggerOptions) error { 188 | if or, ok := i.Logger.(OutputResettable); ok { 189 | return or.ResetOutput(opts) 190 | } else { 191 | return nil 192 | } 193 | } 194 | 195 | func (i *interceptLogger) ResetOutputWithFlush(opts *LoggerOptions, flushable Flushable) error { 196 | if or, ok := i.Logger.(OutputResettable); ok { 197 | return or.ResetOutputWithFlush(opts, flushable) 198 | } else { 199 | return nil 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /interceptlogger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "fmt" 10 | "runtime" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestInterceptLogger(t *testing.T) { 19 | t.Run("sends output to registered sinks", func(t *testing.T) { 20 | var buf bytes.Buffer 21 | var sbuf bytes.Buffer 22 | 23 | intercept := NewInterceptLogger(&LoggerOptions{ 24 | Level: Info, 25 | Output: &buf, 26 | }) 27 | 28 | sink := NewSinkAdapter(&LoggerOptions{ 29 | Level: Debug, 30 | Output: &sbuf, 31 | }) 32 | 33 | intercept.RegisterSink(sink) 34 | defer intercept.DeregisterSink(sink) 35 | 36 | intercept.Debug("test log", "who", "programmer") 37 | 38 | str := sbuf.String() 39 | dataIdx := strings.IndexByte(str, ' ') 40 | rest := str[dataIdx+1:] 41 | 42 | assert.Equal(t, "[DEBUG] test log: who=programmer\n", rest) 43 | 44 | }) 45 | 46 | t.Run("sink includes with arguments", func(t *testing.T) { 47 | var buf bytes.Buffer 48 | var sbuf bytes.Buffer 49 | 50 | intercept := NewInterceptLogger(&LoggerOptions{ 51 | Name: "with_test", 52 | Level: Info, 53 | Output: &buf, 54 | }) 55 | 56 | sink := NewSinkAdapter(&LoggerOptions{ 57 | Level: Debug, 58 | Output: &sbuf, 59 | }) 60 | intercept.RegisterSink(sink) 61 | defer intercept.DeregisterSink(sink) 62 | 63 | derived := intercept.With("a", 1, "b", 2) 64 | derived = derived.With("c", 3) 65 | 66 | derived.Info("test1") 67 | output := buf.String() 68 | dataIdx := strings.IndexByte(output, ' ') 69 | rest := output[dataIdx+1:] 70 | 71 | assert.Equal(t, "[INFO] with_test: test1: a=1 b=2 c=3\n", rest) 72 | 73 | // Ensure intercept works 74 | output = sbuf.String() 75 | dataIdx = strings.IndexByte(output, ' ') 76 | rest = output[dataIdx+1:] 77 | 78 | assert.Equal(t, "[INFO] with_test: test1: a=1 b=2 c=3\n", rest) 79 | }) 80 | 81 | t.Run("sink includes name", func(t *testing.T) { 82 | var buf bytes.Buffer 83 | var sbuf bytes.Buffer 84 | 85 | intercept := NewInterceptLogger(&LoggerOptions{ 86 | Name: "with_test", 87 | Level: Info, 88 | Output: &buf, 89 | }) 90 | 91 | sink := NewSinkAdapter(&LoggerOptions{ 92 | Level: Debug, 93 | Output: &sbuf, 94 | }) 95 | intercept.RegisterSink(sink) 96 | defer intercept.DeregisterSink(sink) 97 | 98 | httpLogger := intercept.Named("http") 99 | 100 | httpLogger.Info("test1") 101 | output := buf.String() 102 | dataIdx := strings.IndexByte(output, ' ') 103 | rest := output[dataIdx+1:] 104 | 105 | assert.Equal(t, "[INFO] with_test.http: test1\n", rest) 106 | 107 | // Ensure intercept works 108 | output = sbuf.String() 109 | dataIdx = strings.IndexByte(output, ' ') 110 | rest = output[dataIdx+1:] 111 | 112 | assert.Equal(t, "[INFO] with_test.http: test1\n", rest) 113 | }) 114 | 115 | t.Run("intercepting logger can create logger with reset name", func(t *testing.T) { 116 | var buf bytes.Buffer 117 | var sbuf bytes.Buffer 118 | 119 | intercept := NewInterceptLogger(&LoggerOptions{ 120 | Name: "with_test", 121 | Level: Info, 122 | Output: &buf, 123 | }) 124 | 125 | sink := NewSinkAdapter(&LoggerOptions{ 126 | Level: Debug, 127 | Output: &sbuf, 128 | }) 129 | intercept.RegisterSink(sink) 130 | defer intercept.DeregisterSink(sink) 131 | 132 | httpLogger := intercept.ResetNamed("http") 133 | 134 | httpLogger.Info("test1") 135 | output := buf.String() 136 | dataIdx := strings.IndexByte(output, ' ') 137 | rest := output[dataIdx+1:] 138 | 139 | assert.Equal(t, "[INFO] http: test1\n", rest) 140 | 141 | // Ensure intercept works 142 | output = sbuf.String() 143 | dataIdx = strings.IndexByte(output, ' ') 144 | rest = output[dataIdx+1:] 145 | 146 | assert.Equal(t, "[INFO] http: test1\n", rest) 147 | }) 148 | 149 | t.Run("Intercepting logger sink can deregister itself", func(t *testing.T) { 150 | var buf bytes.Buffer 151 | var sbuf bytes.Buffer 152 | 153 | intercept := NewInterceptLogger(&LoggerOptions{ 154 | Name: "with_test", 155 | Level: Info, 156 | Output: &buf, 157 | }) 158 | 159 | sink := NewSinkAdapter(&LoggerOptions{ 160 | Level: Debug, 161 | Output: &sbuf, 162 | }) 163 | intercept.RegisterSink(sink) 164 | intercept.DeregisterSink(sink) 165 | 166 | intercept.Info("test1") 167 | 168 | assert.Equal(t, "", sbuf.String()) 169 | }) 170 | 171 | t.Run("Sinks accept different log formats", func(t *testing.T) { 172 | var buf bytes.Buffer 173 | var sbuf bytes.Buffer 174 | 175 | intercept := NewInterceptLogger(&LoggerOptions{ 176 | Level: Info, 177 | Output: &buf, 178 | IncludeLocation: true, 179 | }) 180 | 181 | sink := NewSinkAdapter(&LoggerOptions{ 182 | Level: Debug, 183 | Output: &sbuf, 184 | JSONFormat: true, 185 | IncludeLocation: true, 186 | }) 187 | 188 | intercept.RegisterSink(sink) 189 | defer intercept.DeregisterSink(sink) 190 | 191 | intercept.Info("this is a test", "who", "caller") 192 | _, file, line, ok := runtime.Caller(0) 193 | require.True(t, ok) 194 | 195 | output := buf.String() 196 | dataIdx := strings.IndexByte(output, ' ') 197 | rest := output[dataIdx+1:] 198 | 199 | expected := fmt.Sprintf("[INFO] go-hclog/interceptlogger_test.go:%d: this is a test: who=caller\n", line-1) 200 | assert.Equal(t, expected, rest) 201 | 202 | b := sbuf.Bytes() 203 | 204 | var raw map[string]interface{} 205 | if err := json.Unmarshal(b, &raw); err != nil { 206 | t.Fatal(err) 207 | } 208 | 209 | assert.Equal(t, "this is a test", raw["@message"]) 210 | assert.Equal(t, "caller", raw["who"]) 211 | assert.Equal(t, fmt.Sprintf("%v:%d", file, line-1), raw["@caller"]) 212 | }) 213 | 214 | t.Run("handles parent with arguments and log level args", func(t *testing.T) { 215 | var buf bytes.Buffer 216 | var sbuf bytes.Buffer 217 | 218 | intercept := NewInterceptLogger(&LoggerOptions{ 219 | Name: "with_test", 220 | Level: Debug, 221 | Output: &buf, 222 | }) 223 | 224 | sink := NewSinkAdapter(&LoggerOptions{ 225 | Level: Debug, 226 | Output: &sbuf, 227 | }) 228 | intercept.RegisterSink(sink) 229 | defer intercept.DeregisterSink(sink) 230 | 231 | named := intercept.Named("sub_logger") 232 | named = named.With("parent", "logger") 233 | subNamed := named.Named("http") 234 | 235 | subNamed.Debug("test1", "path", "/some/test/path", "args", []string{"test", "test"}) 236 | 237 | output := buf.String() 238 | dataIdx := strings.IndexByte(output, ' ') 239 | rest := output[dataIdx+1:] 240 | assert.Equal(t, "[DEBUG] with_test.sub_logger.http: test1: parent=logger path=/some/test/path args=[\"test\", \"test\"]\n", rest) 241 | }) 242 | 243 | t.Run("derived standard loggers send output to sinks", func(t *testing.T) { 244 | var buf bytes.Buffer 245 | var sbuf bytes.Buffer 246 | 247 | intercept := NewInterceptLogger(&LoggerOptions{ 248 | Name: "with_name", 249 | Level: Debug, 250 | Output: &buf, 251 | }) 252 | 253 | standard := intercept.StandardLogger(&StandardLoggerOptions{InferLevels: true}) 254 | 255 | sink := NewSinkAdapter(&LoggerOptions{ 256 | Level: Debug, 257 | Output: &sbuf, 258 | }) 259 | intercept.RegisterSink(sink) 260 | defer intercept.DeregisterSink(sink) 261 | 262 | standard.Println("[DEBUG] test log") 263 | 264 | output := buf.String() 265 | dataIdx := strings.IndexByte(output, ' ') 266 | rest := output[dataIdx+1:] 267 | assert.Equal(t, "[DEBUG] with_name: test log\n", rest) 268 | 269 | output = sbuf.String() 270 | dataIdx = strings.IndexByte(output, ' ') 271 | rest = output[dataIdx+1:] 272 | assert.Equal(t, "[DEBUG] with_name: test log\n", rest) 273 | }) 274 | 275 | t.Run("includes the caller location", func(t *testing.T) { 276 | var buf bytes.Buffer 277 | var sbuf bytes.Buffer 278 | 279 | logger := NewInterceptLogger(&LoggerOptions{ 280 | Name: "test", 281 | Output: &buf, 282 | IncludeLocation: true, 283 | }) 284 | 285 | sink := NewSinkAdapter(&LoggerOptions{ 286 | IncludeLocation: true, 287 | Level: Debug, 288 | Output: &sbuf, 289 | }) 290 | logger.RegisterSink(sink) 291 | defer logger.DeregisterSink(sink) 292 | 293 | logger.Info("this is test", "who", "programmer", "why", "testing is fun") 294 | _, _, line, ok := runtime.Caller(0) 295 | require.True(t, ok) 296 | 297 | str := buf.String() 298 | dataIdx := strings.IndexByte(str, ' ') 299 | rest := str[dataIdx+1:] 300 | 301 | expected := fmt.Sprintf("[INFO] go-hclog/interceptlogger_test.go:%d: test: this is test: who=programmer why=\"testing is fun\"\n", line-1) 302 | assert.Equal(t, expected, rest) 303 | 304 | str = sbuf.String() 305 | dataIdx = strings.IndexByte(str, ' ') 306 | rest = str[dataIdx+1:] 307 | assert.Equal(t, expected, rest) 308 | }) 309 | 310 | t.Run("supports resetting the output", func(t *testing.T) { 311 | var first, second bytes.Buffer 312 | 313 | logger := NewInterceptLogger(&LoggerOptions{ 314 | Output: &first, 315 | }) 316 | 317 | logger.Info("this is test", "production", Fmt("%d beans/day", 12)) 318 | 319 | str := first.String() 320 | dataIdx := strings.IndexByte(str, ' ') 321 | rest := str[dataIdx+1:] 322 | 323 | assert.Equal(t, "[INFO] this is test: production=\"12 beans/day\"\n", rest) 324 | 325 | _ = logger.(OutputResettable).ResetOutput(&LoggerOptions{ 326 | Output: &second, 327 | }) 328 | 329 | logger.Info("this is another test", "production", Fmt("%d beans/day", 13)) 330 | 331 | str = first.String() 332 | dataIdx = strings.IndexByte(str, ' ') 333 | rest = str[dataIdx+1:] 334 | assert.Equal(t, "[INFO] this is test: production=\"12 beans/day\"\n", rest) 335 | 336 | str = second.String() 337 | dataIdx = strings.IndexByte(str, ' ') 338 | rest = str[dataIdx+1:] 339 | assert.Equal(t, "[INFO] this is another test: production=\"13 beans/day\"\n", rest) 340 | }) 341 | 342 | t.Run("supports resetting the output with flushing", func(t *testing.T) { 343 | var first bufferingBuffer 344 | var second bytes.Buffer 345 | 346 | logger := NewInterceptLogger(&LoggerOptions{ 347 | Output: &first, 348 | }) 349 | 350 | logger.Info("this is test", "production", Fmt("%d beans/day", 12)) 351 | 352 | str := first.String() 353 | assert.Empty(t, str) 354 | 355 | logger.(OutputResettable).ResetOutputWithFlush(&LoggerOptions{ 356 | Output: &second, 357 | }, &first) 358 | 359 | logger.Info("this is another test", "production", Fmt("%d beans/day", 13)) 360 | 361 | str = first.String() 362 | dataIdx := strings.IndexByte(str, ' ') 363 | rest := str[dataIdx+1:] 364 | assert.Equal(t, "[INFO] this is test: production=\"12 beans/day\"\n", rest) 365 | 366 | str = second.String() 367 | dataIdx = strings.IndexByte(str, ' ') 368 | rest = str[dataIdx+1:] 369 | assert.Equal(t, "[INFO] this is another test: production=\"13 beans/day\"\n", rest) 370 | }) 371 | } 372 | -------------------------------------------------------------------------------- /intlogger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "bytes" 8 | "encoding" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "log" 14 | "reflect" 15 | "runtime" 16 | "sort" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "sync/atomic" 21 | "time" 22 | "unicode" 23 | "unicode/utf8" 24 | 25 | "github.com/fatih/color" 26 | ) 27 | 28 | // TimeFormat is the time format to use for plain (non-JSON) output. 29 | // This is a version of RFC3339 that contains millisecond precision. 30 | const TimeFormat = "2006-01-02T15:04:05.000Z0700" 31 | 32 | // TimeFormatJSON is the time format to use for JSON output. 33 | // This is a version of RFC3339 that contains microsecond precision. 34 | const TimeFormatJSON = "2006-01-02T15:04:05.000000Z07:00" 35 | 36 | // errJsonUnsupportedTypeMsg is included in log json entries, if an arg cannot be serialized to json 37 | const errJsonUnsupportedTypeMsg = "logging contained values that don't serialize to json" 38 | 39 | var ( 40 | _levelToBracket = map[Level]string{ 41 | Debug: "[DEBUG]", 42 | Trace: "[TRACE]", 43 | Info: "[INFO] ", 44 | Warn: "[WARN] ", 45 | Error: "[ERROR]", 46 | } 47 | 48 | _levelToColor = map[Level]*color.Color{ 49 | Debug: color.New(color.FgHiWhite), 50 | Trace: color.New(color.FgHiGreen), 51 | Info: color.New(color.FgHiBlue), 52 | Warn: color.New(color.FgHiYellow), 53 | Error: color.New(color.FgHiRed), 54 | } 55 | 56 | faintBoldColor = color.New(color.Faint, color.Bold) 57 | faintColor = color.New(color.Faint) 58 | faintMultiLinePrefix string 59 | faintFieldSeparator string 60 | faintFieldSeparatorWithNewLine string 61 | ) 62 | 63 | func init() { 64 | // Force all the colors to enabled because we do our own detection of color usage. 65 | for _, c := range _levelToColor { 66 | c.EnableColor() 67 | } 68 | 69 | faintBoldColor.EnableColor() 70 | faintColor.EnableColor() 71 | 72 | faintMultiLinePrefix = faintColor.Sprint(" | ") 73 | faintFieldSeparator = faintColor.Sprint("=") 74 | faintFieldSeparatorWithNewLine = faintColor.Sprint("=\n") 75 | } 76 | 77 | // Make sure that intLogger is a Logger 78 | var _ Logger = &intLogger{} 79 | 80 | // intLogger is an internal logger implementation. Internal in that it is 81 | // defined entirely by this package. 82 | type intLogger struct { 83 | json bool 84 | jsonEscapeEnabled bool 85 | callerOffset int 86 | name string 87 | timeFormat string 88 | timeFn TimeFunction 89 | disableTime bool 90 | 91 | // This is an interface so that it's shared by any derived loggers, since 92 | // those derived loggers share the bufio.Writer as well. 93 | mutex Locker 94 | writer *writer 95 | level *int32 96 | 97 | // The value of curEpoch when our level was set 98 | setEpoch uint64 99 | 100 | // The value of curEpoch the last time we performed the level sync process 101 | ownEpoch uint64 102 | 103 | // Shared amongst all the loggers created in this hierachy, used to determine 104 | // if the level sync process should be run by comparing it with ownEpoch 105 | curEpoch *uint64 106 | 107 | // The logger this one was created from. Only set when syncParentLevel is set 108 | parent *intLogger 109 | 110 | headerColor ColorOption 111 | fieldColor ColorOption 112 | 113 | implied []interface{} 114 | 115 | exclude func(level Level, msg string, args ...interface{}) bool 116 | 117 | // create subloggers with their own level setting 118 | independentLevels bool 119 | syncParentLevel bool 120 | 121 | subloggerHook func(sub Logger) Logger 122 | } 123 | 124 | // New returns a configured logger. 125 | func New(opts *LoggerOptions) Logger { 126 | return newLogger(opts) 127 | } 128 | 129 | // NewSinkAdapter returns a SinkAdapter with configured settings 130 | // defined by LoggerOptions 131 | func NewSinkAdapter(opts *LoggerOptions) SinkAdapter { 132 | l := newLogger(opts) 133 | if l.callerOffset > 0 { 134 | // extra frames for interceptLogger.{Warn,Info,Log,etc...}, and SinkAdapter.Accept 135 | l.callerOffset += 2 136 | } 137 | return l 138 | } 139 | 140 | func newLogger(opts *LoggerOptions) *intLogger { 141 | if opts == nil { 142 | opts = &LoggerOptions{} 143 | } 144 | 145 | output := opts.Output 146 | if output == nil { 147 | output = DefaultOutput 148 | } 149 | 150 | level := opts.Level 151 | if level == NoLevel { 152 | level = DefaultLevel 153 | } 154 | 155 | mutex := opts.Mutex 156 | if mutex == nil { 157 | mutex = new(sync.Mutex) 158 | } 159 | 160 | var ( 161 | primaryColor = ColorOff 162 | headerColor = ColorOff 163 | fieldColor = ColorOff 164 | ) 165 | switch { 166 | case opts.ColorHeaderOnly: 167 | headerColor = opts.Color 168 | case opts.ColorHeaderAndFields: 169 | fieldColor = opts.Color 170 | headerColor = opts.Color 171 | default: 172 | primaryColor = opts.Color 173 | } 174 | 175 | l := &intLogger{ 176 | json: opts.JSONFormat, 177 | jsonEscapeEnabled: !opts.JSONEscapeDisabled, 178 | name: opts.Name, 179 | timeFormat: TimeFormat, 180 | timeFn: time.Now, 181 | disableTime: opts.DisableTime, 182 | mutex: mutex, 183 | writer: newWriter(output, primaryColor), 184 | level: new(int32), 185 | curEpoch: new(uint64), 186 | exclude: opts.Exclude, 187 | independentLevels: opts.IndependentLevels, 188 | syncParentLevel: opts.SyncParentLevel, 189 | headerColor: headerColor, 190 | fieldColor: fieldColor, 191 | subloggerHook: opts.SubloggerHook, 192 | } 193 | if opts.IncludeLocation { 194 | l.callerOffset = offsetIntLogger + opts.AdditionalLocationOffset 195 | } 196 | 197 | if l.json { 198 | l.timeFormat = TimeFormatJSON 199 | } 200 | if opts.TimeFn != nil { 201 | l.timeFn = opts.TimeFn 202 | } 203 | if opts.TimeFormat != "" { 204 | l.timeFormat = opts.TimeFormat 205 | } 206 | 207 | if l.subloggerHook == nil { 208 | l.subloggerHook = identityHook 209 | } 210 | 211 | l.setColorization(opts) 212 | 213 | atomic.StoreInt32(l.level, int32(level)) 214 | 215 | return l 216 | } 217 | 218 | func identityHook(logger Logger) Logger { 219 | return logger 220 | } 221 | 222 | // offsetIntLogger is the stack frame offset in the call stack for the caller to 223 | // one of the Warn, Info, Log, etc methods. 224 | const offsetIntLogger = 3 225 | 226 | // Log a message and a set of key/value pairs if the given level is at 227 | // or more severe that the threshold configured in the Logger. 228 | func (l *intLogger) log(name string, level Level, msg string, args ...interface{}) { 229 | if level < l.GetLevel() { 230 | return 231 | } 232 | 233 | t := l.timeFn() 234 | 235 | l.mutex.Lock() 236 | defer l.mutex.Unlock() 237 | 238 | if l.exclude != nil && l.exclude(level, msg, args...) { 239 | return 240 | } 241 | 242 | if l.json { 243 | l.logJSON(t, name, level, msg, args...) 244 | } else { 245 | l.logPlain(t, name, level, msg, args...) 246 | } 247 | 248 | l.writer.Flush(level) 249 | } 250 | 251 | // Cleanup a path by returning the last 2 segments of the path only. 252 | func trimCallerPath(path string) string { 253 | // lovely borrowed from zap 254 | // nb. To make sure we trim the path correctly on Windows too, we 255 | // counter-intuitively need to use '/' and *not* os.PathSeparator here, 256 | // because the path given originates from Go stdlib, specifically 257 | // runtime.Caller() which (as of Mar/17) returns forward slashes even on 258 | // Windows. 259 | // 260 | // See https://github.com/golang/go/issues/3335 261 | // and https://github.com/golang/go/issues/18151 262 | // 263 | // for discussion on the issue on Go side. 264 | 265 | // Find the last separator. 266 | idx := strings.LastIndexByte(path, '/') 267 | if idx == -1 { 268 | return path 269 | } 270 | 271 | // Find the penultimate separator. 272 | idx = strings.LastIndexByte(path[:idx], '/') 273 | if idx == -1 { 274 | return path 275 | } 276 | 277 | return path[idx+1:] 278 | } 279 | 280 | // isNormal indicates if the rune is one allowed to exist as an unquoted 281 | // string value. This is a subset of ASCII, `-` through `~`. 282 | func isNormal(r rune) bool { 283 | return 0x2D <= r && r <= 0x7E // - through ~ 284 | } 285 | 286 | // needsQuoting returns false if all the runes in string are normal, according 287 | // to isNormal 288 | func needsQuoting(str string) bool { 289 | for _, r := range str { 290 | if !isNormal(r) { 291 | return true 292 | } 293 | } 294 | 295 | return false 296 | } 297 | 298 | // logPlain is the non-JSON logging format function which writes directly 299 | // to the underlying writer the logger was initialized with. 300 | // 301 | // If the logger was initialized with a color function, it also handles 302 | // applying the color to the log message. 303 | // 304 | // Color Options 305 | // 1. No color. 306 | // 2. Color the whole log line, based on the level. 307 | // 3. Color only the header (level) part of the log line. 308 | // 4. Color both the header and fields of the log line. 309 | func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, args ...interface{}) { 310 | 311 | if !l.disableTime { 312 | _, _ = l.writer.WriteString(t.Format(l.timeFormat)) 313 | _ = l.writer.WriteByte(' ') 314 | } 315 | 316 | s, ok := _levelToBracket[level] 317 | if ok { 318 | if l.headerColor != ColorOff { 319 | color := _levelToColor[level] 320 | color.Fprint(l.writer, s) 321 | } else { 322 | _, _ = l.writer.WriteString(s) 323 | } 324 | } else { 325 | _, _ = l.writer.WriteString("[?????]") 326 | } 327 | 328 | if l.callerOffset > 0 { 329 | if _, file, line, ok := runtime.Caller(l.callerOffset); ok { 330 | _ = l.writer.WriteByte(' ') 331 | _, _ = l.writer.WriteString(trimCallerPath(file)) 332 | _ = l.writer.WriteByte(':') 333 | _, _ = l.writer.WriteString(strconv.Itoa(line)) 334 | _ = l.writer.WriteByte(':') 335 | } 336 | } 337 | 338 | _ = l.writer.WriteByte(' ') 339 | 340 | if name != "" { 341 | _, _ = l.writer.WriteString(name) 342 | if msg != "" { 343 | _, _ = l.writer.WriteString(": ") 344 | _, _ = l.writer.WriteString(msg) 345 | } 346 | } else if msg != "" { 347 | _, _ = l.writer.WriteString(msg) 348 | } 349 | 350 | args = append(l.implied, args...) 351 | 352 | var stacktrace CapturedStacktrace 353 | 354 | if len(args) > 0 { 355 | if len(args)%2 != 0 { 356 | cs, ok := args[len(args)-1].(CapturedStacktrace) 357 | if ok { 358 | args = args[:len(args)-1] 359 | stacktrace = cs 360 | } else { 361 | extra := args[len(args)-1] 362 | args = append(args[:len(args)-1], MissingKey, extra) 363 | } 364 | } 365 | 366 | _ = l.writer.WriteByte(':') 367 | 368 | // Handle the field arguments, which come in pairs (key=val). 369 | FOR: 370 | for i := 0; i < len(args); i = i + 2 { 371 | var ( 372 | key string 373 | val string 374 | raw bool 375 | ) 376 | 377 | // Convert the field value to a string. 378 | switch st := args[i+1].(type) { 379 | case string: 380 | val = st 381 | if st == "" { 382 | val = `""` 383 | raw = true 384 | } 385 | case int: 386 | val = strconv.FormatInt(int64(st), 10) 387 | case int64: 388 | val = strconv.FormatInt(int64(st), 10) 389 | case int32: 390 | val = strconv.FormatInt(int64(st), 10) 391 | case int16: 392 | val = strconv.FormatInt(int64(st), 10) 393 | case int8: 394 | val = strconv.FormatInt(int64(st), 10) 395 | case uint: 396 | val = strconv.FormatUint(uint64(st), 10) 397 | case uint64: 398 | val = strconv.FormatUint(uint64(st), 10) 399 | case uint32: 400 | val = strconv.FormatUint(uint64(st), 10) 401 | case uint16: 402 | val = strconv.FormatUint(uint64(st), 10) 403 | case uint8: 404 | val = strconv.FormatUint(uint64(st), 10) 405 | case Hex: 406 | val = "0x" + strconv.FormatUint(uint64(st), 16) 407 | case Octal: 408 | val = "0" + strconv.FormatUint(uint64(st), 8) 409 | case Binary: 410 | val = "0b" + strconv.FormatUint(uint64(st), 2) 411 | case CapturedStacktrace: 412 | stacktrace = st 413 | continue FOR 414 | case Format: 415 | val = fmt.Sprintf(st[0].(string), st[1:]...) 416 | case Quote: 417 | raw = true 418 | val = strconv.Quote(string(st)) 419 | default: 420 | v := reflect.ValueOf(st) 421 | if v.Kind() == reflect.Slice { 422 | val = l.renderSlice(v) 423 | raw = true 424 | } else { 425 | val = fmt.Sprintf("%v", st) 426 | } 427 | } 428 | 429 | // Convert the field key to a string. 430 | switch st := args[i].(type) { 431 | case string: 432 | key = st 433 | default: 434 | key = fmt.Sprintf("%s", st) 435 | } 436 | 437 | // Optionally apply the ANSI "faint" and "bold" 438 | // SGR values to the key. 439 | if l.fieldColor != ColorOff { 440 | key = faintBoldColor.Sprint(key) 441 | } 442 | 443 | // Values may contain multiple lines, and that format 444 | // is preserved, with each line prefixed with a " | " 445 | // to show it's part of a collection of lines. 446 | // 447 | // Values may also need quoting, if not all the runes 448 | // in the value string are "normal", like if they 449 | // contain ANSI escape sequences. 450 | if strings.Contains(val, "\n") { 451 | _, _ = l.writer.WriteString("\n ") 452 | _, _ = l.writer.WriteString(key) 453 | if l.fieldColor != ColorOff { 454 | _, _ = l.writer.WriteString(faintFieldSeparatorWithNewLine) 455 | writeIndent(l.writer, val, faintMultiLinePrefix) 456 | } else { 457 | _, _ = l.writer.WriteString("=\n") 458 | writeIndent(l.writer, val, " | ") 459 | } 460 | _, _ = l.writer.WriteString(" ") 461 | } else if !raw && needsQuoting(val) { 462 | _ = l.writer.WriteByte(' ') 463 | _, _ = l.writer.WriteString(key) 464 | if l.fieldColor != ColorOff { 465 | _, _ = l.writer.WriteString(faintFieldSeparator) 466 | } else { 467 | _ = l.writer.WriteByte('=') 468 | } 469 | _ = l.writer.WriteByte('"') 470 | writeEscapedForOutput(l.writer, val, true) 471 | _ = l.writer.WriteByte('"') 472 | } else { 473 | _ = l.writer.WriteByte(' ') 474 | _, _ = l.writer.WriteString(key) 475 | if l.fieldColor != ColorOff { 476 | _, _ = l.writer.WriteString(faintFieldSeparator) 477 | } else { 478 | _ = l.writer.WriteByte('=') 479 | } 480 | _, _ = l.writer.WriteString(val) 481 | } 482 | } 483 | } 484 | 485 | _, _ = l.writer.WriteString("\n") 486 | 487 | if stacktrace != "" { 488 | _, _ = l.writer.WriteString(string(stacktrace)) 489 | _, _ = l.writer.WriteString("\n") 490 | } 491 | } 492 | 493 | func writeIndent(w *writer, str string, indent string) { 494 | for { 495 | nl := strings.IndexByte(str, "\n"[0]) 496 | if nl == -1 { 497 | if str != "" { 498 | _, _ = w.WriteString(indent) 499 | writeEscapedForOutput(w, str, false) 500 | _, _ = w.WriteString("\n") 501 | } 502 | return 503 | } 504 | 505 | _, _ = w.WriteString(indent) 506 | writeEscapedForOutput(w, str[:nl], false) 507 | _, _ = w.WriteString("\n") 508 | str = str[nl+1:] 509 | } 510 | } 511 | 512 | func needsEscaping(str string) bool { 513 | for _, b := range str { 514 | if !unicode.IsPrint(b) || b == '"' { 515 | return true 516 | } 517 | } 518 | 519 | return false 520 | } 521 | 522 | const ( 523 | lowerhex = "0123456789abcdef" 524 | ) 525 | 526 | var bufPool = sync.Pool{ 527 | New: func() interface{} { 528 | return new(bytes.Buffer) 529 | }, 530 | } 531 | 532 | func writeEscapedForOutput(w io.Writer, str string, escapeQuotes bool) { 533 | if !needsEscaping(str) { 534 | _, _ = w.Write([]byte(str)) 535 | return 536 | } 537 | 538 | bb := bufPool.Get().(*bytes.Buffer) 539 | bb.Reset() 540 | 541 | defer bufPool.Put(bb) 542 | 543 | for _, r := range str { 544 | if escapeQuotes && r == '"' { 545 | bb.WriteString(`\"`) 546 | } else if unicode.IsPrint(r) { 547 | bb.WriteRune(r) 548 | } else { 549 | switch r { 550 | case '\a': 551 | bb.WriteString(`\a`) 552 | case '\b': 553 | bb.WriteString(`\b`) 554 | case '\f': 555 | bb.WriteString(`\f`) 556 | case '\n': 557 | bb.WriteString(`\n`) 558 | case '\r': 559 | bb.WriteString(`\r`) 560 | case '\t': 561 | bb.WriteString(`\t`) 562 | case '\v': 563 | bb.WriteString(`\v`) 564 | default: 565 | switch { 566 | case r < ' ': 567 | bb.WriteString(`\x`) 568 | bb.WriteByte(lowerhex[byte(r)>>4]) 569 | bb.WriteByte(lowerhex[byte(r)&0xF]) 570 | case !utf8.ValidRune(r): 571 | r = 0xFFFD 572 | fallthrough 573 | case r < 0x10000: 574 | bb.WriteString(`\u`) 575 | for s := 12; s >= 0; s -= 4 { 576 | bb.WriteByte(lowerhex[r>>uint(s)&0xF]) 577 | } 578 | default: 579 | bb.WriteString(`\U`) 580 | for s := 28; s >= 0; s -= 4 { 581 | bb.WriteByte(lowerhex[r>>uint(s)&0xF]) 582 | } 583 | } 584 | } 585 | } 586 | } 587 | 588 | _, _ = w.Write(bb.Bytes()) 589 | } 590 | 591 | func (l *intLogger) renderSlice(v reflect.Value) string { 592 | var buf bytes.Buffer 593 | 594 | buf.WriteRune('[') 595 | 596 | for i := 0; i < v.Len(); i++ { 597 | if i > 0 { 598 | buf.WriteString(", ") 599 | } 600 | 601 | sv := v.Index(i) 602 | 603 | var val string 604 | 605 | switch sv.Kind() { 606 | case reflect.String: 607 | val = strconv.Quote(sv.String()) 608 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 609 | val = strconv.FormatInt(sv.Int(), 10) 610 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 611 | val = strconv.FormatUint(sv.Uint(), 10) 612 | default: 613 | val = fmt.Sprintf("%v", sv.Interface()) 614 | if strings.ContainsAny(val, " \t\n\r") { 615 | val = strconv.Quote(val) 616 | } 617 | } 618 | 619 | buf.WriteString(val) 620 | } 621 | 622 | buf.WriteRune(']') 623 | 624 | return buf.String() 625 | } 626 | 627 | // JSON logging function 628 | func (l *intLogger) logJSON(t time.Time, name string, level Level, msg string, args ...interface{}) { 629 | vals := l.jsonMapEntry(t, name, level, msg) 630 | args = append(l.implied, args...) 631 | 632 | if len(args) > 0 { 633 | if len(args)%2 != 0 { 634 | cs, ok := args[len(args)-1].(CapturedStacktrace) 635 | if ok { 636 | args = args[:len(args)-1] 637 | vals["stacktrace"] = cs 638 | } else { 639 | extra := args[len(args)-1] 640 | args = append(args[:len(args)-1], MissingKey, extra) 641 | } 642 | } 643 | 644 | for i := 0; i < len(args); i = i + 2 { 645 | val := args[i+1] 646 | switch sv := val.(type) { 647 | case error: 648 | // Check if val is of type error. If error type doesn't 649 | // implement json.Marshaler or encoding.TextMarshaler 650 | // then set val to err.Error() so that it gets marshaled 651 | switch sv.(type) { 652 | case json.Marshaler, encoding.TextMarshaler: 653 | default: 654 | val = sv.Error() 655 | } 656 | case Format: 657 | val = fmt.Sprintf(sv[0].(string), sv[1:]...) 658 | } 659 | 660 | var key string 661 | 662 | switch st := args[i].(type) { 663 | case string: 664 | key = st 665 | default: 666 | key = fmt.Sprintf("%s", st) 667 | } 668 | vals[key] = val 669 | } 670 | } 671 | 672 | encoder := json.NewEncoder(l.writer) 673 | encoder.SetEscapeHTML(l.jsonEscapeEnabled) 674 | if err := encoder.Encode(vals); err != nil { 675 | if _, ok := err.(*json.UnsupportedTypeError); ok { 676 | plainVal := l.jsonMapEntry(t, name, level, msg) 677 | plainVal["@warn"] = errJsonUnsupportedTypeMsg 678 | 679 | errEncoder := json.NewEncoder(l.writer) 680 | errEncoder.SetEscapeHTML(l.jsonEscapeEnabled) 681 | _ = errEncoder.Encode(plainVal) 682 | } 683 | } 684 | } 685 | 686 | func (l intLogger) jsonMapEntry(t time.Time, name string, level Level, msg string) map[string]interface{} { 687 | vals := map[string]interface{}{ 688 | "@message": msg, 689 | } 690 | if !l.disableTime { 691 | vals["@timestamp"] = t.Format(l.timeFormat) 692 | } 693 | 694 | var levelStr string 695 | switch level { 696 | case Error: 697 | levelStr = "error" 698 | case Warn: 699 | levelStr = "warn" 700 | case Info: 701 | levelStr = "info" 702 | case Debug: 703 | levelStr = "debug" 704 | case Trace: 705 | levelStr = "trace" 706 | default: 707 | levelStr = "all" 708 | } 709 | 710 | vals["@level"] = levelStr 711 | 712 | if name != "" { 713 | vals["@module"] = name 714 | } 715 | 716 | if l.callerOffset > 0 { 717 | if _, file, line, ok := runtime.Caller(l.callerOffset + 1); ok { 718 | vals["@caller"] = fmt.Sprintf("%s:%d", file, line) 719 | } 720 | } 721 | return vals 722 | } 723 | 724 | // Emit the message and args at the provided level 725 | func (l *intLogger) Log(level Level, msg string, args ...interface{}) { 726 | l.log(l.Name(), level, msg, args...) 727 | } 728 | 729 | // Emit the message and args at DEBUG level 730 | func (l *intLogger) Debug(msg string, args ...interface{}) { 731 | l.log(l.Name(), Debug, msg, args...) 732 | } 733 | 734 | // Emit the message and args at TRACE level 735 | func (l *intLogger) Trace(msg string, args ...interface{}) { 736 | l.log(l.Name(), Trace, msg, args...) 737 | } 738 | 739 | // Emit the message and args at INFO level 740 | func (l *intLogger) Info(msg string, args ...interface{}) { 741 | l.log(l.Name(), Info, msg, args...) 742 | } 743 | 744 | // Emit the message and args at WARN level 745 | func (l *intLogger) Warn(msg string, args ...interface{}) { 746 | l.log(l.Name(), Warn, msg, args...) 747 | } 748 | 749 | // Emit the message and args at ERROR level 750 | func (l *intLogger) Error(msg string, args ...interface{}) { 751 | l.log(l.Name(), Error, msg, args...) 752 | } 753 | 754 | // Indicate that the logger would emit TRACE level logs 755 | func (l *intLogger) IsTrace() bool { 756 | return l.GetLevel() == Trace 757 | } 758 | 759 | // Indicate that the logger would emit DEBUG level logs 760 | func (l *intLogger) IsDebug() bool { 761 | return l.GetLevel() <= Debug 762 | } 763 | 764 | // Indicate that the logger would emit INFO level logs 765 | func (l *intLogger) IsInfo() bool { 766 | return l.GetLevel() <= Info 767 | } 768 | 769 | // Indicate that the logger would emit WARN level logs 770 | func (l *intLogger) IsWarn() bool { 771 | return l.GetLevel() <= Warn 772 | } 773 | 774 | // Indicate that the logger would emit ERROR level logs 775 | func (l *intLogger) IsError() bool { 776 | return l.GetLevel() <= Error 777 | } 778 | 779 | const MissingKey = "EXTRA_VALUE_AT_END" 780 | 781 | // Return a sub-Logger for which every emitted log message will contain 782 | // the given key/value pairs. This is used to create a context specific 783 | // Logger. 784 | func (l *intLogger) With(args ...interface{}) Logger { 785 | var extra interface{} 786 | 787 | if len(args)%2 != 0 { 788 | extra = args[len(args)-1] 789 | args = args[:len(args)-1] 790 | } 791 | 792 | sl := l.copy() 793 | 794 | result := make(map[string]interface{}, len(l.implied)+len(args)) 795 | keys := make([]string, 0, len(l.implied)+len(args)) 796 | 797 | // Read existing args, store map and key for consistent sorting 798 | for i := 0; i < len(l.implied); i += 2 { 799 | key := l.implied[i].(string) 800 | keys = append(keys, key) 801 | result[key] = l.implied[i+1] 802 | } 803 | // Read new args, store map and key for consistent sorting 804 | for i := 0; i < len(args); i += 2 { 805 | key := args[i].(string) 806 | _, exists := result[key] 807 | if !exists { 808 | keys = append(keys, key) 809 | } 810 | result[key] = args[i+1] 811 | } 812 | 813 | // Sort keys to be consistent 814 | sort.Strings(keys) 815 | 816 | sl.implied = make([]interface{}, 0, len(l.implied)+len(args)) 817 | for _, k := range keys { 818 | sl.implied = append(sl.implied, k) 819 | sl.implied = append(sl.implied, result[k]) 820 | } 821 | 822 | if extra != nil { 823 | sl.implied = append(sl.implied, MissingKey, extra) 824 | } 825 | 826 | return l.subloggerHook(sl) 827 | } 828 | 829 | // Create a new sub-Logger that a name decending from the current name. 830 | // This is used to create a subsystem specific Logger. 831 | func (l *intLogger) Named(name string) Logger { 832 | sl := l.copy() 833 | 834 | if sl.name != "" { 835 | sl.name = sl.name + "." + name 836 | } else { 837 | sl.name = name 838 | } 839 | 840 | return l.subloggerHook(sl) 841 | } 842 | 843 | // Create a new sub-Logger with an explicit name. This ignores the current 844 | // name. This is used to create a standalone logger that doesn't fall 845 | // within the normal hierarchy. 846 | func (l *intLogger) ResetNamed(name string) Logger { 847 | sl := l.copy() 848 | 849 | sl.name = name 850 | 851 | return l.subloggerHook(sl) 852 | } 853 | 854 | func (l *intLogger) ResetOutput(opts *LoggerOptions) error { 855 | if opts.Output == nil { 856 | return errors.New("given output is nil") 857 | } 858 | 859 | l.mutex.Lock() 860 | defer l.mutex.Unlock() 861 | 862 | return l.resetOutput(opts) 863 | } 864 | 865 | func (l *intLogger) ResetOutputWithFlush(opts *LoggerOptions, flushable Flushable) error { 866 | if opts.Output == nil { 867 | return errors.New("given output is nil") 868 | } 869 | if flushable == nil { 870 | return errors.New("flushable is nil") 871 | } 872 | 873 | l.mutex.Lock() 874 | defer l.mutex.Unlock() 875 | 876 | if err := flushable.Flush(); err != nil { 877 | return err 878 | } 879 | 880 | return l.resetOutput(opts) 881 | } 882 | 883 | func (l *intLogger) resetOutput(opts *LoggerOptions) error { 884 | l.writer = newWriter(opts.Output, opts.Color) 885 | l.setColorization(opts) 886 | return nil 887 | } 888 | 889 | // Update the logging level on-the-fly. This will affect all subloggers as 890 | // well. 891 | func (l *intLogger) SetLevel(level Level) { 892 | if !l.syncParentLevel { 893 | atomic.StoreInt32(l.level, int32(level)) 894 | return 895 | } 896 | 897 | nsl := new(int32) 898 | *nsl = int32(level) 899 | 900 | l.level = nsl 901 | 902 | l.ownEpoch = atomic.AddUint64(l.curEpoch, 1) 903 | l.setEpoch = l.ownEpoch 904 | } 905 | 906 | func (l *intLogger) searchLevelPtr() *int32 { 907 | p := l.parent 908 | 909 | ptr := l.level 910 | 911 | max := l.setEpoch 912 | 913 | for p != nil { 914 | if p.setEpoch > max { 915 | max = p.setEpoch 916 | ptr = p.level 917 | } 918 | 919 | p = p.parent 920 | } 921 | 922 | return ptr 923 | } 924 | 925 | // Returns the current level 926 | func (l *intLogger) GetLevel() Level { 927 | // We perform the loads immediately to keep the CPU pipeline busy, which 928 | // effectively makes the second load cost nothing. Once loaded into registers 929 | // the comparison returns the already loaded value. The comparison is almost 930 | // always true, so the branch predictor should hit consistently with it. 931 | var ( 932 | curEpoch = atomic.LoadUint64(l.curEpoch) 933 | level = Level(atomic.LoadInt32(l.level)) 934 | own = l.ownEpoch 935 | ) 936 | 937 | if curEpoch == own { 938 | return level 939 | } 940 | 941 | // Perform the level sync process. We'll avoid doing this next time by seeing the 942 | // epoch as current. 943 | 944 | ptr := l.searchLevelPtr() 945 | l.level = ptr 946 | l.ownEpoch = curEpoch 947 | 948 | return Level(atomic.LoadInt32(ptr)) 949 | } 950 | 951 | // Create a *log.Logger that will send it's data through this Logger. This 952 | // allows packages that expect to be using the standard library log to actually 953 | // use this logger. 954 | func (l *intLogger) StandardLogger(opts *StandardLoggerOptions) *log.Logger { 955 | if opts == nil { 956 | opts = &StandardLoggerOptions{} 957 | } 958 | 959 | return log.New(l.StandardWriter(opts), "", 0) 960 | } 961 | 962 | func (l *intLogger) StandardWriter(opts *StandardLoggerOptions) io.Writer { 963 | newLog := *l 964 | if l.callerOffset > 0 { 965 | // the stack is 966 | // logger.printf() -> l.Output() ->l.out.writer(hclog:stdlogAdaptor.write) -> hclog:stdlogAdaptor.dispatch() 967 | // So plus 4. 968 | newLog.callerOffset = l.callerOffset + 4 969 | } 970 | return &stdlogAdapter{ 971 | log: &newLog, 972 | inferLevels: opts.InferLevels, 973 | inferLevelsWithTimestamp: opts.InferLevelsWithTimestamp, 974 | forceLevel: opts.ForceLevel, 975 | } 976 | } 977 | 978 | // Accept implements the SinkAdapter interface 979 | func (i *intLogger) Accept(name string, level Level, msg string, args ...interface{}) { 980 | i.log(name, level, msg, args...) 981 | } 982 | 983 | // ImpliedArgs returns the loggers implied args 984 | func (i *intLogger) ImpliedArgs() []interface{} { 985 | return i.implied 986 | } 987 | 988 | // Name returns the loggers name 989 | func (i *intLogger) Name() string { 990 | return i.name 991 | } 992 | 993 | // copy returns a shallow copy of the intLogger, replacing the level pointer 994 | // when necessary 995 | func (l *intLogger) copy() *intLogger { 996 | sl := *l 997 | 998 | if l.independentLevels { 999 | sl.level = new(int32) 1000 | *sl.level = *l.level 1001 | } else if l.syncParentLevel { 1002 | sl.parent = l 1003 | } 1004 | 1005 | return &sl 1006 | } 1007 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | var ( 15 | // DefaultOutput is used as the default log output. 16 | DefaultOutput io.Writer = os.Stderr 17 | 18 | // DefaultLevel is used as the default log level. 19 | DefaultLevel = Info 20 | ) 21 | 22 | // Level represents a log level. 23 | type Level int32 24 | 25 | const ( 26 | // NoLevel is a special level used to indicate that no level has been 27 | // set and allow for a default to be used. 28 | NoLevel Level = 0 29 | 30 | // Trace is the most verbose level. Intended to be used for the tracing 31 | // of actions in code, such as function enters/exits, etc. 32 | Trace Level = 1 33 | 34 | // Debug information for programmer low-level analysis. 35 | Debug Level = 2 36 | 37 | // Info information about steady state operations. 38 | Info Level = 3 39 | 40 | // Warn information about rare but handled events. 41 | Warn Level = 4 42 | 43 | // Error information about unrecoverable events. 44 | Error Level = 5 45 | 46 | // Off disables all logging output. 47 | Off Level = 6 48 | ) 49 | 50 | // Format is a simple convenience type for when formatting is required. When 51 | // processing a value of this type, the logger automatically treats the first 52 | // argument as a Printf formatting string and passes the rest as the values 53 | // to be formatted. For example: L.Info(Fmt{"%d beans/day", beans}). 54 | type Format []interface{} 55 | 56 | // Fmt returns a Format type. This is a convenience function for creating a Format 57 | // type. 58 | func Fmt(str string, args ...interface{}) Format { 59 | return append(Format{str}, args...) 60 | } 61 | 62 | // A simple shortcut to format numbers in hex when displayed with the normal 63 | // text output. For example: L.Info("header value", Hex(17)) 64 | type Hex int 65 | 66 | // A simple shortcut to format numbers in octal when displayed with the normal 67 | // text output. For example: L.Info("perms", Octal(17)) 68 | type Octal int 69 | 70 | // A simple shortcut to format numbers in binary when displayed with the normal 71 | // text output. For example: L.Info("bits", Binary(17)) 72 | type Binary int 73 | 74 | // A simple shortcut to format strings with Go quoting. Control and 75 | // non-printable characters will be escaped with their backslash equivalents in 76 | // output. Intended for untrusted or multiline strings which should be logged 77 | // as concisely as possible. 78 | type Quote string 79 | 80 | // ColorOption expresses how the output should be colored, if at all. 81 | type ColorOption uint8 82 | 83 | const ( 84 | // ColorOff is the default coloration, and does not 85 | // inject color codes into the io.Writer. 86 | ColorOff ColorOption = iota 87 | // AutoColor checks if the io.Writer is a tty, 88 | // and if so enables coloring. 89 | AutoColor 90 | // ForceColor will enable coloring, regardless of whether 91 | // the io.Writer is a tty or not. 92 | ForceColor 93 | ) 94 | 95 | // SupportsColor is an optional interface that can be implemented by the output 96 | // value. If implemented and SupportsColor() returns true, then AutoColor will 97 | // enable colorization. 98 | type SupportsColor interface { 99 | SupportsColor() bool 100 | } 101 | 102 | // LevelFromString returns a Level type for the named log level, or "NoLevel" if 103 | // the level string is invalid. This facilitates setting the log level via 104 | // config or environment variable by name in a predictable way. 105 | func LevelFromString(levelStr string) Level { 106 | // We don't care about case. Accept both "INFO" and "info". 107 | levelStr = strings.ToLower(strings.TrimSpace(levelStr)) 108 | switch levelStr { 109 | case "trace": 110 | return Trace 111 | case "debug": 112 | return Debug 113 | case "info": 114 | return Info 115 | case "warn": 116 | return Warn 117 | case "error": 118 | return Error 119 | case "off": 120 | return Off 121 | default: 122 | return NoLevel 123 | } 124 | } 125 | 126 | func (l Level) String() string { 127 | switch l { 128 | case Trace: 129 | return "trace" 130 | case Debug: 131 | return "debug" 132 | case Info: 133 | return "info" 134 | case Warn: 135 | return "warn" 136 | case Error: 137 | return "error" 138 | case NoLevel: 139 | return "none" 140 | case Off: 141 | return "off" 142 | default: 143 | return "unknown" 144 | } 145 | } 146 | 147 | // Logger describes the interface that must be implemented by all loggers. 148 | type Logger interface { 149 | // Args are alternating key, val pairs 150 | // keys must be strings 151 | // vals can be any type, but display is implementation specific 152 | // Emit a message and key/value pairs at a provided log level 153 | Log(level Level, msg string, args ...interface{}) 154 | 155 | // Emit a message and key/value pairs at the TRACE level 156 | Trace(msg string, args ...interface{}) 157 | 158 | // Emit a message and key/value pairs at the DEBUG level 159 | Debug(msg string, args ...interface{}) 160 | 161 | // Emit a message and key/value pairs at the INFO level 162 | Info(msg string, args ...interface{}) 163 | 164 | // Emit a message and key/value pairs at the WARN level 165 | Warn(msg string, args ...interface{}) 166 | 167 | // Emit a message and key/value pairs at the ERROR level 168 | Error(msg string, args ...interface{}) 169 | 170 | // Indicate if TRACE logs would be emitted. This and the other Is* guards 171 | // are used to elide expensive logging code based on the current level. 172 | IsTrace() bool 173 | 174 | // Indicate if DEBUG logs would be emitted. This and the other Is* guards 175 | IsDebug() bool 176 | 177 | // Indicate if INFO logs would be emitted. This and the other Is* guards 178 | IsInfo() bool 179 | 180 | // Indicate if WARN logs would be emitted. This and the other Is* guards 181 | IsWarn() bool 182 | 183 | // Indicate if ERROR logs would be emitted. This and the other Is* guards 184 | IsError() bool 185 | 186 | // ImpliedArgs returns With key/value pairs 187 | ImpliedArgs() []interface{} 188 | 189 | // Creates a sublogger that will always have the given key/value pairs 190 | With(args ...interface{}) Logger 191 | 192 | // Returns the Name of the logger 193 | Name() string 194 | 195 | // Create a logger that will prepend the name string on the front of all messages. 196 | // If the logger already has a name, the new value will be appended to the current 197 | // name. That way, a major subsystem can use this to decorate all it's own logs 198 | // without losing context. 199 | Named(name string) Logger 200 | 201 | // Create a logger that will prepend the name string on the front of all messages. 202 | // This sets the name of the logger to the value directly, unlike Named which honor 203 | // the current name as well. 204 | ResetNamed(name string) Logger 205 | 206 | // Updates the level. This should affect all related loggers as well, 207 | // unless they were created with IndependentLevels. If an 208 | // implementation cannot update the level on the fly, it should no-op. 209 | SetLevel(level Level) 210 | 211 | // Returns the current level 212 | GetLevel() Level 213 | 214 | // Return a value that conforms to the stdlib log.Logger interface 215 | StandardLogger(opts *StandardLoggerOptions) *log.Logger 216 | 217 | // Return a value that conforms to io.Writer, which can be passed into log.SetOutput() 218 | StandardWriter(opts *StandardLoggerOptions) io.Writer 219 | } 220 | 221 | // StandardLoggerOptions can be used to configure a new standard logger. 222 | type StandardLoggerOptions struct { 223 | // Indicate that some minimal parsing should be done on strings to try 224 | // and detect their level and re-emit them. 225 | // This supports the strings like [ERROR], [ERR] [TRACE], [WARN], [INFO], 226 | // [DEBUG] and strip it off before reapplying it. 227 | InferLevels bool 228 | 229 | // Indicate that some minimal parsing should be done on strings to try 230 | // and detect their level and re-emit them while ignoring possible 231 | // timestamp values in the beginning of the string. 232 | // This supports the strings like [ERROR], [ERR] [TRACE], [WARN], [INFO], 233 | // [DEBUG] and strip it off before reapplying it. 234 | // The timestamp detection may result in false positives and incomplete 235 | // string outputs. 236 | // InferLevelsWithTimestamp is only relevant if InferLevels is true. 237 | InferLevelsWithTimestamp bool 238 | 239 | // ForceLevel is used to force all output from the standard logger to be at 240 | // the specified level. Similar to InferLevels, this will strip any level 241 | // prefix contained in the logged string before applying the forced level. 242 | // If set, this override InferLevels. 243 | ForceLevel Level 244 | } 245 | 246 | type TimeFunction = func() time.Time 247 | 248 | // LoggerOptions can be used to configure a new logger. 249 | type LoggerOptions struct { 250 | // Name of the subsystem to prefix logs with 251 | Name string 252 | 253 | // The threshold for the logger. Anything less severe is suppressed 254 | Level Level 255 | 256 | // Where to write the logs to. Defaults to os.Stderr if nil 257 | Output io.Writer 258 | 259 | // An optional Locker in case Output is shared. This can be a sync.Mutex or 260 | // a NoopLocker if the caller wants control over output, e.g. for batching 261 | // log lines. 262 | Mutex Locker 263 | 264 | // Control if the output should be in JSON. 265 | JSONFormat bool 266 | 267 | // Control the escape switch of json.Encoder 268 | JSONEscapeDisabled bool 269 | 270 | // Include file and line information in each log line 271 | IncludeLocation bool 272 | 273 | // AdditionalLocationOffset is the number of additional stack levels to skip 274 | // when finding the file and line information for the log line 275 | AdditionalLocationOffset int 276 | 277 | // The time format to use instead of the default 278 | TimeFormat string 279 | 280 | // A function which is called to get the time object that is formatted using `TimeFormat` 281 | TimeFn TimeFunction 282 | 283 | // Control whether or not to display the time at all. This is required 284 | // because setting TimeFormat to empty assumes the default format. 285 | DisableTime bool 286 | 287 | // Color the output. On Windows, colored logs are only available for io.Writers that 288 | // are concretely instances of *os.File. 289 | Color ColorOption 290 | 291 | // Only color the header, not the body. This can help with readability of long messages. 292 | ColorHeaderOnly bool 293 | 294 | // Color the header and message body fields. This can help with readability 295 | // of long messages with multiple fields. 296 | ColorHeaderAndFields bool 297 | 298 | // A function which is called with the log information and if it returns true the value 299 | // should not be logged. 300 | // This is useful when interacting with a system that you wish to suppress the log 301 | // message for (because it's too noisy, etc) 302 | Exclude func(level Level, msg string, args ...interface{}) bool 303 | 304 | // IndependentLevels causes subloggers to be created with an independent 305 | // copy of this logger's level. This means that using SetLevel on this 306 | // logger will not affect any subloggers, and SetLevel on any subloggers 307 | // will not affect the parent or sibling loggers. 308 | IndependentLevels bool 309 | 310 | // When set, changing the level of a logger effects only it's direct sub-loggers 311 | // rather than all sub-loggers. For example: 312 | // a := logger.Named("a") 313 | // a.SetLevel(Error) 314 | // b := a.Named("b") 315 | // c := a.Named("c") 316 | // b.GetLevel() => Error 317 | // c.GetLevel() => Error 318 | // b.SetLevel(Info) 319 | // a.GetLevel() => Error 320 | // b.GetLevel() => Info 321 | // c.GetLevel() => Error 322 | // a.SetLevel(Warn) 323 | // a.GetLevel() => Warn 324 | // b.GetLevel() => Warn 325 | // c.GetLevel() => Warn 326 | SyncParentLevel bool 327 | 328 | // SubloggerHook registers a function that is called when a sublogger via 329 | // Named, With, or ResetNamed is created. If defined, the function is passed 330 | // the newly created Logger and the returned Logger is returned from the 331 | // original function. This option allows customization via interception and 332 | // wrapping of Logger instances. 333 | SubloggerHook func(sub Logger) Logger 334 | } 335 | 336 | // InterceptLogger describes the interface for using a logger 337 | // that can register different output sinks. 338 | // This is useful for sending lower level log messages 339 | // to a different output while keeping the root logger 340 | // at a higher one. 341 | type InterceptLogger interface { 342 | // Logger is the root logger for an InterceptLogger 343 | Logger 344 | 345 | // RegisterSink adds a SinkAdapter to the InterceptLogger 346 | RegisterSink(sink SinkAdapter) 347 | 348 | // DeregisterSink removes a SinkAdapter from the InterceptLogger 349 | DeregisterSink(sink SinkAdapter) 350 | 351 | // Create a interceptlogger that will prepend the name string on the front of all messages. 352 | // If the logger already has a name, the new value will be appended to the current 353 | // name. That way, a major subsystem can use this to decorate all it's own logs 354 | // without losing context. 355 | NamedIntercept(name string) InterceptLogger 356 | 357 | // Create a interceptlogger that will prepend the name string on the front of all messages. 358 | // This sets the name of the logger to the value directly, unlike Named which honor 359 | // the current name as well. 360 | ResetNamedIntercept(name string) InterceptLogger 361 | 362 | // Deprecated: use StandardLogger 363 | StandardLoggerIntercept(opts *StandardLoggerOptions) *log.Logger 364 | 365 | // Deprecated: use StandardWriter 366 | StandardWriterIntercept(opts *StandardLoggerOptions) io.Writer 367 | } 368 | 369 | // SinkAdapter describes the interface that must be implemented 370 | // in order to Register a new sink to an InterceptLogger 371 | type SinkAdapter interface { 372 | Accept(name string, level Level, msg string, args ...interface{}) 373 | } 374 | 375 | // Flushable represents a method for flushing an output buffer. It can be used 376 | // if Resetting the log to use a new output, in order to flush the writes to 377 | // the existing output beforehand. 378 | type Flushable interface { 379 | Flush() error 380 | } 381 | 382 | // OutputResettable provides ways to swap the output in use at runtime 383 | type OutputResettable interface { 384 | // ResetOutput swaps the current output writer with the one given in the 385 | // opts. Color options given in opts will be used for the new output. 386 | ResetOutput(opts *LoggerOptions) error 387 | 388 | // ResetOutputWithFlush swaps the current output writer with the one given 389 | // in the opts, first calling Flush on the given Flushable. Color options 390 | // given in opts will be used for the new output. 391 | ResetOutputWithFlush(opts *LoggerOptions, flushable Flushable) error 392 | } 393 | 394 | // Locker is used for locking output. If not set when creating a logger, a 395 | // sync.Mutex will be used internally. 396 | type Locker interface { 397 | // Lock is called when the output is going to be changed or written to 398 | Lock() 399 | 400 | // Unlock is called when the operation that called Lock() completes 401 | Unlock() 402 | } 403 | 404 | // NoopLocker implements locker but does nothing. This is useful if the client 405 | // wants tight control over locking, in order to provide grouping of log 406 | // entries or other functionality. 407 | type NoopLocker struct{} 408 | 409 | // Lock does nothing 410 | func (n NoopLocker) Lock() {} 411 | 412 | // Unlock does nothing 413 | func (n NoopLocker) Unlock() {} 414 | 415 | var _ Locker = (*NoopLocker)(nil) 416 | -------------------------------------------------------------------------------- /logger_loc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | // This file contains tests that are sensitive to their location in the file, 17 | // because they contain line numbers. They're basically "quarantined" from the 18 | // other tests because they break all the time when new tests are added. 19 | 20 | func TestLoggerLoc(t *testing.T) { 21 | t.Run("includes the caller location", func(t *testing.T) { 22 | var buf bytes.Buffer 23 | 24 | logger := New(&LoggerOptions{ 25 | Name: "test", 26 | Output: &buf, 27 | IncludeLocation: true, 28 | }) 29 | 30 | _, _, line, _ := runtime.Caller(0) 31 | 32 | logger.Info("this is test", "who", "programmer", "why", "testing is fun") 33 | 34 | str := buf.String() 35 | dataIdx := strings.IndexByte(str, ' ') 36 | rest := str[dataIdx+1:] 37 | 38 | assert.Equal(t, 39 | fmt.Sprintf( 40 | "[INFO] go-hclog/logger_loc_test.go:%d: test: this is test: who=programmer why=\"testing is fun\"\n", 41 | line+2), 42 | rest) 43 | }) 44 | 45 | t.Run("includes the caller location excluding helper functions", func(t *testing.T) { 46 | var buf bytes.Buffer 47 | 48 | logMe := func(l Logger) { 49 | l.Info("this is test", "who", "programmer", "why", "testing is fun") 50 | } 51 | 52 | logger := New(&LoggerOptions{ 53 | Name: "test", 54 | Output: &buf, 55 | IncludeLocation: true, 56 | AdditionalLocationOffset: 1, 57 | }) 58 | 59 | _, _, line, _ := runtime.Caller(0) 60 | 61 | logMe(logger) 62 | 63 | str := buf.String() 64 | dataIdx := strings.IndexByte(str, ' ') 65 | rest := str[dataIdx+1:] 66 | 67 | assert.Equal(t, 68 | fmt.Sprintf( 69 | "[INFO] go-hclog/logger_loc_test.go:%d: test: this is test: who=programmer why=\"testing is fun\"\n", 70 | line+2, 71 | ), 72 | rest) 73 | }) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package hclog 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "testing" 16 | "time" 17 | 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | type bufferingBuffer struct { 23 | held bytes.Buffer 24 | flushed bytes.Buffer 25 | } 26 | 27 | func (b *bufferingBuffer) Write(p []byte) (int, error) { 28 | return b.held.Write(p) 29 | } 30 | 31 | func (b *bufferingBuffer) String() string { 32 | return b.flushed.String() 33 | } 34 | 35 | func (b *bufferingBuffer) Flush() error { 36 | _, err := b.flushed.WriteString(b.held.String()) 37 | return err 38 | } 39 | 40 | func TestLogger(t *testing.T) { 41 | t.Run("uses default output if none is given", func(t *testing.T) { 42 | var buf bytes.Buffer 43 | DefaultOutput = &buf 44 | 45 | logger := New(&LoggerOptions{ 46 | Name: "test", 47 | }) 48 | 49 | logger.Info("this is test", "who", "programmer", "why", "testing") 50 | 51 | str := buf.String() 52 | dataIdx := strings.IndexByte(str, ' ') 53 | rest := str[dataIdx+1:] 54 | 55 | assert.Equal(t, "[INFO] test: this is test: who=programmer why=testing\n", rest) 56 | }) 57 | 58 | t.Run("formats log entries", func(t *testing.T) { 59 | var buf bytes.Buffer 60 | 61 | logger := New(&LoggerOptions{ 62 | Name: "test", 63 | Output: &buf, 64 | }) 65 | 66 | logger.Info("this is test", "who", "programmer", "why", "testing") 67 | 68 | str := buf.String() 69 | dataIdx := strings.IndexByte(str, ' ') 70 | rest := str[dataIdx+1:] 71 | 72 | assert.Equal(t, "[INFO] test: this is test: who=programmer why=testing\n", rest) 73 | }) 74 | 75 | t.Run("renders slice values specially", func(t *testing.T) { 76 | var buf bytes.Buffer 77 | 78 | logger := New(&LoggerOptions{ 79 | Name: "test", 80 | Output: &buf, 81 | }) 82 | 83 | logger.Info("this is test", "who", "programmer", "why", []interface{}{"testing", "dev", 1, uint64(5), []int{3, 4}}) 84 | 85 | str := buf.String() 86 | dataIdx := strings.IndexByte(str, ' ') 87 | rest := str[dataIdx+1:] 88 | 89 | assert.Equal(t, "[INFO] test: this is test: who=programmer why=[testing, dev, 1, 5, \"[3 4]\"]\n", rest) 90 | }) 91 | 92 | t.Run("renders values in slices with quotes", func(t *testing.T) { 93 | var buf bytes.Buffer 94 | 95 | logger := New(&LoggerOptions{ 96 | Name: "test", 97 | Output: &buf, 98 | }) 99 | 100 | logger.Info("this is test", "who", "programmer", "why", []string{"testing & qa", "dev"}) 101 | 102 | str := buf.String() 103 | dataIdx := strings.IndexByte(str, ' ') 104 | rest := str[dataIdx+1:] 105 | 106 | assert.Equal(t, "[INFO] test: this is test: who=programmer why=[\"testing & qa\", \"dev\"]\n", rest) 107 | }) 108 | 109 | t.Run("escapes quotes in values", func(t *testing.T) { 110 | var buf bytes.Buffer 111 | 112 | logger := New(&LoggerOptions{ 113 | Name: "test", 114 | Output: &buf, 115 | }) 116 | 117 | logger.Info("this is test", "who", "programmer", "why", `this is "quoted"`) 118 | 119 | str := buf.String() 120 | dataIdx := strings.IndexByte(str, ' ') 121 | rest := str[dataIdx+1:] 122 | 123 | assert.Equal(t, `[INFO] test: this is test: who=programmer why="this is \"quoted\""`+"\n", rest) 124 | }) 125 | 126 | t.Run("prints empty double quotes for empty strings", func(t *testing.T) { 127 | var buf bytes.Buffer 128 | 129 | logger := New(&LoggerOptions{ 130 | Name: "test", 131 | Output: &buf, 132 | }) 133 | 134 | logger.Info("this is test", "who", "programmer", "why", ``) 135 | 136 | str := buf.String() 137 | dataIdx := strings.IndexByte(str, ' ') 138 | rest := str[dataIdx+1:] 139 | 140 | assert.Equal(t, `[INFO] test: this is test: who=programmer why=""`+"\n", rest) 141 | }) 142 | 143 | t.Run("quotes when there are nonprintable sequences in a value", func(t *testing.T) { 144 | var buf bytes.Buffer 145 | 146 | logger := New(&LoggerOptions{ 147 | Name: "test", 148 | Output: &buf, 149 | }) 150 | 151 | logger.Info("this is test", "who", "programmer", "why", "\U0001F603") 152 | 153 | str := buf.String() 154 | dataIdx := strings.IndexByte(str, ' ') 155 | rest := str[dataIdx+1:] 156 | 157 | assert.Equal(t, "[INFO] test: this is test: who=programmer why=\"\U0001F603\"\n", rest) 158 | }) 159 | 160 | t.Run("formats multiline values nicely", func(t *testing.T) { 161 | var buf bytes.Buffer 162 | 163 | logger := New(&LoggerOptions{ 164 | Name: "test", 165 | Output: &buf, 166 | }) 167 | 168 | logger.Info("this is test", "who", "programmer", "why", "testing\nand other\npretty cool things") 169 | 170 | str := buf.String() 171 | dataIdx := strings.IndexByte(str, ' ') 172 | rest := str[dataIdx+1:] 173 | 174 | expected := `[INFO] test: this is test: who=programmer 175 | why= 176 | | testing 177 | | and other 178 | | pretty cool things` + "\n \n" 179 | assert.Equal(t, expected, rest) 180 | }) 181 | 182 | t.Run("handles backslash r in entries", func(t *testing.T) { 183 | var buf bytes.Buffer 184 | 185 | logger := New(&LoggerOptions{ 186 | Name: "test", 187 | Output: &buf, 188 | }) 189 | 190 | logger.Info("this is test", "who", "programmer", "why", "testing\n\rand other\n\rpretty cool things like \x01 and \u1680 and \U00101120") 191 | 192 | str := buf.String() 193 | dataIdx := strings.IndexByte(str, ' ') 194 | rest := str[dataIdx+1:] 195 | 196 | expected := `[INFO] test: this is test: who=programmer 197 | why= 198 | | testing 199 | | \rand other 200 | | \rpretty cool things like \x01 and \u1680 and \U00101120` + "\n \n" 201 | assert.Equal(t, expected, rest) 202 | }) 203 | 204 | t.Run("outputs stack traces", func(t *testing.T) { 205 | var buf bytes.Buffer 206 | 207 | logger := New(&LoggerOptions{ 208 | Name: "test", 209 | Output: &buf, 210 | }) 211 | 212 | logger.Info("who", "programmer", "why", "testing", Stacktrace()) 213 | 214 | lines := strings.Split(buf.String(), "\n") 215 | require.True(t, len(lines) > 1) 216 | 217 | assert.Equal(t, "github.com/hashicorp/go-hclog.Stacktrace", lines[1]) 218 | }) 219 | 220 | t.Run("outputs stack traces with it's given a name", func(t *testing.T) { 221 | var buf bytes.Buffer 222 | 223 | logger := New(&LoggerOptions{ 224 | Name: "test", 225 | Output: &buf, 226 | }) 227 | 228 | logger.Info("who", "programmer", "why", "testing", "foo", Stacktrace()) 229 | 230 | lines := strings.Split(buf.String(), "\n") 231 | require.True(t, len(lines) > 1) 232 | 233 | assert.Equal(t, "github.com/hashicorp/go-hclog.Stacktrace", lines[1]) 234 | }) 235 | 236 | t.Run("prefixes the name", func(t *testing.T) { 237 | var buf bytes.Buffer 238 | 239 | logger := New(&LoggerOptions{ 240 | // No name! 241 | Output: &buf, 242 | }) 243 | 244 | logger.Info("this is test") 245 | str := buf.String() 246 | dataIdx := strings.IndexByte(str, ' ') 247 | rest := str[dataIdx+1:] 248 | assert.Equal(t, "[INFO] this is test\n", rest) 249 | 250 | buf.Reset() 251 | 252 | another := logger.Named("sublogger") 253 | another.Info("this is test") 254 | str = buf.String() 255 | dataIdx = strings.IndexByte(str, ' ') 256 | rest = str[dataIdx+1:] 257 | assert.Equal(t, "[INFO] sublogger: this is test\n", rest) 258 | }) 259 | 260 | t.Run("can force colors to on in any context", func(t *testing.T) { 261 | if runtime.GOOS == "windows" { 262 | t.Skip("colors are different on windows") 263 | } 264 | 265 | var buf bytes.Buffer 266 | 267 | logger := New(&LoggerOptions{ 268 | // No name! 269 | Output: &buf, 270 | Level: Trace, 271 | Color: ForceColor, 272 | TimeFormat: "