├── .github └── workflows │ ├── gosec.yml │ ├── test.yml │ └── trivy.yml ├── LICENSE ├── README.md ├── async.go ├── async_test.go ├── const.go ├── emitter.go ├── emitter_test.go ├── error.go ├── error_test.go ├── example ├── basic │ └── main.go ├── httpauth │ └── main.go ├── json │ └── main.go └── level │ └── main.go ├── export_test.go ├── filter.go ├── filter ├── field.go ├── pii.go ├── tag.go ├── type.go └── value.go ├── filter_test.go ├── go.mod ├── go.sum ├── hook.go ├── hook_test.go ├── log_level.go ├── log_level_test.go ├── logger.go ├── logger_test.go ├── masking.go ├── masking_test.go ├── option.go ├── types.go └── types_test.go /.github/workflows/gosec.yml: -------------------------------------------------------------------------------- 1 | name: "Security Scan" 2 | 3 | # Run workflow each time code is pushed to your repository and on a schedule. 4 | # The scheduled workflow runs every at 00:00 on Sunday UTC time. 5 | on: 6 | push: 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | env: 12 | GO111MODULE: on 13 | steps: 14 | - name: Checkout Source 15 | uses: actions/checkout@v2 16 | - name: Run Gosec Security Scanner 17 | uses: securego/gosec@master 18 | with: 19 | # we let the report trigger content trigger a failure using the GitHub Security features. 20 | args: '-no-fail -fmt sarif -out results.sarif ./...' 21 | - name: Upload SARIF file 22 | uses: github/codeql-action/upload-sarif@v1 23 | with: 24 | # Path to SARIF file relative to the root of the repository 25 | sarif_file: results.sarif 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | testing: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout upstream repo 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: "1.18" 17 | - run: go test ./... 18 | - run: go vet ./... 19 | -------------------------------------------------------------------------------- /.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | name: Vulnerability scan 2 | 3 | on: [push] 4 | 5 | jobs: 6 | scan: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout upstream repo 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | - name: Run Trivy vulnerability scanner in repo mode 15 | uses: aquasecurity/trivy-action@master 16 | with: 17 | scan-type: "fs" 18 | ignore-unfixed: true 19 | format: "template" 20 | template: "@/contrib/sarif.tpl" 21 | output: "trivy-results.sarif" 22 | 23 | - name: Upload Trivy scan results to GitHub Security tab 24 | uses: github/codeql-action/upload-sarif@v1 25 | with: 26 | sarif_file: "trivy-results.sarif" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Masayoshi Mizutani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO Log SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zlog [![Go Reference](https://pkg.go.dev/badge/github.com/m-mizutani/zlog.svg)](https://pkg.go.dev/github.com/m-mizutani/zlog) [![Vulnerability scan](https://github.com/m-mizutani/zlog/actions/workflows/trivy.yml/badge.svg)](https://github.com/m-mizutani/zlog/actions/workflows/trivy.yml) [![Unit test](https://github.com/m-mizutani/zlog/actions/workflows/test.yml/badge.svg)](https://github.com/m-mizutani/zlog/actions/workflows/test.yml) [![Security Scan](https://github.com/m-mizutani/zlog/actions/workflows/gosec.yml/badge.svg)](https://github.com/m-mizutani/zlog/actions/workflows/gosec.yml) 2 | 3 | > [!IMPORTANT] 4 | > zlog has been serving two key roles in Go language: structured logging and the removal of confidential values. However, with Go 1.21, [slog](https://pkg.go.dev/log/slog), the official structured logging mechanism, has been formally incorporated. In addition, slog is equipped with a feature called ReplaceAttr, which enables the removal of confidential values as well. Moving forward, we recommend using slog for structured logging and a newly created package called [masq](https://github.com/m-mizutani/masq) for the removal of confidential values. We have decided to gradually stop maintaining zlog, believing that it has fulfilled its role, and will soon archive the repository. Thank you for your patronage. 5 | 6 | A main distinct feature of `zlog` is secure logging that avoid to output secret/sensitive values to log. The feature reduce risk to store secret values (API token, password and such things) and sensitive data like PII (Personal Identifiable Information) such as address, phone number, email address and etc into logging storage. 7 | 8 | `zlog` also has major logger features: contextual logging, leveled logging, structured message, show stacktrace of error. See following usage for mote detail. 9 | 10 | For example, adding `zlog:"secret"` tag to `Email` field in structure. 11 | 12 | ```go 13 | type myRecord struct { 14 | ID string 15 | EMail string `zlog:"secret"` 16 | } 17 | record := myRecord{ 18 | ID: "m-mizutani", 19 | EMail: "mizutani@hey.com", 20 | } 21 | 22 | logger := newExampleLogger(zlog.WithFilters(filter.Tag())) 23 | logger.With("record", record).Info("Got record") 24 | ``` 25 | 26 | Then, output following log with filtered `Email` field. 27 | 28 | ```bash 29 | [info] Got record 30 | "record" => zlog_test.myRecord{ 31 | ID: "m-mizutani", 32 | EMail: "[filtered]", 33 | } 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Basic example 39 | 40 | ```go 41 | import "github.com/m-mizutani/zlog" 42 | 43 | type myRecord struct { 44 | Name string 45 | EMail string 46 | } 47 | 48 | func main() { 49 | record := myRecord{ 50 | Name: "mizutani", 51 | EMail: "mizutani@hey.com", 52 | } 53 | 54 | logger := zlog.New() 55 | logger.With("record", record).Info("hello my logger") 56 | } 57 | ``` 58 | 59 | `zlog.New()` creates a new logger with default settings (console formatter). 60 | 61 | ![example](https://user-images.githubusercontent.com/605953/139518107-e1b1deb0-f9c8-4439-b527-7e3ae4e575a0.png) 62 | 63 | #### Contextual logging 64 | 65 | `Logger.With(key string, value interface{})` method allows contextual logging that output not only message but also related variables. The method saves a pair of key and value and output it by pretty printing (powered by [k0kubun/pp](https://github.com/k0kubun/pp)). 66 | 67 | 68 | ### Filter sensitive data 69 | 70 | #### By specified value 71 | 72 | This function is designed to hide limited and predetermined secret values, such as API tokens that the application itself uses to call external services. 73 | 74 | ```go 75 | const issuedToken = "abcd1234" 76 | authHeader := "Authorization: Bearer " + issuedToken 77 | 78 | logger := newExampleLogger(zlog.WithFilters( 79 | filter.Value(issuedToken), 80 | )) 81 | 82 | logger.With("auth", authHeader).Info("send header") 83 | // Output: [info] send header 84 | // "auth" => "Authorization: Bearer [filtered]" 85 | ``` 86 | 87 | #### By field name 88 | 89 | This filter hides the secret value if it matches the field name of the specified structure. 90 | 91 | ```go 92 | type myRecord struct { 93 | ID string 94 | Phone string 95 | } 96 | record := myRecord{ 97 | ID: "m-mizutani", 98 | Phone: "090-0000-0000", 99 | } 100 | 101 | logger := newExampleLogger( 102 | zlog.WithFilters(filter.Field("Phone")), 103 | ) 104 | logger.With("record", record).Info("Got record") 105 | // Output: [info] Got record 106 | // "record" => zlog_test.myRecord{ 107 | // ID: "m-mizutani", 108 | // Phone: "[filtered]", 109 | // } 110 | ``` 111 | 112 | #### By custom type 113 | 114 | You can define a type that you want to keep secret, and then specify it in a Filter to prevent it from being displayed. The advantage of this method is that copying a value from a custom type to the original type requires a cast, making it easier for the developer to notice unintentional copying. (Of course, this is not a perfect solution because you can still copy by casting.) 115 | 116 | This method may be useful for use cases where you need to use secret values between multiple structures. 117 | 118 | ```go 119 | type password string 120 | type myRecord struct { 121 | ID string 122 | EMail password 123 | } 124 | record := myRecord{ 125 | ID: "m-mizutani", 126 | EMail: "abcd1234", 127 | } 128 | 129 | logger := newExampleLogger( 130 | zlog.WithFilters(filter.Type(password(""))), 131 | ) 132 | logger.With("record", record).Info("Got record") 133 | // Output: [info] Got record 134 | // "record" => zlog_test.myRecord{ 135 | // ID: "m-mizutani", 136 | // EMail: "[filtered]", 137 | // } 138 | ``` 139 | 140 | #### By struct tag 141 | 142 | ```go 143 | type myRecord struct { 144 | ID string 145 | EMail string `zlog:"secret"` 146 | } 147 | record := myRecord{ 148 | ID: "m-mizutani", 149 | EMail: "mizutani@hey.com", 150 | } 151 | 152 | logger := newExampleLogger(zlog.WithFilters(filter.Tag())) 153 | logger.With("record", record).Info("Got record") 154 | // Output: [info] Got record 155 | // "record" => zlog_test.myRecord{ 156 | // ID: "m-mizutani", 157 | // EMail: "[filtered]", 158 | // } 159 | ``` 160 | 161 | #### By data pattern (e.g. personal information) 162 | 163 | This is an experimental effort and not a very reliable method, but it may have some value. It is a way to detect and hide personal information that should not be output based on a predefined pattern, like many DLP (Data Leakage Protection) solutions. 164 | 165 | In the following example, we use a filter that we wrote to detect Japanese phone numbers. The content is just a regular expression. Currently zlog does not have as many patterns as the existing DLP solutions, and the patterns are not accurate enough. However we hope to expand it in the future if necessary. 166 | 167 | ```go 168 | type myRecord struct { 169 | ID string 170 | Phone string 171 | } 172 | record := myRecord{ 173 | ID: "m-mizutani", 174 | Phone: "090-0000-0000", 175 | } 176 | 177 | logger := newExampleLogger(zlog.WithFilters(filter.PhoneNumber())) 178 | logger.With("record", record).Info("Got record") 179 | // Output: [info] Got record 180 | // "record" => zlog_test.myRecord{ 181 | // ID: "m-mizutani", 182 | // Phone: "[filtered]", 183 | // } 184 | ``` 185 | 186 | ### Customize Log output format 187 | 188 | zlog has `Emitter` that is interface to output log event. A default emitter is `Writer` that has `Formatter` to format log message, values and error information and `io.Writer` to output formatted log data. 189 | 190 | #### Change io.Writer 191 | 192 | For example, change output to standard error. 193 | 194 | ```go 195 | logger := logger := zlog.New( 196 | zlog.WithEmitter( 197 | zlog.NewWriterWith(zlog.NewConsoleFormatter(), os.Stderr), 198 | ), 199 | ) 200 | logger.Info("output to stderr") 201 | ``` 202 | 203 | #### Change formatter 204 | 205 | For example, use JsonFormatter to output structured json. 206 | 207 | ```go 208 | logger := zlog.New( 209 | zlog.WithEmitter( 210 | zlog.NewWriterWith(zlog.NewJsonFormatter(), os.Stdout), 211 | ), 212 | ) 213 | 214 | logger.Info("output as json format") 215 | // Output: {"timestamp":"2021-10-02T14:58:11.791258","level":"info","msg":"output as json format","values":null} 216 | ``` 217 | 218 | #### Use original emitter 219 | 220 | You can use your original Emitter that has `Emit(*zlog.Log) error` method. 221 | 222 | ```go 223 | type myEmitter struct { 224 | seq int 225 | } 226 | 227 | func (x *myEmitter) Emit(ev *zlog.Log) error { 228 | x.seq++ 229 | prefix := []string{"\(^o^)/", "(´・ω・`)", "(・∀・)"} 230 | fmt.Println(prefix[x.seq%3], ev.Msg) 231 | return nil 232 | } 233 | 234 | func ExampleEmitter() { 235 | logger := zlog.New( 236 | zlog.WithEmitter(&myEmitter{}), 237 | ) 238 | 239 | logger.Info("waiwai") 240 | logger.Info("heyhey") 241 | // Output: 242 | // \(^o^)/ waiwai 243 | // (´・ω・`) heyhey 244 | } 245 | ``` 246 | 247 | ### Leveled Logging 248 | 249 | zlog allows for logging at the following levels. 250 | 251 | - `trace` (`zlog.LevelTrace`) 252 | - `debug` (`zlog.LevelDebug`) 253 | - `info` (`zlog.LevelInfo`) 254 | - `warn` (`zlog.LevelWarn`) 255 | - `error` (`zlog.LevelError`) 256 | 257 | Log level can be changed by modifying `Logger.Level` or calling `Logger.SetLogLevel()` method. 258 | 259 | Modifying `Logger.Level` directly: 260 | ```go 261 | logger = zlog.New(zlog.WithLogLevel("info")) 262 | logger.Debug("debugging") 263 | logger.Info("information") 264 | // Output: [info] information 265 | ``` 266 | 267 | Using `SetLogLevel()` method. Log level is case insensitive. 268 | ```go 269 | logger.SetLogLevel("InFo") 270 | 271 | logger.Debug("debugging") 272 | logger.Info("information") 273 | // Output: [info] information 274 | ``` 275 | 276 | ### Error handling 277 | 278 | `Logger.Err(err error)` outputs not only error message but also stack trace and error related values. 279 | 280 | #### Output stack trace 281 | 282 | Support below error packages. 283 | 284 | - [github.com/pkg/errors](https://github.com/pkg/errors) 285 | - [github.com/m-mizutani/goerr](https://github.com/m-mizutani/goerr) 286 | 287 | ```go 288 | func crash() error { 289 | return errors.New("oops") 290 | } 291 | 292 | func main() { 293 | logger := zlog.New() 294 | if err := crash(); err != nil { 295 | logger.Err(err).Error("failed") 296 | } 297 | } 298 | 299 | // Output: 300 | // [error] failed 301 | // 302 | // ------------------ 303 | // *errors.fundamental: oops 304 | // 305 | // [StackTrace] 306 | // github.com/m-mizutani/zlog_test.crash 307 | // /Users/mizutani/.ghq/github.com/m-mizutani/zlog_test/main.go:xx 308 | // github.com/m-mizutani/zlog_test.main 309 | // /Users/mizutani/.ghq/github.com/m-mizutani/zlog_test/main.go:xx 310 | // runtime.main 311 | // /usr/local/Cellar/go/1.17/libexec/src/runtime/proc.go:255 312 | // runtime.goexit 313 | // /usr/local/Cellar/go/1.17/libexec/src/runtime/asm_amd64.s:1581 314 | // ------------------ 315 | ``` 316 | 317 | #### Output error related values 318 | 319 | ```go 320 | func crash(args string) error { 321 | return goerr.New("oops").With("args", args) 322 | } 323 | 324 | func main() { 325 | logger := zlog.New() 326 | if err := crash("hello"); err != nil { 327 | logger.Err(err).Error("failed") 328 | } 329 | } 330 | 331 | // Output: 332 | // [error] failed 333 | // 334 | // ------------------ 335 | // *goerr.Error: oops 336 | // 337 | // (snip) 338 | // 339 | // [Values] 340 | // args => "hello" 341 | // ------------------ 342 | ``` 343 | 344 | 345 | ## License 346 | 347 | - MIT License 348 | - Author: Masayoshi Mizutani 349 | -------------------------------------------------------------------------------- /async.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import "sync" 4 | 5 | type async struct { 6 | ch chan *asyncMsg 7 | wg sync.WaitGroup 8 | } 9 | 10 | type asyncMsg struct { 11 | logger *Logger 12 | log *Log 13 | } 14 | 15 | func newAsync(queueSize int) *async { 16 | x := &async{ 17 | ch: make(chan *asyncMsg, queueSize), 18 | } 19 | x.wg.Add(1) 20 | go func() { 21 | defer x.wg.Done() 22 | for msg := range x.ch { 23 | msg.logger.emit(msg.log) 24 | } 25 | }() 26 | 27 | return x 28 | } 29 | 30 | func (x *async) emit(logger *Logger, log *Log) { 31 | x.ch <- &asyncMsg{ 32 | logger: logger, 33 | log: log, 34 | } 35 | } 36 | 37 | func (x *async) flush() { 38 | close(x.ch) 39 | x.wg.Wait() 40 | } 41 | -------------------------------------------------------------------------------- /async_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/m-mizutani/zlog" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type delayWriter struct { 12 | output []byte 13 | } 14 | 15 | func (x *delayWriter) Write(data []byte) (int, error) { 16 | time.Sleep(time.Second) 17 | x.output = append(x.output, data...) 18 | return len(data), nil 19 | } 20 | 21 | func TestAsync(t *testing.T) { 22 | buf := &delayWriter{} 23 | logger := zlog.New( 24 | zlog.WithAsync(128), 25 | zlog.WithEmitter( 26 | zlog.NewJsonEmitter( 27 | zlog.JsonWriter(buf), 28 | ), 29 | ), 30 | ) 31 | 32 | logger.Info("blue") 33 | assert.NotContains(t, string(buf.output), "blue") 34 | logger.Flush() 35 | assert.Contains(t, string(buf.output), "blue") 36 | } 37 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import "github.com/m-mizutani/goerr" 4 | 5 | const ( 6 | Version = "v0.0.1" 7 | FilteredLabel = "[filtered]" 8 | ) 9 | 10 | var ( 11 | ErrInvalidLogLevel = goerr.New("invalid log level") 12 | ) 13 | -------------------------------------------------------------------------------- /emitter.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | 10 | "github.com/k0kubun/colorstring" 11 | "github.com/k0kubun/pp/v3" 12 | "github.com/m-mizutani/goerr" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type Emitter interface { 17 | Emit(*Log) error 18 | } 19 | 20 | // ConsoleEmitter outputs log to console with rich format. 21 | type ConsoleEmitter struct { 22 | timeFormat string 23 | noColor bool 24 | writer io.Writer 25 | prettyPrint bool 26 | printer *pp.PrettyPrinter 27 | } 28 | 29 | type ConsoleEmitterOption func(x *ConsoleEmitter) 30 | 31 | func ConsoleTimeFormat(format string) ConsoleEmitterOption { 32 | return func(x *ConsoleEmitter) { 33 | x.timeFormat = format 34 | } 35 | } 36 | func ConsoleNoColor() ConsoleEmitterOption { 37 | return func(x *ConsoleEmitter) { 38 | x.noColor = true 39 | x.printer.SetColoringEnabled(false) 40 | } 41 | } 42 | func ConsoleWriter(w io.Writer) ConsoleEmitterOption { 43 | return func(x *ConsoleEmitter) { 44 | x.writer = w 45 | } 46 | } 47 | func ConsolePrettyPrint(w io.Writer) ConsoleEmitterOption { 48 | return func(x *ConsoleEmitter) { 49 | x.prettyPrint = true 50 | } 51 | } 52 | 53 | func NewConsoleEmitter(options ...ConsoleEmitterOption) *ConsoleEmitter { 54 | emitter := &ConsoleEmitter{ 55 | timeFormat: "15:04:05.000", 56 | noColor: false, 57 | writer: os.Stdout, 58 | printer: pp.New(), 59 | } 60 | for _, opt := range options { 61 | opt(emitter) 62 | } 63 | return emitter 64 | } 65 | 66 | var colorMap = map[LogLevel]string{ 67 | LevelTrace: "blue", 68 | LevelDebug: "cyan", 69 | LevelInfo: "white", 70 | LevelWarn: "yellow", 71 | LevelError: "red", 72 | } 73 | 74 | func (x *ConsoleEmitter) Emit(log *Log) error { 75 | baseFmt := colorstring.Color("%s [[" + colorMap[log.Level] + "][bold]%s[reset]] %s") 76 | if x.noColor { 77 | baseFmt = "%s [%s] %s" 78 | } 79 | 80 | w := x.writer 81 | 82 | base := fmt.Sprintf(baseFmt, 83 | log.Timestamp.Format(x.timeFormat), 84 | log.Level.String(), 85 | log.Msg) 86 | if _, err := w.Write([]byte(base)); err != nil { 87 | return goerr.Wrap(err, "fail to write timestamp") 88 | } 89 | if _, err := w.Write([]byte("\n")); err != nil { 90 | return goerr.Wrap(err, "fail to write console") 91 | } 92 | 93 | if len(log.Values) > 0 { 94 | for _, k := range log.OrderedKeys() { 95 | v := log.Values[k] 96 | if _, err := w.Write([]byte(fmt.Sprintf("\"%s\" => ", k))); err != nil { 97 | return goerr.Wrap(err, "fail to write console") 98 | } 99 | if _, err := x.printer.Fprint(w, v); err != nil { 100 | return goerr.Wrap(err, "fail to write console") 101 | } 102 | if _, err := w.Write([]byte("\n")); err != nil { 103 | return goerr.Wrap(err, "fail to write console") 104 | } 105 | } 106 | if _, err := w.Write([]byte("\n")); err != nil { 107 | return goerr.Wrap(err, "fail to write console") 108 | } 109 | } 110 | 111 | if log.Error != nil { 112 | errType := reflect.TypeOf(log.Error) 113 | 114 | fmt.Fprintf(w, "----------------[StackTrace]----------------\n") 115 | fmt.Fprintf(w, "%s: %s\n", errType, log.Error.Cause.Error()) 116 | 117 | if len(log.Error.StackTrace) > 0 { 118 | fmt.Fprintf(w, "\n[StackTrace]\n") 119 | for _, frame := range log.Error.StackTrace { 120 | fmt.Fprintf(w, "%s\n\t%s:%d\n", frame.Function, frame.Filename, frame.Lineno) 121 | } 122 | } 123 | if log.Error.Values != nil && len(log.Error.Values) > 0 { 124 | fmt.Fprintf(w, "\n[Values]\n") 125 | for key, value := range log.Error.Values { 126 | if _, err := fmt.Fprintf(w, "%s => ", key); err != nil { 127 | return goerr.Wrap(err, "fail to write console") 128 | } 129 | if _, err := x.printer.Fprint(w, value); err != nil { 130 | return goerr.Wrap(err, "fail to write console") 131 | } 132 | if _, err := fmt.Fprintf(w, "\n"); err != nil { 133 | return goerr.Wrap(err, "fail to write console") 134 | } 135 | } 136 | } 137 | fmt.Fprintf(w, "--------------------------------------------\n") 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // JsonEmitter outputs log as one line JSON text 144 | type JsonEmitter struct { 145 | timeFormat string 146 | writer io.Writer 147 | prettyPrint bool 148 | } 149 | 150 | func NewJsonEmitter(options ...JsonEmitterOption) *JsonEmitter { 151 | emitter := &JsonEmitter{ 152 | timeFormat: "2006-01-02T15:04:05.000000", 153 | writer: os.Stdout, 154 | } 155 | 156 | for _, opt := range options { 157 | opt(emitter) 158 | } 159 | 160 | return emitter 161 | } 162 | 163 | type JsonEmitterOption func(x *JsonEmitter) 164 | 165 | func JsonTimeFormat(format string) JsonEmitterOption { 166 | return func(x *JsonEmitter) { 167 | x.timeFormat = format 168 | } 169 | } 170 | func JsonWriter(w io.Writer) JsonEmitterOption { 171 | return func(x *JsonEmitter) { 172 | x.writer = w 173 | } 174 | } 175 | func JsonPrettyPrint() JsonEmitterOption { 176 | return func(x *JsonEmitter) { 177 | x.prettyPrint = true 178 | } 179 | } 180 | 181 | type jsonMsg struct { 182 | Timestamp string `json:"timestamp"` 183 | Level string `json:"level"` 184 | Msg string `json:"msg"` 185 | Values map[string]any `json:"values,omitempty"` 186 | Error *jsonError `json:"error,omitempty"` 187 | } 188 | 189 | type jsonErrorStack struct { 190 | Function string `json:"function"` 191 | File string `json:"file"` 192 | } 193 | 194 | type jsonErrorMsg struct { 195 | Msg string `json:"msg"` 196 | Type string `json:"type"` 197 | } 198 | 199 | type jsonError struct { 200 | jsonErrorMsg 201 | Causes []*jsonErrorMsg `json:"causes,omitempty"` 202 | StackTrace []*jsonErrorStack `json:"stacktrace,omitempty"` 203 | Values map[string]any `json:"values,omitempty"` 204 | } 205 | 206 | func newjsonError(err *Error) *jsonError { 207 | if err == nil { 208 | return nil 209 | } 210 | 211 | jerr := &jsonError{ 212 | jsonErrorMsg: jsonErrorMsg{ 213 | Msg: err.Cause.Error(), 214 | Type: reflect.TypeOf(err.Cause).String(), 215 | }, 216 | } 217 | 218 | cause := err.Cause 219 | for { 220 | if unwrapped := errors.Cause(cause); cause != unwrapped { 221 | cause = unwrapped 222 | } else { 223 | break 224 | } 225 | 226 | jerr.Causes = append(jerr.Causes, &jsonErrorMsg{ 227 | Msg: cause.Error(), 228 | Type: reflect.TypeOf(cause).String(), 229 | }) 230 | } 231 | 232 | if len(err.StackTrace) > 0 { 233 | for _, frame := range err.StackTrace { 234 | jerr.StackTrace = append(jerr.StackTrace, &jsonErrorStack{ 235 | Function: frame.Function, 236 | File: fmt.Sprintf("%s:%d", frame.Filename, frame.Lineno), 237 | }) 238 | } 239 | } 240 | if err.Values != nil && len(err.Values) > 0 { 241 | jerr.Values = make(map[string]any) 242 | for key, value := range err.Values { 243 | jerr.Values[fmt.Sprintf("%v", key)] = value 244 | } 245 | } 246 | 247 | return jerr 248 | } 249 | 250 | func (x *JsonEmitter) Emit(log *Log) error { 251 | m := jsonMsg{ 252 | Timestamp: log.Timestamp.Format(x.timeFormat), 253 | Level: log.Level.String(), 254 | Msg: log.Msg, 255 | Values: log.Values, 256 | Error: newjsonError(log.Error), 257 | } 258 | 259 | encoder := json.NewEncoder(x.writer) 260 | if x.prettyPrint { 261 | encoder.SetIndent("", " ") 262 | } 263 | if err := encoder.Encode(m); err != nil { 264 | return goerr.Wrap(err) 265 | } 266 | return nil 267 | } 268 | -------------------------------------------------------------------------------- /emitter_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/m-mizutani/zlog" 7 | ) 8 | 9 | type myEmitter struct { 10 | seq int 11 | } 12 | 13 | func (x *myEmitter) Emit(ev *zlog.Log) error { 14 | prefix := []string{"\(^o^)/", "(´・ω・`)", "(・∀・)"} 15 | fmt.Println(prefix[x.seq%3], ev.Msg) 16 | x.seq++ 17 | return nil 18 | } 19 | 20 | func ExampleEmitter() { 21 | logger := zlog.New(zlog.WithEmitter(&myEmitter{})) 22 | 23 | logger.Info("waiwai") 24 | logger.Info("heyhey") 25 | // Output: 26 | // \(^o^)/ waiwai 27 | // (´・ω・`) heyhey 28 | } 29 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/m-mizutani/goerr" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type pkgErrorsStackTracer interface { 14 | StackTrace() errors.StackTrace 15 | } 16 | 17 | type Frame struct { 18 | Function string 19 | Filename string 20 | Lineno int 21 | } 22 | 23 | type Error struct { 24 | Cause error 25 | StackTrace []*Frame 26 | Values map[string]any 27 | } 28 | 29 | func newError(err error, m *masking) *Error { 30 | if err == nil { 31 | return nil 32 | } 33 | 34 | values := extractErrorValues(err) 35 | 36 | maskedValues := map[string]any{} 37 | for key, value := range values { 38 | maskedValues[key] = m.clone(key, reflect.ValueOf(value), "").Interface() 39 | } 40 | 41 | return &Error{ 42 | Cause: err, 43 | StackTrace: extractStackTrace(err), 44 | Values: maskedValues, 45 | } 46 | } 47 | 48 | func extractStackTrace(err error) []*Frame { 49 | switch e := err.(type) { 50 | case pkgErrorsStackTracer: 51 | var frames []*Frame 52 | for _, frame := range e.StackTrace() { 53 | // Ignore if failed to parse 54 | l, _ := strconv.ParseInt(fmt.Sprintf("%d", frame), 10, 64) 55 | f := strings.Split(fmt.Sprintf("%+s", frame), "\n\t") 56 | frames = append(frames, &Frame{ 57 | Filename: f[1], 58 | Function: f[0], 59 | Lineno: int(l), 60 | }) 61 | } 62 | return frames 63 | 64 | case *goerr.Error: 65 | var frames []*Frame 66 | for _, stack := range e.Stacks() { 67 | frames = append(frames, &Frame{ 68 | Function: stack.Func, 69 | Filename: stack.File, 70 | Lineno: stack.Line, 71 | }) 72 | } 73 | return frames 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func extractErrorValues(err error) map[string]any { 80 | var goErr *goerr.Error 81 | switch { 82 | case errors.As(err, &goErr): 83 | values := map[string]any{} 84 | for k, v := range goErr.Values() { 85 | values[fmt.Sprintf("%v", k)] = v 86 | } 87 | return values 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/m-mizutani/goerr" 8 | "github.com/m-mizutani/zlog" 9 | "github.com/m-mizutani/zlog/filter" 10 | "github.com/pkg/errors" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func crash1() error { 15 | return errors.New("oops") 16 | } 17 | 18 | func crash2() error { 19 | return goerr.New("oops").With("param", "value") 20 | } 21 | 22 | func TestErrWithPkgErrors(t *testing.T) { 23 | buf := &bytes.Buffer{} 24 | emitter := zlog.NewConsoleEmitter( 25 | zlog.ConsoleNoColor(), 26 | zlog.ConsoleWriter(buf), 27 | ) 28 | logger := zlog.New(zlog.WithEmitter(emitter)) 29 | 30 | logger.Err(crash1()).Error("bomb!") 31 | 32 | output := buf.String() 33 | assert.Contains(t, output, "[StackTrace]\ngithub.com/m-mizutani/zlog_test.crash1\n") 34 | assert.Contains(t, output, "/zlog/error_test.go:30\n") 35 | assert.NotContains(t, output, "[Values]\nparam => \"value\"\n") 36 | 37 | // Output: 38 | // [error] bomb! 39 | // 40 | // ------------------ 41 | // *errors.fundamental: oops 42 | // 43 | // [StackTrace] 44 | // github.com/m-mizutani/zlog_test.crash1 45 | // /Users/mizutani/.ghq/github.com/m-mizutani/zlog/error_test.go:14 46 | // github.com/m-mizutani/zlog_test.ExampleErrWithPkgErrors 47 | // /Users/mizutani/.ghq/github.com/m-mizutani/zlog/error_test.go:23 48 | // testing.runExample 49 | // /usr/local/Cellar/go/1.17/libexec/src/testing/run_example.go:64 50 | // testing.runExamples 51 | // /usr/local/Cellar/go/1.17/libexec/src/testing/example.go:44 52 | // testing.(*M).Run 53 | // /usr/local/Cellar/go/1.17/libexec/src/testing/testing.go:1505 54 | // main.main 55 | // _testmain.go:61 56 | // runtime.main 57 | // /usr/local/Cellar/go/1.17/libexec/src/runtime/proc.go:255 58 | // runtime.goexit 59 | // /usr/local/Cellar/go/1.17/libexec/src/runtime/asm_amd64.s:1581 60 | // ------------------ 61 | } 62 | 63 | func TestErrWithPkgErrorsWithJSON(t *testing.T) { 64 | buf := &bytes.Buffer{} 65 | logger := zlog.New( 66 | zlog.WithEmitter(zlog.NewJsonEmitter(zlog.JsonWriter(buf))), 67 | ) 68 | 69 | logger.Err(errors.Wrap(crash1(), "wrapped")).Error("bomb!") 70 | 71 | assert.Contains(t, buf.String(), `"msg":"wrapped: oops"`) 72 | assert.Contains(t, buf.String(), `"function":"github.com/m-mizutani/zlog_test.TestErrWithPkgErrorsWithJSON"`) 73 | } 74 | 75 | func TestErrWithGoErrWithJSON(t *testing.T) { 76 | buf := &bytes.Buffer{} 77 | logger := zlog.New( 78 | zlog.WithEmitter(zlog.NewJsonEmitter(zlog.JsonWriter(buf))), 79 | zlog.WithErrHook(func(err error, l *zlog.Log) { 80 | t.Log(err) 81 | t.FailNow() 82 | }), 83 | ) 84 | 85 | logger.Err(goerr.Wrap(crash2(), "wrapped")).Error("bomb!") 86 | 87 | assert.Contains(t, buf.String(), `"msg":"wrapped: oops"`) 88 | assert.Contains(t, buf.String(), `"function":"github.com/m-mizutani/zlog_test.TestErrWithGoErrWithJSON"`) 89 | } 90 | 91 | func TestErrWithGoErr(t *testing.T) { 92 | buf := &bytes.Buffer{} 93 | emitter := zlog.NewConsoleEmitter( 94 | zlog.ConsoleNoColor(), 95 | zlog.ConsoleWriter(buf), 96 | ) 97 | logger := zlog.New(zlog.WithEmitter(emitter)) 98 | 99 | logger.Err(crash2()).Error("bomb!") 100 | 101 | output := buf.String() 102 | assert.Contains(t, output, "[StackTrace]\ngithub.com/m-mizutani/zlog_test.crash2\n") 103 | assert.Contains(t, output, "/zlog/error_test.go:19\n") 104 | assert.Contains(t, output, "[Values]\nparam => \"value\"\n") 105 | } 106 | 107 | func TestErrValueFilter(t *testing.T) { 108 | buf := &bytes.Buffer{} 109 | emitter := zlog.NewConsoleEmitter( 110 | zlog.ConsoleNoColor(), 111 | zlog.ConsoleWriter(buf), 112 | ) 113 | logger := zlog.New( 114 | zlog.WithEmitter(emitter), 115 | zlog.WithFilters(filter.Field("Password")), 116 | ) 117 | 118 | v := struct { 119 | Password string 120 | }{ 121 | Password: "abc123", 122 | } 123 | err := goerr.New("missing potato").With("v", v) 124 | logger.Err(err).Error("bomb!") 125 | 126 | s := buf.String() 127 | assert.Contains(t, s, "[filtered]") 128 | assert.NotContains(t, s, "abc123") 129 | } 130 | -------------------------------------------------------------------------------- /example/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/m-mizutani/zlog" 5 | ) 6 | 7 | type myRecord struct { 8 | Name string 9 | EMail string 10 | } 11 | 12 | func main() { 13 | record := myRecord{ 14 | Name: "mizutani", 15 | EMail: "mizutani@hey.com", 16 | } 17 | 18 | logger := zlog.New() 19 | logger.With("record", record).Info("hello my logger") 20 | } 21 | -------------------------------------------------------------------------------- /example/httpauth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/m-mizutani/zlog" 7 | ) 8 | 9 | type httpAuthFilter struct{} 10 | 11 | func (x *httpAuthFilter) ReplaceString(s string) string { 12 | return s 13 | } 14 | 15 | func (x *httpAuthFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 16 | if fieldName != "Authorization" { 17 | return false 18 | } 19 | if _, ok := value.([]string); !ok { 20 | return false 21 | } 22 | 23 | return true 24 | } 25 | 26 | func main() { 27 | logger := zlog.New(zlog.WithFilters(&httpAuthFilter{})) 28 | 29 | req, _ := http.NewRequest("GET", "https://example.com", nil) 30 | 31 | req.Header.Add("Authorization", "Barer xxxx") 32 | 33 | logger.With("req", req).Info("send request") 34 | } 35 | -------------------------------------------------------------------------------- /example/json/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/m-mizutani/zlog" 5 | "github.com/m-mizutani/zlog/filter" 6 | ) 7 | 8 | type request struct { 9 | Name string 10 | Password string `zlog:"secure"` 11 | } 12 | 13 | func main() { 14 | logger := zlog.New( 15 | zlog.WithEmitter(zlog.NewJsonEmitter( 16 | zlog.JsonPrettyPrint(), 17 | )), 18 | zlog.WithFilters(filter.Tag("secure")), 19 | ) 20 | 21 | req := &request{ 22 | Name: "mizutani", 23 | Password: "abc123", 24 | } 25 | 26 | logger.With("req", req).Info("send request") 27 | } 28 | -------------------------------------------------------------------------------- /example/level/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/m-mizutani/zlog" 4 | 5 | func main() { 6 | logger := zlog.New(zlog.WithLogLevel("trace")) 7 | 8 | logger.Trace("not") 9 | logger.Debug("sane") 10 | logger.Info("five") 11 | logger.Warn("timeless") 12 | logger.Error("words") 13 | } 14 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import "reflect" 4 | 5 | func NewMasking(filters Filters) *masking { 6 | return newMasking(filters) 7 | } 8 | 9 | func (x *masking) Clone(v interface{}) interface{} { 10 | return x.clone("", reflect.ValueOf(v), "").Interface() 11 | } 12 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | type Filter interface { 4 | // ReplaceString is called when checking string type. The argument is the value to be checked, and the return value should be the value to be replaced. If nothing needs to be done, the method should return the argument as is. This method is intended for the case where you want to hide a part of a string. 5 | ReplaceString(s string) string 6 | 7 | // ShouldMask is called for all values to be checked. The field name of the value to be checked, the value to be checked, and tag value if the structure has `zlog` tag will be passed as arguments. If the return value is false, nothing is done; if it is true, the entire field is hidden. Hidden values will be replaced with the value "[filtered]" if string type. For other type, empty value will be set. 8 | ShouldMask(fieldName string, value interface{}, tag string) bool 9 | } 10 | 11 | type Filters []Filter 12 | 13 | func (x Filters) ReplaceString(s string) string { 14 | for _, f := range x { 15 | s = f.ReplaceString(s) 16 | } 17 | return s 18 | } 19 | 20 | func (x Filters) ShouldMask(fieldName string, value interface{}, tag string) bool { 21 | for _, f := range x { 22 | if f.ShouldMask(fieldName, value, tag) { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /filter/field.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "strings" 4 | 5 | type FieldFilter struct { 6 | target string 7 | } 8 | 9 | func Field(target string) *FieldFilter { 10 | return &FieldFilter{ 11 | target: target, 12 | } 13 | } 14 | 15 | func (x *FieldFilter) ReplaceString(s string) string { 16 | return s 17 | } 18 | 19 | func (x *FieldFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 20 | return x.target == fieldName 21 | } 22 | 23 | type FieldPrefixFilter struct { 24 | prefix string 25 | } 26 | 27 | func FieldPrefix(prefix string) *FieldPrefixFilter { 28 | return &FieldPrefixFilter{ 29 | prefix: prefix, 30 | } 31 | } 32 | 33 | func (x *FieldPrefixFilter) ReplaceString(s string) string { 34 | return s 35 | } 36 | 37 | func (x *FieldPrefixFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 38 | return strings.HasPrefix(fieldName, x.prefix) 39 | } 40 | -------------------------------------------------------------------------------- /filter/pii.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/m-mizutani/zlog" 7 | ) 8 | 9 | type PhoneNumberFilter struct { 10 | RegexSet []regexp.Regexp 11 | } 12 | 13 | func PhoneNumber() *PhoneNumberFilter { 14 | return &PhoneNumberFilter{ 15 | RegexSet: []regexp.Regexp{ 16 | *regexp.MustCompile("[0-9]{2,4}-[0-9]{2,4}-[0-9]{4}"), // japan phone number format 17 | }, 18 | } 19 | } 20 | 21 | func (x *PhoneNumberFilter) ReplaceString(s string) string { 22 | for _, p := range x.RegexSet { 23 | s = p.ReplaceAllString(s, zlog.FilteredLabel) 24 | } 25 | return s 26 | } 27 | 28 | func (x *PhoneNumberFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /filter/tag.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | type TagFilter struct { 4 | SecureTags []string 5 | } 6 | 7 | const defaultFilterTagName = "secret" 8 | 9 | func Tag(tags ...string) *TagFilter { 10 | if len(tags) == 0 { 11 | tags = []string{defaultFilterTagName} 12 | } 13 | return &TagFilter{ 14 | SecureTags: tags, 15 | } 16 | } 17 | 18 | func (x *TagFilter) ReplaceString(s string) string { return s } 19 | 20 | func (x *TagFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 21 | for i := range x.SecureTags { 22 | if x.SecureTags[i] == tag { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /filter/type.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/m-mizutani/zlog" 7 | ) 8 | 9 | type TypeFilter struct { 10 | target reflect.Type 11 | zlog.Filter 12 | } 13 | 14 | func Type(t interface{}) *TypeFilter { 15 | return &TypeFilter{ 16 | target: reflect.TypeOf(t), 17 | } 18 | } 19 | 20 | func (x *TypeFilter) ReplaceString(s string) string { return s } 21 | 22 | func (x *TypeFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 23 | return x.target == reflect.TypeOf(value) 24 | } 25 | -------------------------------------------------------------------------------- /filter/value.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/m-mizutani/zlog" 7 | ) 8 | 9 | type ValueFilter struct { 10 | target string 11 | } 12 | 13 | func Value(target string) *ValueFilter { 14 | return &ValueFilter{ 15 | target: target, 16 | } 17 | } 18 | 19 | func (x *ValueFilter) ReplaceString(s string) string { 20 | return strings.ReplaceAll(s, x.target, zlog.FilteredLabel) 21 | } 22 | 23 | func (x *ValueFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/m-mizutani/zlog" 7 | "github.com/m-mizutani/zlog/filter" 8 | ) 9 | 10 | func newExampleLogger(options ...zlog.Option) *zlog.Logger { 11 | emitter := zlog.NewConsoleEmitter( 12 | zlog.ConsoleNoColor(), 13 | zlog.ConsoleTimeFormat(""), 14 | ) 15 | logger := zlog.New(append(options, zlog.WithEmitter(emitter))...) 16 | 17 | return logger 18 | } 19 | 20 | func ExampleTypeFilter() { 21 | 22 | type password string 23 | type myRecord struct { 24 | ID string 25 | EMail password 26 | } 27 | record := myRecord{ 28 | ID: "m-mizutani", 29 | EMail: "abcd1234", 30 | } 31 | 32 | logger := newExampleLogger( 33 | zlog.WithFilters(filter.Type(password(""))), 34 | ) 35 | logger.With("record", record).Info("Got record") 36 | // Output: [info] Got record 37 | // "record" => zlog_test.myRecord{ 38 | // ID: "m-mizutani", 39 | // EMail: "[filtered]", 40 | // } 41 | } 42 | 43 | func ExampleValueFilter() { 44 | const issuedToken = "abcd1234" 45 | authHeader := "Authorization: Bearer " + issuedToken 46 | 47 | logger := newExampleLogger(zlog.WithFilters( 48 | filter.Value(issuedToken), 49 | ), zlog.WithClock(func() time.Time { 50 | return time.Date(2021, 4, 20, 5, 12, 19, 0, time.Local) 51 | }), 52 | ) 53 | 54 | logger.With("auth", authHeader).Info("send header") 55 | // Output: [info] send header 56 | // "auth" => "Authorization: Bearer [filtered]" 57 | } 58 | 59 | func ExampleTagFilter() { 60 | type myRecord struct { 61 | ID string 62 | EMail string `zlog:"secret"` 63 | } 64 | record := myRecord{ 65 | ID: "m-mizutani", 66 | EMail: "mizutani@hey.com", 67 | } 68 | 69 | logger := newExampleLogger( 70 | zlog.WithFilters(filter.Tag()), 71 | zlog.WithClock(func() time.Time { 72 | return time.Date(2021, 4, 20, 5, 12, 19, 0, time.Local) 73 | }), 74 | ) 75 | logger.With("record", record).Info("Got record") 76 | // Output: [info] Got record 77 | // "record" => zlog_test.myRecord{ 78 | // ID: "m-mizutani", 79 | // EMail: "[filtered]", 80 | // } 81 | } 82 | 83 | func ExamplePhoneNumberFilter() { 84 | type myRecord struct { 85 | ID string 86 | Phone string 87 | } 88 | record := myRecord{ 89 | ID: "m-mizutani", 90 | Phone: "090-0000-0000", 91 | } 92 | 93 | logger := newExampleLogger(zlog.WithFilters(filter.PhoneNumber())) 94 | logger.With("record", record).Info("Got record") 95 | // Output: [info] Got record 96 | // "record" => zlog_test.myRecord{ 97 | // ID: "m-mizutani", 98 | // Phone: "[filtered]", 99 | // } 100 | } 101 | 102 | func ExampleFieldFilter() { 103 | type myRecord struct { 104 | ID string 105 | Phone string 106 | } 107 | record := myRecord{ 108 | ID: "m-mizutani", 109 | Phone: "090-0000-0000", 110 | } 111 | 112 | logger := newExampleLogger( 113 | zlog.WithFilters(filter.Field("Phone")), 114 | ) 115 | logger.With("record", record).Info("Got record") 116 | // Output: [info] Got record 117 | // "record" => zlog_test.myRecord{ 118 | // ID: "m-mizutani", 119 | // Phone: "[filtered]", 120 | // } 121 | } 122 | 123 | func ExampleFieldPrefixFilter() { 124 | type myRecord struct { 125 | ID string 126 | SecurePhone string 127 | } 128 | record := myRecord{ 129 | ID: "m-mizutani", 130 | SecurePhone: "090-0000-0000", 131 | } 132 | 133 | logger := newExampleLogger(zlog.WithFilters(filter.FieldPrefix("Secure"))) 134 | 135 | logger.With("record", record).Info("Got record") 136 | // Output: [info] Got record 137 | // "record" => zlog_test.myRecord{ 138 | // ID: "m-mizutani", 139 | // SecurePhone: "[filtered]", 140 | // } 141 | } 142 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/m-mizutani/zlog 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 7 | github.com/k0kubun/pp/v3 v3.2.0 8 | github.com/m-mizutani/goerr v0.1.7 9 | github.com/pkg/errors v0.9.1 10 | github.com/stretchr/testify v1.7.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/google/uuid v1.3.0 // indirect 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.16 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/sys v0.3.0 // indirect 20 | golang.org/x/text v0.5.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= 7 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 8 | github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= 9 | github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= 10 | github.com/m-mizutani/goerr v0.1.7 h1:T0k3nUVQPBXkLrhE+ZmzJP87KVa9Eb6PAWPVSO6bRYU= 11 | github.com/m-mizutani/goerr v0.1.7/go.mod h1:fQkXuu06q+oLlp4FkbiTFzI/N/+WAK/Mz1W5kPZ6yzs= 12 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 13 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 14 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 15 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 25 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 27 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 32 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /hook.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | type ErrorHook func(error, *Log) 4 | 5 | type LogHook func(*Log) 6 | -------------------------------------------------------------------------------- /hook_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/m-mizutani/zlog" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type testEmitter struct { 11 | emit func(log *zlog.Log) error 12 | } 13 | 14 | func (x *testEmitter) Emit(log *zlog.Log) error { 15 | return x.emit(log) 16 | } 17 | 18 | func TestLogHook(t *testing.T) { 19 | var calledPre, calledEmit, calledPost int 20 | e := &testEmitter{} 21 | 22 | logger := zlog.New(zlog.WithEmitter(e), 23 | zlog.WithPreHook(func(l *zlog.Log) { 24 | assert.Equal(t, 0, calledPre) 25 | assert.Equal(t, 0, calledEmit) 26 | assert.Equal(t, 0, calledPost) 27 | calledPre++ 28 | }), 29 | zlog.WithPostHook(func(l *zlog.Log) { 30 | assert.Equal(t, 1, calledPre) 31 | assert.Equal(t, 1, calledEmit) 32 | assert.Equal(t, 0, calledPost) 33 | calledPost++ 34 | }), 35 | ) 36 | 37 | e.emit = func(log *zlog.Log) error { 38 | calledEmit++ 39 | assert.Equal(t, 1, calledPre) 40 | assert.Equal(t, 1, calledEmit) 41 | assert.Equal(t, 0, calledPost) 42 | return nil 43 | } 44 | logger.Info("test") 45 | assert.Equal(t, 1, calledPre) 46 | assert.Equal(t, 1, calledEmit) 47 | assert.Equal(t, 1, calledPost) 48 | } 49 | -------------------------------------------------------------------------------- /log_level.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/m-mizutani/goerr" 7 | ) 8 | 9 | type LogLevel int 10 | 11 | const ( 12 | LevelTrace LogLevel = iota 13 | LevelDebug 14 | LevelInfo 15 | LevelWarn 16 | LevelError 17 | LevelFatal 18 | ) 19 | 20 | var strToLevelMap = map[string]LogLevel{ 21 | "trace": LevelTrace, 22 | "debug": LevelDebug, 23 | "info": LevelInfo, 24 | "warn": LevelWarn, 25 | "error": LevelError, 26 | "fatal": LevelFatal, 27 | } 28 | 29 | func (x LogLevel) String() string { 30 | s, ok := levelToStrMap[x] 31 | if !ok { 32 | panic("invalid log level variable") 33 | } 34 | return s 35 | } 36 | 37 | var levelToStrMap = map[LogLevel]string{} 38 | 39 | func init() { 40 | for k, v := range strToLevelMap { 41 | levelToStrMap[v] = k 42 | } 43 | } 44 | 45 | func LookupLogLevel(s string) (LogLevel, error) { 46 | level, ok := strToLevelMap[strings.ToLower(s)] 47 | if !ok { 48 | return LevelError, goerr.Wrap(ErrInvalidLogLevel).With("level", s) 49 | } 50 | return level, nil 51 | } 52 | -------------------------------------------------------------------------------- /log_level_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import "github.com/m-mizutani/zlog" 4 | 5 | func ExampleLogLevel() { 6 | logger := newExampleLogger(zlog.WithLogLevel("info")) 7 | 8 | logger.Debug("debugging") 9 | logger.Info("information") 10 | // Output: [info] information 11 | } 12 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | type loggerBase struct { 10 | level LogLevel 11 | emitter Emitter 12 | filters Filters 13 | errHooks []ErrorHook 14 | preHooks []LogHook 15 | postHooks []LogHook 16 | now func() time.Time 17 | async *async 18 | } 19 | 20 | type Logger struct { 21 | loggerBase 22 | 23 | err error 24 | values map[string]any 25 | } 26 | 27 | // New provides default setting zlog logger. Info level, console formatter and stdout. 28 | func New(options ...Option) *Logger { 29 | logger, err := NewWithError(options...) 30 | if err != nil { 31 | panic(fmt.Sprintf("failed to initialize zlog.Logger: %+v", err)) 32 | } 33 | return logger 34 | } 35 | 36 | func NewWithError(options ...Option) (*Logger, error) { 37 | logger := &Logger{ 38 | loggerBase: loggerBase{ 39 | level: LevelInfo, 40 | emitter: NewConsoleEmitter(), 41 | now: time.Now, 42 | }, 43 | values: make(map[string]any), 44 | } 45 | 46 | for _, opt := range options { 47 | opt(logger) 48 | } 49 | 50 | return logger, nil 51 | } 52 | 53 | func (x *Logger) Clone(options ...Option) *Logger { 54 | newLogger := x.reflect() 55 | newLogger.filters = x.filters[:] 56 | newLogger.preHooks = x.preHooks[:] 57 | newLogger.postHooks = x.postHooks[:] 58 | newLogger.errHooks = x.errHooks[:] 59 | 60 | for _, opt := range options { 61 | opt(newLogger) 62 | } 63 | return newLogger 64 | } 65 | 66 | func (x *Logger) reflect() *Logger { 67 | newLogger := &Logger{ 68 | loggerBase: x.loggerBase, 69 | values: make(map[string]any), 70 | err: x.err, 71 | } 72 | for k, v := range x.values { 73 | newLogger.values[k] = v 74 | } 75 | 76 | return newLogger 77 | } 78 | 79 | func (x *Logger) With(key string, value interface{}) *Logger { 80 | // With sets key-value pair for the log. A previous value is overwritten by same key. 81 | e := x.reflect() 82 | 83 | if len(x.filters) > 0 && value != nil { 84 | e.values[key] = newMasking(x.filters).clone(key, reflect.ValueOf(value), "").Interface() 85 | } else { 86 | e.values[key] = value 87 | } 88 | 89 | return e 90 | } 91 | 92 | func (x *Logger) Err(err error) *Logger { 93 | e := x.reflect() 94 | e.err = err 95 | return e 96 | } 97 | 98 | func (x *Logger) msg(level LogLevel, format string, args ...interface{}) { 99 | if level < x.level { 100 | return // skip 101 | } 102 | 103 | log := &Log{ 104 | Level: level, 105 | Msg: fmt.Sprintf(format, args...), 106 | Timestamp: x.now(), 107 | Values: x.values, 108 | Error: newError(x.err, newMasking(x.filters)), 109 | } 110 | 111 | if x.async == nil { 112 | x.emit(log) 113 | } else { 114 | x.async.emit(x, log) 115 | } 116 | } 117 | 118 | func (x *Logger) emit(log *Log) { 119 | for _, hook := range x.preHooks { 120 | hook(log) 121 | } 122 | if err := x.emitter.Emit(log); err != nil { 123 | for _, hook := range x.errHooks { 124 | hook(err, log) 125 | } 126 | } 127 | for _, hook := range x.postHooks { 128 | hook(log) 129 | } 130 | } 131 | 132 | func (x *Logger) Trace(format string, args ...interface{}) { 133 | x.msg(LevelTrace, format, args...) 134 | } 135 | func (x *Logger) Debug(format string, args ...interface{}) { 136 | x.msg(LevelDebug, format, args...) 137 | } 138 | func (x *Logger) Info(format string, args ...interface{}) { 139 | x.msg(LevelInfo, format, args...) 140 | } 141 | func (x *Logger) Warn(format string, args ...interface{}) { 142 | x.msg(LevelWarn, format, args...) 143 | } 144 | func (x *Logger) Error(format string, args ...interface{}) { 145 | x.msg(LevelError, format, args...) 146 | } 147 | func (x *Logger) Fatal(format string, args ...interface{}) { 148 | x.msg(LevelFatal, format, args...) 149 | } 150 | 151 | func (x *Logger) Flush() { 152 | if x.async == nil { 153 | return 154 | } 155 | 156 | x.async.flush() 157 | } 158 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | "time" 7 | 8 | "github.com/m-mizutani/zlog" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func newTestLogger(options ...zlog.Option) (*zlog.Logger, *bytes.Buffer) { 13 | buf := &bytes.Buffer{} 14 | emitter := zlog.NewConsoleEmitter( 15 | zlog.ConsoleNoColor(), 16 | zlog.ConsoleWriter(buf), 17 | ) 18 | 19 | logger := zlog.New(append(options, zlog.WithEmitter(emitter))...) 20 | 21 | return logger, buf 22 | } 23 | 24 | func TestLogger(t *testing.T) { 25 | t.Run("outout message with values", func(t *testing.T) { 26 | logger, buf := newTestLogger(zlog.WithClock(func() time.Time { 27 | return time.Date(2021, 4, 20, 5, 12, 19, 0, time.Local) 28 | })) 29 | logger.With("magic", "five").Info("hello %s", "friends") 30 | 31 | msg := buf.String() 32 | assert.NotContains(t, msg, "2021") 33 | assert.Contains(t, msg, "05:12:19.000") 34 | assert.Contains(t, msg, "hello friends") 35 | assert.Contains(t, msg, "magic") 36 | assert.Contains(t, msg, "five") 37 | }) 38 | 39 | t.Run("outout message if level is equal or higher than logger level", func(t *testing.T) { 40 | logger, buf := newTestLogger(zlog.WithLogLevel("warn")) 41 | 42 | logger.Trace("one") 43 | logger.Debug("two") 44 | logger.Info("three") 45 | logger.Warn("four") 46 | logger.Error("five") 47 | 48 | msg := buf.String() 49 | assert.NotContains(t, msg, "one") 50 | assert.NotContains(t, msg, "two") 51 | assert.NotContains(t, msg, "three") 52 | assert.Contains(t, msg, "four") 53 | assert.Contains(t, msg, "five") 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /masking.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type masking struct { 8 | filters Filters 9 | } 10 | 11 | func newMasking(filters Filters) *masking { 12 | return &masking{ 13 | filters: filters, 14 | } 15 | } 16 | 17 | func (x *masking) clone(fieldName string, value reflect.Value, tag string) reflect.Value { 18 | adjustValue := func(ret reflect.Value) reflect.Value { 19 | switch value.Kind() { 20 | case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Array: 21 | return ret 22 | default: 23 | return ret.Elem() 24 | } 25 | } 26 | 27 | src := value 28 | if value.Kind() == reflect.Ptr { 29 | if value.IsNil() { 30 | return reflect.New(value.Type()).Elem() 31 | } 32 | src = value.Elem() 33 | } 34 | 35 | var dst reflect.Value 36 | if x.filters.ShouldMask(fieldName, src.Interface(), tag) { 37 | dst = reflect.New(src.Type()) 38 | switch src.Kind() { 39 | case reflect.String: 40 | dst.Elem().SetString(FilteredLabel) 41 | case reflect.Array, reflect.Slice: 42 | dst = dst.Elem() 43 | } 44 | return adjustValue(dst) 45 | } 46 | 47 | switch src.Kind() { 48 | case reflect.String: 49 | dst = reflect.New(src.Type()) 50 | filtered := x.filters.ReplaceString(value.String()) 51 | dst.Elem().SetString(filtered) 52 | 53 | case reflect.Struct: 54 | dst = reflect.New(src.Type()) 55 | t := src.Type() 56 | 57 | for i := 0; i < t.NumField(); i++ { 58 | f := t.Field(i) 59 | fv := src.Field(i) 60 | if !fv.CanInterface() { 61 | continue 62 | } 63 | 64 | dst.Elem().Field(i).Set(x.clone(f.Name, fv, f.Tag.Get("zlog"))) 65 | } 66 | 67 | case reflect.Map: 68 | dst = reflect.MakeMap(src.Type()) 69 | keys := src.MapKeys() 70 | for i := 0; i < src.Len(); i++ { 71 | mValue := src.MapIndex(keys[i]) 72 | dst.SetMapIndex(keys[i], x.clone(keys[i].String(), mValue, "")) 73 | } 74 | 75 | case reflect.Array, reflect.Slice: 76 | dst = reflect.MakeSlice(src.Type(), src.Len(), src.Cap()) 77 | for i := 0; i < src.Len(); i++ { 78 | dst.Index(i).Set(x.clone(fieldName, src.Index(i), "")) 79 | } 80 | 81 | default: 82 | dst = reflect.New(src.Type()) 83 | dst.Elem().Set(src) 84 | } 85 | 86 | return adjustValue(dst) 87 | } 88 | -------------------------------------------------------------------------------- /masking_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/m-mizutani/zlog" 8 | "github.com/m-mizutani/zlog/filter" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type allFieldFilter struct{} 14 | 15 | func (x *allFieldFilter) ReplaceString(s string) string { 16 | return s 17 | } 18 | 19 | func (x *allFieldFilter) ShouldMask(fieldName string, value interface{}, tag string) bool { 20 | return fieldName != "" 21 | } 22 | 23 | func TestClone(t *testing.T) { 24 | c := zlog.NewMasking(zlog.Filters{ 25 | filter.Value("blue"), 26 | }) 27 | 28 | t.Run("string", func(t *testing.T) { 29 | v := c.Clone("blue is blue") 30 | v, ok := v.(string) 31 | require.True(t, ok) 32 | assert.Equal(t, zlog.FilteredLabel+" is "+zlog.FilteredLabel, v) 33 | }) 34 | 35 | t.Run("struct", func(t *testing.T) { 36 | type testData struct { 37 | ID int 38 | Name string 39 | Label string 40 | } 41 | 42 | t.Run("original data is not modified when filtered", func(t *testing.T) { 43 | data := &testData{ 44 | ID: 100, 45 | Name: "blue", 46 | Label: "five", 47 | } 48 | v := c.Clone(data) 49 | require.NotNil(t, v) 50 | copied, ok := v.(*testData) 51 | require.True(t, ok) 52 | require.NotNil(t, copied) 53 | assert.Equal(t, zlog.FilteredLabel, copied.Name) 54 | assert.Equal(t, "blue", data.Name) 55 | assert.Equal(t, "five", data.Label) 56 | assert.Equal(t, "five", copied.Label) 57 | assert.Equal(t, 100, copied.ID) 58 | }) 59 | 60 | t.Run("non-ptr struct can be modified", func(t *testing.T) { 61 | data := testData{ 62 | Name: "blue", 63 | Label: "five", 64 | } 65 | v := c.Clone(data) 66 | require.NotNil(t, v) 67 | copied, ok := v.(testData) 68 | require.True(t, ok) 69 | require.NotNil(t, copied) 70 | assert.Equal(t, zlog.FilteredLabel, copied.Name) 71 | assert.Equal(t, "five", copied.Label) 72 | }) 73 | 74 | t.Run("nested structure can be modified", func(t *testing.T) { 75 | type testDataParent struct { 76 | Child testData 77 | } 78 | 79 | data := &testDataParent{ 80 | Child: testData{ 81 | Name: "blue", 82 | Label: "five", 83 | }, 84 | } 85 | v := c.Clone(data) 86 | require.NotNil(t, v) 87 | copied, ok := v.(*testDataParent) 88 | require.True(t, ok) 89 | require.NotNil(t, copied) 90 | assert.Equal(t, zlog.FilteredLabel, copied.Child.Name) 91 | assert.Equal(t, "five", copied.Child.Label) 92 | }) 93 | 94 | t.Run("map data", func(t *testing.T) { 95 | data := map[string]*testData{ 96 | "xyz": { 97 | Name: "blue", 98 | Label: "five", 99 | }, 100 | } 101 | v := c.Clone(data) 102 | require.NotNil(t, v) 103 | copied, ok := v.(map[string]*testData) 104 | require.True(t, ok) 105 | require.NotNil(t, copied) 106 | assert.Equal(t, zlog.FilteredLabel, copied["xyz"].Name) 107 | assert.Equal(t, "five", copied["xyz"].Label) 108 | }) 109 | 110 | t.Run("array data", func(t *testing.T) { 111 | data := []testData{ 112 | { 113 | Name: "orange", 114 | Label: "five", 115 | }, 116 | { 117 | Name: "blue", 118 | Label: "five", 119 | }, 120 | } 121 | v := c.Clone(data) 122 | require.NotNil(t, v) 123 | copied, ok := v.([]testData) 124 | require.True(t, ok) 125 | require.NotNil(t, copied) 126 | assert.Equal(t, "orange", copied[0].Name) 127 | assert.Equal(t, zlog.FilteredLabel, copied[1].Name) 128 | assert.Equal(t, "five", copied[1].Label) 129 | }) 130 | 131 | t.Run("array data with ptr", func(t *testing.T) { 132 | data := []*testData{ 133 | { 134 | Name: "orange", 135 | Label: "five", 136 | }, 137 | { 138 | Name: "blue", 139 | Label: "five", 140 | }, 141 | } 142 | v := c.Clone(data) 143 | require.NotNil(t, v) 144 | copied, ok := v.([]*testData) 145 | require.True(t, ok) 146 | require.NotNil(t, copied) 147 | assert.Equal(t, "orange", copied[0].Name) 148 | assert.Equal(t, zlog.FilteredLabel, copied[1].Name) 149 | assert.Equal(t, "five", copied[1].Label) 150 | }) 151 | 152 | t.Run("original type", func(t *testing.T) { 153 | type myType string 154 | type myData struct { 155 | Name myType 156 | } 157 | data := &myData{ 158 | Name: "miss blue", 159 | } 160 | v := c.Clone(data) 161 | require.NotNil(t, v) 162 | copied, ok := v.(*myData) 163 | require.True(t, ok) 164 | require.NotNil(t, copied) 165 | assert.Equal(t, myType("miss "+zlog.FilteredLabel), copied.Name) 166 | }) 167 | 168 | t.Run("unexported field is also copied", func(t *testing.T) { 169 | type myStruct struct { 170 | unexported string 171 | Exported string 172 | } 173 | 174 | data := &myStruct{ 175 | unexported: "red", 176 | Exported: "orange", 177 | } 178 | v := c.Clone(data) 179 | require.NotNil(t, v) 180 | copied, ok := v.(*myStruct) 181 | require.True(t, ok) 182 | require.NotNil(t, copied) 183 | assert.Equal(t, "red", data.unexported) 184 | assert.Equal(t, "orange", data.Exported) 185 | }) 186 | 187 | t.Run("various field", func(t *testing.T) { 188 | type child struct{} 189 | type myStruct struct { 190 | Func func() time.Time 191 | Chan chan int 192 | Bool bool 193 | Bytes []byte 194 | Interface interface{} 195 | Child *child 196 | } 197 | data := &myStruct{ 198 | Func: time.Now, 199 | Chan: make(chan int), 200 | Bool: true, 201 | Bytes: []byte("timeless"), 202 | Child: nil, 203 | } 204 | v := c.Clone(data) 205 | require.NotNil(t, v) 206 | copied, ok := v.(*myStruct) 207 | require.True(t, ok) 208 | require.NotNil(t, copied) 209 | 210 | // function type is not compareable, but it's ok if not nil 211 | assert.NotNil(t, copied.Func) 212 | assert.Equal(t, data.Chan, copied.Chan) 213 | assert.Equal(t, data.Bool, copied.Bool) 214 | assert.Equal(t, data.Bytes, copied.Bytes) 215 | }) 216 | }) 217 | 218 | t.Run("filter various type", func(t *testing.T) { 219 | mask := zlog.NewMasking(zlog.Filters{ 220 | &allFieldFilter{}, 221 | }) 222 | s := "test" 223 | 224 | type child struct { 225 | Data string 226 | } 227 | type myStruct struct { 228 | Func func() time.Time 229 | Chan chan int 230 | Bool bool 231 | Bytes []byte 232 | Strs []string 233 | StrsPtr []*string 234 | Interface interface{} 235 | Child child 236 | ChildPtr *child 237 | } 238 | data := &myStruct{ 239 | Func: time.Now, 240 | Chan: make(chan int), 241 | Bool: true, 242 | Bytes: []byte("timeless"), 243 | Strs: []string{"aa"}, 244 | StrsPtr: []*string{&s}, 245 | Interface: &s, 246 | Child: child{Data: "x"}, 247 | ChildPtr: &child{Data: "y"}, 248 | } 249 | 250 | v := mask.Clone(data) 251 | require.NotNil(t, v) 252 | copied, ok := v.(*myStruct) 253 | require.True(t, ok) 254 | require.NotNil(t, copied) 255 | assert.Nil(t, copied.Func) 256 | assert.Nil(t, copied.Chan) 257 | assert.Nil(t, copied.Bytes) 258 | assert.Nil(t, copied.Strs) 259 | assert.Nil(t, copied.StrsPtr) 260 | assert.Nil(t, copied.Interface) 261 | assert.Empty(t, copied.Child.Data) 262 | assert.Empty(t, copied.ChildPtr.Data) 263 | }) 264 | } 265 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import "time" 4 | 5 | type Option func(logger *Logger) 6 | 7 | // WithFilters appends filters 8 | func WithFilters(filters ...Filter) Option { 9 | return func(logger *Logger) { 10 | logger.filters = append(logger.filters, filters...) 11 | } 12 | } 13 | 14 | // WithLogLevel sets logging level to one of "trace", "debug", "info", "warn", "error" and "fatal". Argument *level* is not case sensitive. 15 | func WithLogLevel(level string) Option { 16 | return func(logger *Logger) { 17 | l, err := LookupLogLevel(level) 18 | if err != nil { 19 | panic("failed to set log level: " + err.Error()) 20 | } 21 | 22 | logger.level = l 23 | } 24 | } 25 | 26 | // WithLogLevelValue sets directly zlog.LevelTrace, zlog.LevelDebug and so on. 27 | func WithLogLevelValue(level LogLevel) Option { 28 | return func(logger *Logger) { 29 | logger.level = level 30 | } 31 | } 32 | 33 | // WithEmitter replaces emitter in the logger 34 | func WithEmitter(emitter Emitter) Option { 35 | return func(logger *Logger) { 36 | logger.emitter = emitter 37 | } 38 | } 39 | 40 | // WithClock replaces time.Now function in the logger 41 | func WithClock(clock func() time.Time) Option { 42 | return func(logger *Logger) { 43 | logger.now = clock 44 | } 45 | } 46 | 47 | // WithErrHook sets hook that is called when emitter has error 48 | func WithErrHook(hook func(error, *Log)) Option { 49 | return func(logger *Logger) { 50 | logger.errHooks = append(logger.errHooks, hook) 51 | } 52 | } 53 | 54 | // WithPreHook sets hook that is called before emitting log 55 | func WithPreHook(hook func(*Log)) Option { 56 | return func(logger *Logger) { 57 | logger.preHooks = append(logger.preHooks, hook) 58 | } 59 | } 60 | 61 | // WithPostHook sets hook that is called after emitting log 62 | func WithPostHook(hook func(*Log)) Option { 63 | return func(logger *Logger) { 64 | logger.postHooks = append(logger.postHooks, hook) 65 | } 66 | } 67 | 68 | func WithAsync(queueSize int) Option { 69 | return func(logger *Logger) { 70 | logger.async = newAsync(queueSize) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package zlog 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | ) 7 | 8 | type Log struct { 9 | Level LogLevel 10 | Timestamp time.Time 11 | Msg string 12 | Values map[string]any 13 | Error *Error 14 | } 15 | 16 | func (x *Log) OrderedKeys() []string { 17 | keys := make([]string, 0, len(x.Values)) 18 | for k := range x.Values { 19 | keys = append(keys, k) 20 | } 21 | sort.Slice(keys, func(i, j int) bool { 22 | return keys[i] < keys[j] 23 | }) 24 | 25 | return keys 26 | } 27 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package zlog_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/m-mizutani/zlog" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLog_OrderedKeys(t *testing.T) { 11 | log := zlog.Log{ 12 | Values: make(map[string]any), 13 | } 14 | log.Values["a"] = 1 15 | log.Values["b"] = 1 16 | log.Values["c"] = 1 17 | log.Values["d"] = 1 18 | log.Values["e"] = 1 19 | 20 | ordered := log.OrderedKeys() 21 | assert.Equal(t, "a", ordered[0]) 22 | assert.Equal(t, "b", ordered[1]) 23 | assert.Equal(t, "c", ordered[2]) 24 | assert.Equal(t, "d", ordered[3]) 25 | assert.Equal(t, "e", ordered[4]) 26 | } 27 | --------------------------------------------------------------------------------