README.md
1# Zero Allocation JSON Logger
2
3[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/zerolog) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [![Build Status](https://travis-ci.org/rs/zerolog.svg?branch=master)](https://travis-ci.org/rs/zerolog) [![Coverage](http://gocover.io/_badge/github.com/rs/zerolog)](http://gocover.io/github.com/rs/zerolog)
4
5The zerolog package provides a fast and simple logger dedicated to JSON output.
6
7Zerolog's API is designed to provide both a great developer experience and stunning [performance](#benchmarks). Its unique chaining API allows zerolog to write JSON (or CBOR) log events by avoiding allocations and reflection.
8
9Uber's [zap](https://godoc.org/go.uber.org/zap) library pioneered this approach. Zerolog is taking this concept to the next level with a simpler to use API and even better performance.
10
11To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) [`zerolog.ConsoleWriter`](#pretty-logging).
12
13![Pretty Logging Image](pretty.png)
14
15## Who uses zerolog
16
17Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog) and add your company / project to the list.
18
19## Features
20
21* [Blazing fast](#benchmarks)
22* [Low to zero allocation](#benchmarks)
23* [Leveled logging](#leveled-logging)
24* [Sampling](#log-sampling)
25* [Hooks](#hooks)
26* [Contextual fields](#contextual-logging)
27* `context.Context` integration
28* [Integration with `net/http`](#integration-with-nethttp)
29* [JSON and CBOR encoding formats](#binary-encoding)
30* [Pretty logging for development](#pretty-logging)
31* [Error Logging (with optional Stacktrace)](#error-logging)
32
33## Installation
34
35```bash
36go get -u github.com/rs/zerolog/log
37```
38
39## Getting Started
40
41### Simple Logging Example
42
43For simple logging, import the global logger package **github.com/rs/zerolog/log**
44
45```go
46package main
47
48import (
49 "github.com/rs/zerolog"
50 "github.com/rs/zerolog/log"
51)
52
53func main() {
54 // UNIX Time is faster and smaller than most timestamps
55 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
56
57 log.Print("hello world")
58}
59
60// Output: {"time":1516134303,"level":"debug","message":"hello world"}
61```
62> Note: By default log writes to `os.Stderr`
63> Note: The default log level for `log.Print` is *debug*
64
65### Contextual Logging
66
67**zerolog** allows data to be added to log messages in the form of key:value pairs. The data added to the message adds "context" about the log event that can be critical for debugging as well as myriad other purposes. An example of this is below:
68
69```go
70package main
71
72import (
73 "github.com/rs/zerolog"
74 "github.com/rs/zerolog/log"
75)
76
77func main() {
78 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
79
80 log.Debug().
81 Str("Scale", "833 cents").
82 Float64("Interval", 833.09).
83 Msg("Fibonacci is everywhere")
84
85 log.Debug().
86 Str("Name", "Tom").
87 Send()
88}
89
90// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
91// Output: {"level":"debug","Name":"Tom","time":1562212768}
92```
93
94> You'll note in the above example that when adding contextual fields, the fields are strongly typed. You can find the full list of supported fields [here](#standard-types)
95
96### Leveled Logging
97
98#### Simple Leveled Logging Example
99
100```go
101package main
102
103import (
104 "github.com/rs/zerolog"
105 "github.com/rs/zerolog/log"
106)
107
108func main() {
109 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
110
111 log.Info().Msg("hello world")
112}
113
114// Output: {"time":1516134303,"level":"info","message":"hello world"}
115```
116
117> It is very important to note that when using the **zerolog** chaining API, as shown above (`log.Info().Msg("hello world"`), the chain must have either the `Msg` or `Msgf` method call. If you forget to add either of these, the log will not occur and there is no compile time error to alert you of this.
118
119**zerolog** allows for logging at the following levels (from highest to lowest):
120
121* panic (`zerolog.PanicLevel`, 5)
122* fatal (`zerolog.FatalLevel`, 4)
123* error (`zerolog.ErrorLevel`, 3)
124* warn (`zerolog.WarnLevel`, 2)
125* info (`zerolog.InfoLevel`, 1)
126* debug (`zerolog.DebugLevel`, 0)
127* trace (`zerolog.TraceLevel`, -1)
128
129You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
130
131#### Setting Global Log Level
132
133This example uses command-line flags to demonstrate various outputs depending on the chosen log level.
134
135```go
136package main
137
138import (
139 "flag"
140
141 "github.com/rs/zerolog"
142 "github.com/rs/zerolog/log"
143)
144
145func main() {
146 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
147 debug := flag.Bool("debug", false, "sets log level to debug")
148
149 flag.Parse()
150
151 // Default level for this example is info, unless debug flag is present
152 zerolog.SetGlobalLevel(zerolog.InfoLevel)
153 if *debug {
154 zerolog.SetGlobalLevel(zerolog.DebugLevel)
155 }
156
157 log.Debug().Msg("This message appears only when log level set to Debug")
158 log.Info().Msg("This message appears when log level set to Debug or Info")
159
160 if e := log.Debug(); e.Enabled() {
161 // Compute log output only if enabled.
162 value := "bar"
163 e.Str("foo", value).Msg("some debug message")
164 }
165}
166```
167
168Info Output (no flag)
169
170```bash
171$ ./logLevelExample
172{"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"}
173```
174
175Debug Output (debug flag set)
176
177```bash
178$ ./logLevelExample -debug
179{"time":1516387573,"level":"debug","message":"This message appears only when log level set to Debug"}
180{"time":1516387573,"level":"info","message":"This message appears when log level set to Debug or Info"}
181{"time":1516387573,"level":"debug","foo":"bar","message":"some debug message"}
182```
183
184#### Logging without Level or Message
185
186You may choose to log without a specific level by using the `Log` method. You may also write without a message by setting an empty string in the `msg string` parameter of the `Msg` method. Both are demonstrated in the example below.
187
188```go
189package main
190
191import (
192 "github.com/rs/zerolog"
193 "github.com/rs/zerolog/log"
194)
195
196func main() {
197 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
198
199 log.Log().
200 Str("foo", "bar").
201 Msg("")
202}
203
204// Output: {"time":1494567715,"foo":"bar"}
205```
206
207### Error Logging
208
209You can log errors using the `Err` method
210
211```go
212package main
213
214import (
215 "errors"
216
217 "github.com/rs/zerolog"
218 "github.com/rs/zerolog/log"
219)
220
221func main() {
222 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
223
224 err := errors.New("seems we have an error here")
225 log.Error().Err(err).Msg("")
226}
227
228// Output: {"level":"error","error":"seems we have an error here","time":1609085256}
229```
230
231> The default field name for errors is `error`, you can change this by setting `zerolog.ErrorFieldName` to meet your needs.
232
233#### Error Logging with Stacktrace
234
235Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors.
236
237```go
238package main
239
240import (
241 "github.com/pkg/errors"
242 "github.com/rs/zerolog/pkgerrors"
243
244 "github.com/rs/zerolog"
245 "github.com/rs/zerolog/log"
246)
247
248func main() {
249 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
250 zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
251
252 err := outer()
253 log.Error().Stack().Err(err).Msg("")
254}
255
256func inner() error {
257 return errors.New("seems we have an error here")
258}
259
260func middle() error {
261 err := inner()
262 if err != nil {
263 return err
264 }
265 return nil
266}
267
268func outer() error {
269 err := middle()
270 if err != nil {
271 return err
272 }
273 return nil
274}
275
276// Output: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}
277```
278
279> zerolog.ErrorStackMarshaler must be set in order for the stack to output anything.
280
281#### Logging Fatal Messages
282
283```go
284package main
285
286import (
287 "errors"
288
289 "github.com/rs/zerolog"
290 "github.com/rs/zerolog/log"
291)
292
293func main() {
294 err := errors.New("A repo man spends his life getting into tense situations")
295 service := "myservice"
296
297 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
298
299 log.Fatal().
300 Err(err).
301 Str("service", service).
302 Msgf("Cannot start %s", service)
303}
304
305// Output: {"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}
306// exit status 1
307```
308
309> NOTE: Using `Msgf` generates one allocation even when the logger is disabled.
310
311
312### Create logger instance to manage different outputs
313
314```go
315logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
316
317logger.Info().Str("foo", "bar").Msg("hello world")
318
319// Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"}
320```
321
322### Sub-loggers let you chain loggers with additional context
323
324```go
325sublogger := log.With().
326 Str("component", "foo").
327 Logger()
328sublogger.Info().Msg("hello world")
329
330// Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"}
331```
332
333### Pretty logging
334
335To log a human-friendly, colorized output, use `zerolog.ConsoleWriter`:
336
337```go
338log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
339
340log.Info().Str("foo", "bar").Msg("Hello world")
341
342// Output: 3:04PM INF Hello World foo=bar
343```
344
345To customize the configuration and formatting:
346
347```go
348output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
349output.FormatLevel = func(i interface{}) string {
350 return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
351}
352output.FormatMessage = func(i interface{}) string {
353 return fmt.Sprintf("***%s****", i)
354}
355output.FormatFieldName = func(i interface{}) string {
356 return fmt.Sprintf("%s:", i)
357}
358output.FormatFieldValue = func(i interface{}) string {
359 return strings.ToUpper(fmt.Sprintf("%s", i))
360}
361
362log := zerolog.New(output).With().Timestamp().Logger()
363
364log.Info().Str("foo", "bar").Msg("Hello World")
365
366// Output: 2006-01-02T15:04:05Z07:00 | INFO | ***Hello World**** foo:BAR
367```
368
369### Sub dictionary
370
371```go
372log.Info().
373 Str("foo", "bar").
374 Dict("dict", zerolog.Dict().
375 Str("bar", "baz").
376 Int("n", 1),
377 ).Msg("hello world")
378
379// Output: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}
380```
381
382### Customize automatic field names
383
384```go
385zerolog.TimestampFieldName = "t"
386zerolog.LevelFieldName = "l"
387zerolog.MessageFieldName = "m"
388
389log.Info().Msg("hello world")
390
391// Output: {"l":"info","t":1494567715,"m":"hello world"}
392```
393
394### Add contextual fields to the global logger
395
396```go
397log.Logger = log.With().Str("foo", "bar").Logger()
398```
399
400### Add file and line number to log
401
402```go
403log.Logger = log.With().Caller().Logger()
404log.Info().Msg("hello world")
405
406// Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"}
407```
408
409
410### Thread-safe, lock-free, non-blocking writer
411
412If your writer might be slow or not thread-safe and you need your log producers to never get slowed down by a slow writer, you can use a `diode.Writer` as follow:
413
414```go
415wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
416 fmt.Printf("Logger Dropped %d messages", missed)
417 })
418log := zerolog.New(wr)
419log.Print("test")
420```
421
422You will need to install `code.cloudfoundry.org/go-diodes` to use this feature.
423
424### Log Sampling
425
426```go
427sampled := log.Sample(&zerolog.BasicSampler{N: 10})
428sampled.Info().Msg("will be logged every 10 messages")
429
430// Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"}
431```
432
433More advanced sampling:
434
435```go
436// Will let 5 debug messages per period of 1 second.
437// Over 5 debug message, 1 every 100 debug messages are logged.
438// Other levels are not sampled.
439sampled := log.Sample(zerolog.LevelSampler{
440 DebugSampler: &zerolog.BurstSampler{
441 Burst: 5,
442 Period: 1*time.Second,
443 NextSampler: &zerolog.BasicSampler{N: 100},
444 },
445})
446sampled.Debug().Msg("hello world")
447
448// Output: {"time":1494567715,"level":"debug","message":"hello world"}
449```
450
451### Hooks
452
453```go
454type SeverityHook struct{}
455
456func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
457 if level != zerolog.NoLevel {
458 e.Str("severity", level.String())
459 }
460}
461
462hooked := log.Hook(SeverityHook{})
463hooked.Warn().Msg("")
464
465// Output: {"level":"warn","severity":"warn"}
466```
467
468### Pass a sub-logger by context
469
470```go
471ctx := log.With().Str("component", "module").Logger().WithContext(ctx)
472
473log.Ctx(ctx).Info().Msg("hello world")
474
475// Output: {"component":"module","level":"info","message":"hello world"}
476```
477
478### Set as standard logger output
479
480```go
481log := zerolog.New(os.Stdout).With().
482 Str("foo", "bar").
483 Logger()
484
485stdlog.SetFlags(0)
486stdlog.SetOutput(log)
487
488stdlog.Print("hello world")
489
490// Output: {"foo":"bar","message":"hello world"}
491```
492
493### Integration with `net/http`
494
495The `github.com/rs/zerolog/hlog` package provides some helpers to integrate zerolog with `http.Handler`.
496
497In this example we use [alice](https://github.com/justinas/alice) to install logger for better readability.
498
499```go
500log := zerolog.New(os.Stdout).With().
501 Timestamp().
502 Str("role", "my-service").
503 Str("host", host).
504 Logger()
505
506c := alice.New()
507
508// Install the logger handler with default output on the console
509c = c.Append(hlog.NewHandler(log))
510
511// Install some provided extra handler to set some request's context fields.
512// Thanks to that handler, all our logs will come with some prepopulated fields.
513c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
514 hlog.FromRequest(r).Info().
515 Str("method", r.Method).
516 Stringer("url", r.URL).
517 Int("status", status).
518 Int("size", size).
519 Dur("duration", duration).
520 Msg("")
521}))
522c = c.Append(hlog.RemoteAddrHandler("ip"))
523c = c.Append(hlog.UserAgentHandler("user_agent"))
524c = c.Append(hlog.RefererHandler("referer"))
525c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id"))
526
527// Here is your final handler
528h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
529 // Get the logger from the request's context. You can safely assume it
530 // will be always there: if the handler is removed, hlog.FromRequest
531 // will return a no-op logger.
532 hlog.FromRequest(r).Info().
533 Str("user", "current user").
534 Str("status", "ok").
535 Msg("Something happened")
536
537 // Output: {"level":"info","time":"2001-02-03T04:05:06Z","role":"my-service","host":"local-hostname","req_id":"b4g0l5t6tfid6dtrapu0","user":"current user","status":"ok","message":"Something happened"}
538}))
539http.Handle("/", h)
540
541if err := http.ListenAndServe(":8080", nil); err != nil {
542 log.Fatal().Err(err).Msg("Startup failed")
543}
544```
545
546## Multiple Log Output
547`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs.
548In this example, we send the log message to both `os.Stdout` and the in-built ConsoleWriter.
549```go
550func main() {
551 consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
552
553 multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
554
555 logger := zerolog.New(multi).With().Timestamp().Logger()
556
557 logger.Info().Msg("Hello World!")
558}
559
560// Output (Line 1: Console; Line 2: Stdout)
561// 12:36PM INF Hello World!
562// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}
563```
564
565## Global Settings
566
567Some settings can be changed and will by applied to all loggers:
568
569* `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
570* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode).
571* `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
572* `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
573* `zerolog.LevelFieldName`: Can be set to customize level field name.
574* `zerolog.MessageFieldName`: Can be set to customize message field name.
575* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
576* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formated as UNIX timestamp.
577* `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
578* `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`).
579* `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
580
581## Field Types
582
583### Standard Types
584
585* `Str`
586* `Bool`
587* `Int`, `Int8`, `Int16`, `Int32`, `Int64`
588* `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
589* `Float32`, `Float64`
590
591### Advanced Fields
592
593* `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name.
594* `Func`: Run a `func` only if the level is enabled.
595* `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`.
596* `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`.
597* `Dur`: Adds a field with `time.Duration`.
598* `Dict`: Adds a sub-key/value as a field of the event.
599* `RawJSON`: Adds a field with an already encoded JSON (`[]byte`)
600* `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`)
601* `Interface`: Uses reflection to marshal the type.
602
603Most fields are also available in the slice format (`Strs` for `[]string`, `Errs` for `[]error` etc.)
604
605## Binary Encoding
606
607In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](http://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows:
608
609```bash
610go build -tags binary_log .
611```
612
613To Decode binary encoded log files you can use any CBOR decoder. One has been tested to work
614with zerolog library is [CSD](https://github.com/toravir/csd/).
615
616## Related Projects
617
618* [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
619* [overlog](https://github.com/Trendyol/overlog): Implementation of `Mapped Diagnostic Context` interface using `zerolog`
620* [zerologr](https://github.com/go-logr/zerologr): Implementation of `logr.LogSink` interface using `zerolog`
621
622## Benchmarks
623
624See [logbench](http://hackemist.com/logbench/) for more comprehensive and up-to-date benchmarks.
625
626All operations are allocation free (those numbers *include* JSON encoding):
627
628```text
629BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
630BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op
631BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op
632BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op
633BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
634```
635
636There are a few Go logging benchmarks and comparisons that include zerolog.
637
638* [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
639* [uber-common/zap](https://github.com/uber-go/zap#performance)
640
641Using Uber's zap comparison benchmark:
642
643Log a message and 10 fields:
644
645| Library | Time | Bytes Allocated | Objects Allocated |
646| :--- | :---: | :---: | :---: |
647| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
648| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
649| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
650| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
651| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
652| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
653| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
654| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
655
656Log a message with a logger that already has 10 fields of context:
657
658| Library | Time | Bytes Allocated | Objects Allocated |
659| :--- | :---: | :---: | :---: |
660| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
661| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
662| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
663| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
664| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
665| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
666| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
667| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
668
669Log a static string, without any context or `printf`-style templating:
670
671| Library | Time | Bytes Allocated | Objects Allocated |
672| :--- | :---: | :---: | :---: |
673| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
674| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
675| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
676| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
677| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
678| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
679| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
680| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
681| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
682
683## Caveats
684
685Note that zerolog does no de-duplication of fields. Using the same key multiple times creates multiple keys in final JSON:
686
687```go
688logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
689logger.Info().
690 Timestamp().
691 Msg("dup")
692// Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
693```
694
695In this case, many consumers will take the last value, but this is not guaranteed; check yours if in doubt.
696