├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── array.go ├── array_test.go ├── benchmark_test.go ├── binary_test.go ├── cmd ├── lint │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── lint.go └── prettylog │ ├── README.md │ └── prettylog.go ├── console.go ├── console_test.go ├── context.go ├── ctx.go ├── ctx_test.go ├── diode ├── diode.go ├── diode_example_test.go ├── diode_test.go └── internal │ └── diodes │ ├── README │ ├── many_to_one.go │ ├── one_to_one.go │ ├── poller.go │ └── waiter.go ├── encoder.go ├── encoder_cbor.go ├── encoder_json.go ├── event.go ├── event_test.go ├── example.jsonl ├── fields.go ├── globals.go ├── go.mod ├── go.sum ├── go112.go ├── hlog ├── hlog.go ├── hlog_example_test.go ├── hlog_test.go └── internal │ └── mutil │ ├── LICENSE │ ├── mutil.go │ └── writer_proxy.go ├── hook.go ├── hook_test.go ├── internal ├── cbor │ ├── README.md │ ├── base.go │ ├── cbor.go │ ├── decode_stream.go │ ├── decoder_test.go │ ├── examples │ │ ├── genLog.go │ │ └── makefile │ ├── string.go │ ├── string_test.go │ ├── time.go │ ├── time_test.go │ ├── types.go │ ├── types_64_test.go │ └── types_test.go └── json │ ├── base.go │ ├── bytes.go │ ├── bytes_test.go │ ├── string.go │ ├── string_test.go │ ├── time.go │ ├── types.go │ └── types_test.go ├── journald ├── journald.go └── journald_test.go ├── log.go ├── log ├── log.go └── log_example_test.go ├── log_example_test.go ├── log_test.go ├── not_go112.go ├── pkgerrors ├── stacktrace.go └── stacktrace_test.go ├── pretty.png ├── sampler.go ├── sampler_test.go ├── syslog.go ├── syslog_test.go ├── writer.go └── writer_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.21.x, 1.24.x] 8 | os: [ubuntu-latest, macos-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - uses: actions/cache@v4 18 | with: 19 | path: ~/go/pkg/mod 20 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 21 | restore-keys: | 22 | ${{ runner.os }}-go- 23 | - name: Test 24 | run: go test -race -bench . -benchmem ./... 25 | - name: Test CBOR 26 | run: go test -tags binary_log ./... 27 | coverage: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Update coverage report 31 | uses: ncruces/go-coverage-report@main 32 | with: 33 | report: 'true' 34 | chart: 'true' 35 | amend: 'true' 36 | continue-on-error: true 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | tmp 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Zerolog 2 | 3 | Thank you for your interest in contributing to **Zerolog**! 4 | 5 | Zerolog is a **feature-complete**, high-performance logging library designed to be **lean** and **non-bloated**. The focus of ongoing development is on **bug fixes**, **performance improvements**, and **modernization efforts** (such as keeping up with Go best practices and compatibility with newer Go versions). 6 | 7 | ## What We're Looking For 8 | 9 | We welcome contributions in the following areas: 10 | 11 | - **Bug Fixes**: If you find an issue or unexpected behavior, please open an issue and/or submit a fix. 12 | - **Performance Optimizations**: Improvements that reduce memory usage, allocation count, or CPU cycles without introducing complexity are appreciated. 13 | - **Modernization**: Compatibility updates for newer Go versions or idiomatic improvements that do not increase library size or complexity. 14 | - **Documentation Enhancements**: Corrections, clarifications, and improvements to documentation or code comments. 15 | 16 | ## What We're *Not* Looking For 17 | 18 | Zerolog is intended to remain **minimalistic and efficient**. Therefore, we are **not accepting**: 19 | 20 | - New features that add optional behaviors or extend API surface area. 21 | - Built-in support for frameworks or external systems (e.g., bindings, integrations). 22 | - General-purpose abstractions or configuration helpers. 23 | 24 | If you're unsure whether a change aligns with the project's philosophy, feel free to open an issue for discussion before submitting a PR. 25 | 26 | ## Contributing Guidelines 27 | 28 | 1. **Fork the repository** 29 | 2. **Create a branch** for your fix or improvement 30 | 3. **Write tests** to cover your changes 31 | 4. Ensure `go test ./...` passes 32 | 5. Run `go fmt` and `go vet` to ensure code consistency 33 | 6. **Submit a pull request** with a clear explanation of the motivation and impact 34 | 35 | ## Code Style 36 | 37 | - Keep the code simple, efficient, and idiomatic. 38 | - Avoid introducing new dependencies. 39 | - Preserve backwards compatibility unless explicitly discussed. 40 | 41 | --- 42 | 43 | We appreciate your effort in helping us keep Zerolog fast, minimal, and reliable! 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Olivier Poitrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /array.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var arrayPool = &sync.Pool{ 10 | New: func() interface{} { 11 | return &Array{ 12 | buf: make([]byte, 0, 500), 13 | } 14 | }, 15 | } 16 | 17 | // Array is used to prepopulate an array of items 18 | // which can be re-used to add to log messages. 19 | type Array struct { 20 | buf []byte 21 | } 22 | 23 | func putArray(a *Array) { 24 | // Proper usage of a sync.Pool requires each entry to have approximately 25 | // the same memory cost. To obtain this property when the stored type 26 | // contains a variably-sized buffer, we add a hard limit on the maximum buffer 27 | // to place back in the pool. 28 | // 29 | // See https://golang.org/issue/23199 30 | const maxSize = 1 << 16 // 64KiB 31 | if cap(a.buf) > maxSize { 32 | return 33 | } 34 | arrayPool.Put(a) 35 | } 36 | 37 | // Arr creates an array to be added to an Event or Context. 38 | func Arr() *Array { 39 | a := arrayPool.Get().(*Array) 40 | a.buf = a.buf[:0] 41 | return a 42 | } 43 | 44 | // MarshalZerologArray method here is no-op - since data is 45 | // already in the needed format. 46 | func (*Array) MarshalZerologArray(*Array) { 47 | } 48 | 49 | func (a *Array) write(dst []byte) []byte { 50 | dst = enc.AppendArrayStart(dst) 51 | if len(a.buf) > 0 { 52 | dst = append(dst, a.buf...) 53 | } 54 | dst = enc.AppendArrayEnd(dst) 55 | putArray(a) 56 | return dst 57 | } 58 | 59 | // Object marshals an object that implement the LogObjectMarshaler 60 | // interface and appends it to the array. 61 | func (a *Array) Object(obj LogObjectMarshaler) *Array { 62 | e := Dict() 63 | obj.MarshalZerologObject(e) 64 | e.buf = enc.AppendEndMarker(e.buf) 65 | a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...) 66 | putEvent(e) 67 | return a 68 | } 69 | 70 | // Str appends the val as a string to the array. 71 | func (a *Array) Str(val string) *Array { 72 | a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), val) 73 | return a 74 | } 75 | 76 | // Bytes appends the val as a string to the array. 77 | func (a *Array) Bytes(val []byte) *Array { 78 | a.buf = enc.AppendBytes(enc.AppendArrayDelim(a.buf), val) 79 | return a 80 | } 81 | 82 | // Hex appends the val as a hex string to the array. 83 | func (a *Array) Hex(val []byte) *Array { 84 | a.buf = enc.AppendHex(enc.AppendArrayDelim(a.buf), val) 85 | return a 86 | } 87 | 88 | // RawJSON adds already encoded JSON to the array. 89 | func (a *Array) RawJSON(val []byte) *Array { 90 | a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val) 91 | return a 92 | } 93 | 94 | // Err serializes and appends the err to the array. 95 | func (a *Array) Err(err error) *Array { 96 | switch m := ErrorMarshalFunc(err).(type) { 97 | case LogObjectMarshaler: 98 | e := newEvent(nil, 0) 99 | e.buf = e.buf[:0] 100 | e.appendObject(m) 101 | a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...) 102 | putEvent(e) 103 | case error: 104 | if m == nil || isNilValue(m) { 105 | a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf)) 106 | } else { 107 | a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error()) 108 | } 109 | case string: 110 | a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m) 111 | default: 112 | a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), m) 113 | } 114 | 115 | return a 116 | } 117 | 118 | // Bool appends the val as a bool to the array. 119 | func (a *Array) Bool(b bool) *Array { 120 | a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b) 121 | return a 122 | } 123 | 124 | // Int appends i as a int to the array. 125 | func (a *Array) Int(i int) *Array { 126 | a.buf = enc.AppendInt(enc.AppendArrayDelim(a.buf), i) 127 | return a 128 | } 129 | 130 | // Int8 appends i as a int8 to the array. 131 | func (a *Array) Int8(i int8) *Array { 132 | a.buf = enc.AppendInt8(enc.AppendArrayDelim(a.buf), i) 133 | return a 134 | } 135 | 136 | // Int16 appends i as a int16 to the array. 137 | func (a *Array) Int16(i int16) *Array { 138 | a.buf = enc.AppendInt16(enc.AppendArrayDelim(a.buf), i) 139 | return a 140 | } 141 | 142 | // Int32 appends i as a int32 to the array. 143 | func (a *Array) Int32(i int32) *Array { 144 | a.buf = enc.AppendInt32(enc.AppendArrayDelim(a.buf), i) 145 | return a 146 | } 147 | 148 | // Int64 appends i as a int64 to the array. 149 | func (a *Array) Int64(i int64) *Array { 150 | a.buf = enc.AppendInt64(enc.AppendArrayDelim(a.buf), i) 151 | return a 152 | } 153 | 154 | // Uint appends i as a uint to the array. 155 | func (a *Array) Uint(i uint) *Array { 156 | a.buf = enc.AppendUint(enc.AppendArrayDelim(a.buf), i) 157 | return a 158 | } 159 | 160 | // Uint8 appends i as a uint8 to the array. 161 | func (a *Array) Uint8(i uint8) *Array { 162 | a.buf = enc.AppendUint8(enc.AppendArrayDelim(a.buf), i) 163 | return a 164 | } 165 | 166 | // Uint16 appends i as a uint16 to the array. 167 | func (a *Array) Uint16(i uint16) *Array { 168 | a.buf = enc.AppendUint16(enc.AppendArrayDelim(a.buf), i) 169 | return a 170 | } 171 | 172 | // Uint32 appends i as a uint32 to the array. 173 | func (a *Array) Uint32(i uint32) *Array { 174 | a.buf = enc.AppendUint32(enc.AppendArrayDelim(a.buf), i) 175 | return a 176 | } 177 | 178 | // Uint64 appends i as a uint64 to the array. 179 | func (a *Array) Uint64(i uint64) *Array { 180 | a.buf = enc.AppendUint64(enc.AppendArrayDelim(a.buf), i) 181 | return a 182 | } 183 | 184 | // Float32 appends f as a float32 to the array. 185 | func (a *Array) Float32(f float32) *Array { 186 | a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision) 187 | return a 188 | } 189 | 190 | // Float64 appends f as a float64 to the array. 191 | func (a *Array) Float64(f float64) *Array { 192 | a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision) 193 | return a 194 | } 195 | 196 | // Time appends t formatted as string using zerolog.TimeFieldFormat. 197 | func (a *Array) Time(t time.Time) *Array { 198 | a.buf = enc.AppendTime(enc.AppendArrayDelim(a.buf), t, TimeFieldFormat) 199 | return a 200 | } 201 | 202 | // Dur appends d to the array. 203 | func (a *Array) Dur(d time.Duration) *Array { 204 | a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision) 205 | return a 206 | } 207 | 208 | // Interface appends i marshaled using reflection. 209 | func (a *Array) Interface(i interface{}) *Array { 210 | if obj, ok := i.(LogObjectMarshaler); ok { 211 | return a.Object(obj) 212 | } 213 | a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), i) 214 | return a 215 | } 216 | 217 | // IPAddr adds IPv4 or IPv6 address to the array 218 | func (a *Array) IPAddr(ip net.IP) *Array { 219 | a.buf = enc.AppendIPAddr(enc.AppendArrayDelim(a.buf), ip) 220 | return a 221 | } 222 | 223 | // IPPrefix adds IPv4 or IPv6 Prefix (IP + mask) to the array 224 | func (a *Array) IPPrefix(pfx net.IPNet) *Array { 225 | a.buf = enc.AppendIPPrefix(enc.AppendArrayDelim(a.buf), pfx) 226 | return a 227 | } 228 | 229 | // MACAddr adds a MAC (Ethernet) address to the array 230 | func (a *Array) MACAddr(ha net.HardwareAddr) *Array { 231 | a.buf = enc.AppendMACAddr(enc.AppendArrayDelim(a.buf), ha) 232 | return a 233 | } 234 | 235 | // Dict adds the dict Event to the array 236 | func (a *Array) Dict(dict *Event) *Array { 237 | dict.buf = enc.AppendEndMarker(dict.buf) 238 | a.buf = append(enc.AppendArrayDelim(a.buf), dict.buf...) 239 | return a 240 | } 241 | -------------------------------------------------------------------------------- /array_test.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestArray(t *testing.T) { 10 | a := Arr(). 11 | Bool(true). 12 | Int(1). 13 | Int8(2). 14 | Int16(3). 15 | Int32(4). 16 | Int64(5). 17 | Uint(6). 18 | Uint8(7). 19 | Uint16(8). 20 | Uint32(9). 21 | Uint64(10). 22 | Float32(11.98122). 23 | Float64(12.987654321). 24 | Str("a"). 25 | Bytes([]byte("b")). 26 | Hex([]byte{0x1f}). 27 | RawJSON([]byte(`{"some":"json"}`)). 28 | Time(time.Time{}). 29 | IPAddr(net.IP{192, 168, 0, 10}). 30 | Dur(0). 31 | Dict(Dict(). 32 | Str("bar", "baz"). 33 | Int("n", 1), 34 | ) 35 | want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},"0001-01-01T00:00:00Z","192.168.0.10",0,{"bar":"baz","n":1}]` 36 | if got := decodeObjectToStr(a.write([]byte{})); got != want { 37 | t.Errorf("Array.write()\ngot: %s\nwant: %s", got, want) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var ( 13 | errExample = errors.New("fail") 14 | fakeMessage = "Test logging, but use a somewhat realistic message length." 15 | ) 16 | 17 | func BenchmarkLogEmpty(b *testing.B) { 18 | logger := New(io.Discard) 19 | b.ResetTimer() 20 | b.RunParallel(func(pb *testing.PB) { 21 | for pb.Next() { 22 | logger.Log().Msg("") 23 | } 24 | }) 25 | } 26 | 27 | func BenchmarkDisabled(b *testing.B) { 28 | logger := New(io.Discard).Level(Disabled) 29 | b.ResetTimer() 30 | b.RunParallel(func(pb *testing.PB) { 31 | for pb.Next() { 32 | logger.Info().Msg(fakeMessage) 33 | } 34 | }) 35 | } 36 | 37 | func BenchmarkInfo(b *testing.B) { 38 | logger := New(io.Discard) 39 | b.ResetTimer() 40 | b.RunParallel(func(pb *testing.PB) { 41 | for pb.Next() { 42 | logger.Info().Msg(fakeMessage) 43 | } 44 | }) 45 | } 46 | 47 | func BenchmarkContextFields(b *testing.B) { 48 | logger := New(io.Discard).With(). 49 | Str("string", "four!"). 50 | Time("time", time.Time{}). 51 | Int("int", 123). 52 | Float32("float", -2.203230293249593). 53 | Logger() 54 | b.ResetTimer() 55 | b.RunParallel(func(pb *testing.PB) { 56 | for pb.Next() { 57 | logger.Info().Msg(fakeMessage) 58 | } 59 | }) 60 | } 61 | 62 | func BenchmarkContextAppend(b *testing.B) { 63 | logger := New(io.Discard).With(). 64 | Str("foo", "bar"). 65 | Logger() 66 | b.ResetTimer() 67 | b.RunParallel(func(pb *testing.PB) { 68 | for pb.Next() { 69 | logger.With().Str("bar", "baz") 70 | } 71 | }) 72 | } 73 | 74 | func BenchmarkLogFields(b *testing.B) { 75 | logger := New(io.Discard) 76 | b.ResetTimer() 77 | b.RunParallel(func(pb *testing.PB) { 78 | for pb.Next() { 79 | logger.Info(). 80 | Str("string", "four!"). 81 | Time("time", time.Time{}). 82 | Int("int", 123). 83 | Float32("float", -2.203230293249593). 84 | Msg(fakeMessage) 85 | } 86 | }) 87 | } 88 | 89 | type obj struct { 90 | Pub string 91 | Tag string `json:"tag"` 92 | priv int 93 | } 94 | 95 | func (o obj) MarshalZerologObject(e *Event) { 96 | e.Str("Pub", o.Pub). 97 | Str("Tag", o.Tag). 98 | Int("priv", o.priv) 99 | } 100 | 101 | func BenchmarkLogArrayObject(b *testing.B) { 102 | obj1 := obj{"a", "b", 2} 103 | obj2 := obj{"c", "d", 3} 104 | obj3 := obj{"e", "f", 4} 105 | logger := New(io.Discard) 106 | b.ResetTimer() 107 | b.ReportAllocs() 108 | for i := 0; i < b.N; i++ { 109 | arr := Arr() 110 | arr.Object(&obj1) 111 | arr.Object(&obj2) 112 | arr.Object(&obj3) 113 | logger.Info().Array("objects", arr).Msg("test") 114 | } 115 | } 116 | 117 | func BenchmarkLogFieldType(b *testing.B) { 118 | bools := []bool{true, false, true, false, true, false, true, false, true, false} 119 | ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 120 | floats := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 121 | strings := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} 122 | durations := []time.Duration{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 123 | times := []time.Time{ 124 | time.Unix(0, 0), 125 | time.Unix(1, 0), 126 | time.Unix(2, 0), 127 | time.Unix(3, 0), 128 | time.Unix(4, 0), 129 | time.Unix(5, 0), 130 | time.Unix(6, 0), 131 | time.Unix(7, 0), 132 | time.Unix(8, 0), 133 | time.Unix(9, 0), 134 | } 135 | interfaces := []struct { 136 | Pub string 137 | Tag string `json:"tag"` 138 | priv int 139 | }{ 140 | {"a", "a", 0}, 141 | {"a", "a", 0}, 142 | {"a", "a", 0}, 143 | {"a", "a", 0}, 144 | {"a", "a", 0}, 145 | {"a", "a", 0}, 146 | {"a", "a", 0}, 147 | {"a", "a", 0}, 148 | {"a", "a", 0}, 149 | {"a", "a", 0}, 150 | } 151 | objects := []obj{ 152 | {"a", "a", 0}, 153 | {"a", "a", 0}, 154 | {"a", "a", 0}, 155 | {"a", "a", 0}, 156 | {"a", "a", 0}, 157 | {"a", "a", 0}, 158 | {"a", "a", 0}, 159 | {"a", "a", 0}, 160 | {"a", "a", 0}, 161 | {"a", "a", 0}, 162 | } 163 | errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e")} 164 | ctx := context.Background() 165 | types := map[string]func(e *Event) *Event{ 166 | "Bool": func(e *Event) *Event { 167 | return e.Bool("k", bools[0]) 168 | }, 169 | "Bools": func(e *Event) *Event { 170 | return e.Bools("k", bools) 171 | }, 172 | "Int": func(e *Event) *Event { 173 | return e.Int("k", ints[0]) 174 | }, 175 | "Ints": func(e *Event) *Event { 176 | return e.Ints("k", ints) 177 | }, 178 | "Float": func(e *Event) *Event { 179 | return e.Float64("k", floats[0]) 180 | }, 181 | "Floats": func(e *Event) *Event { 182 | return e.Floats64("k", floats) 183 | }, 184 | "Str": func(e *Event) *Event { 185 | return e.Str("k", strings[0]) 186 | }, 187 | "Strs": func(e *Event) *Event { 188 | return e.Strs("k", strings) 189 | }, 190 | "Err": func(e *Event) *Event { 191 | return e.Err(errs[0]) 192 | }, 193 | "Errs": func(e *Event) *Event { 194 | return e.Errs("k", errs) 195 | }, 196 | "Ctx": func(e *Event) *Event { 197 | return e.Ctx(ctx) 198 | }, 199 | "Time": func(e *Event) *Event { 200 | return e.Time("k", times[0]) 201 | }, 202 | "Times": func(e *Event) *Event { 203 | return e.Times("k", times) 204 | }, 205 | "Dur": func(e *Event) *Event { 206 | return e.Dur("k", durations[0]) 207 | }, 208 | "Durs": func(e *Event) *Event { 209 | return e.Durs("k", durations) 210 | }, 211 | "Interface": func(e *Event) *Event { 212 | return e.Interface("k", interfaces[0]) 213 | }, 214 | "Interfaces": func(e *Event) *Event { 215 | return e.Interface("k", interfaces) 216 | }, 217 | "Interface(Object)": func(e *Event) *Event { 218 | return e.Interface("k", objects[0]) 219 | }, 220 | "Interface(Objects)": func(e *Event) *Event { 221 | return e.Interface("k", objects) 222 | }, 223 | "Object": func(e *Event) *Event { 224 | return e.Object("k", objects[0]) 225 | }, 226 | } 227 | logger := New(io.Discard) 228 | b.ResetTimer() 229 | for name := range types { 230 | f := types[name] 231 | b.Run(name, func(b *testing.B) { 232 | b.RunParallel(func(pb *testing.PB) { 233 | for pb.Next() { 234 | f(logger.Info()).Msg("") 235 | } 236 | }) 237 | }) 238 | } 239 | } 240 | 241 | func BenchmarkContextFieldType(b *testing.B) { 242 | oldFormat := TimeFieldFormat 243 | TimeFieldFormat = TimeFormatUnix 244 | defer func() { TimeFieldFormat = oldFormat }() 245 | bools := []bool{true, false, true, false, true, false, true, false, true, false} 246 | ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 247 | floats := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 248 | strings := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} 249 | stringer := net.IP{127, 0, 0, 1} 250 | durations := []time.Duration{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 251 | times := []time.Time{ 252 | time.Unix(0, 0), 253 | time.Unix(1, 0), 254 | time.Unix(2, 0), 255 | time.Unix(3, 0), 256 | time.Unix(4, 0), 257 | time.Unix(5, 0), 258 | time.Unix(6, 0), 259 | time.Unix(7, 0), 260 | time.Unix(8, 0), 261 | time.Unix(9, 0), 262 | } 263 | interfaces := []struct { 264 | Pub string 265 | Tag string `json:"tag"` 266 | priv int 267 | }{ 268 | {"a", "a", 0}, 269 | {"a", "a", 0}, 270 | {"a", "a", 0}, 271 | {"a", "a", 0}, 272 | {"a", "a", 0}, 273 | {"a", "a", 0}, 274 | {"a", "a", 0}, 275 | {"a", "a", 0}, 276 | {"a", "a", 0}, 277 | {"a", "a", 0}, 278 | } 279 | objects := []obj{ 280 | {"a", "a", 0}, 281 | {"a", "a", 0}, 282 | {"a", "a", 0}, 283 | {"a", "a", 0}, 284 | {"a", "a", 0}, 285 | {"a", "a", 0}, 286 | {"a", "a", 0}, 287 | {"a", "a", 0}, 288 | {"a", "a", 0}, 289 | {"a", "a", 0}, 290 | } 291 | errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e")} 292 | ctx := context.Background() 293 | types := map[string]func(c Context) Context{ 294 | "Bool": func(c Context) Context { 295 | return c.Bool("k", bools[0]) 296 | }, 297 | "Bools": func(c Context) Context { 298 | return c.Bools("k", bools) 299 | }, 300 | "Int": func(c Context) Context { 301 | return c.Int("k", ints[0]) 302 | }, 303 | "Ints": func(c Context) Context { 304 | return c.Ints("k", ints) 305 | }, 306 | "Float": func(c Context) Context { 307 | return c.Float64("k", floats[0]) 308 | }, 309 | "Floats": func(c Context) Context { 310 | return c.Floats64("k", floats) 311 | }, 312 | "Str": func(c Context) Context { 313 | return c.Str("k", strings[0]) 314 | }, 315 | "Strs": func(c Context) Context { 316 | return c.Strs("k", strings) 317 | }, 318 | "Stringer": func(c Context) Context { 319 | return c.Stringer("k", stringer) 320 | }, 321 | "Err": func(c Context) Context { 322 | return c.Err(errs[0]) 323 | }, 324 | "Errs": func(c Context) Context { 325 | return c.Errs("k", errs) 326 | }, 327 | "Ctx": func(c Context) Context { 328 | return c.Ctx(ctx) 329 | }, 330 | "Time": func(c Context) Context { 331 | return c.Time("k", times[0]) 332 | }, 333 | "Times": func(c Context) Context { 334 | return c.Times("k", times) 335 | }, 336 | "Dur": func(c Context) Context { 337 | return c.Dur("k", durations[0]) 338 | }, 339 | "Durs": func(c Context) Context { 340 | return c.Durs("k", durations) 341 | }, 342 | "Interface": func(c Context) Context { 343 | return c.Interface("k", interfaces[0]) 344 | }, 345 | "Interfaces": func(c Context) Context { 346 | return c.Interface("k", interfaces) 347 | }, 348 | "Interface(Object)": func(c Context) Context { 349 | return c.Interface("k", objects[0]) 350 | }, 351 | "Interface(Objects)": func(c Context) Context { 352 | return c.Interface("k", objects) 353 | }, 354 | "Object": func(c Context) Context { 355 | return c.Object("k", objects[0]) 356 | }, 357 | "Timestamp": func(c Context) Context { 358 | return c.Timestamp() 359 | }, 360 | } 361 | logger := New(io.Discard) 362 | b.ResetTimer() 363 | for name := range types { 364 | f := types[name] 365 | b.Run(name, func(b *testing.B) { 366 | b.RunParallel(func(pb *testing.PB) { 367 | for pb.Next() { 368 | l := f(logger.With()).Logger() 369 | l.Info().Msg("") 370 | } 371 | }) 372 | }) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /cmd/lint/README.md: -------------------------------------------------------------------------------- 1 | # Zerolog Lint 2 | 3 | **DEPRECATED: In favor of https://github.com/ykadowak/zerologlint which is integrated with `go vet` and [golangci-lint](https://golangci-lint.run/).** 4 | 5 | This is a basic linter that checks for missing log event finishers. Finds errors like: `log.Error().Int64("userID": 5)` - missing the `Msg`/`Msgf` finishers. 6 | 7 | ## Problem 8 | 9 | When using zerolog it's easy to forget to finish the log event chain by calling a finisher - the `Msg` or `Msgf` function that will schedule the event for writing. The problem with this is that it doesn't warn/panic during compilation and it's not easily found by grep or other general tools. It's even prominently mentioned in the project's readme, that: 10 | 11 | > 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. 12 | 13 | ## Solution 14 | 15 | A basic linter like this one here that looks for method invocations on `zerolog.Event` can examine the last call in a method call chain and check if it is a finisher, thus pointing out these errors. 16 | 17 | ## Usage 18 | 19 | Just compile this and then run it. Or just run it via `go run` command via something like `go run cmd/lint/lint.go`. 20 | 21 | The command accepts only one argument - the package to be inspected - and 4 optional flags, all of which can occur multiple times. The standard synopsis of the command is: 22 | 23 | `lint [-finisher value] [-ignoreFile value] [-ignorePkg value] [-ignorePkgRecursively value] package` 24 | 25 | #### Flags 26 | 27 | - finisher 28 | - specify which finishers to accept, defaults to `Msg` and `Msgf` 29 | - ignoreFile 30 | - which files to ignore, either by full path or by go path (package/file.go) 31 | - ignorePkg 32 | - do not inspect the specified package if found in the dependency tree 33 | - ignorePkgRecursively 34 | - do not inspect the specified package or its subpackages if found in the dependency tree 35 | 36 | ## Drawbacks 37 | 38 | As it is, linter can generate a false positives in a specific case. These false positives come from the fact that if you have a method that returns a `zerolog.Event` the linter will flag it because you are obviously not finishing the event. This will be solved in later release. 39 | 40 | -------------------------------------------------------------------------------- /cmd/lint/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rs/zerolog/cmd/lint 2 | 3 | go 1.15 4 | 5 | require golang.org/x/tools v0.1.8 6 | -------------------------------------------------------------------------------- /cmd/lint/go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 4 | golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= 5 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 8 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 9 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= 16 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 18 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 19 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 20 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 21 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 22 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 23 | golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= 24 | golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 25 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 26 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 27 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 28 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 29 | -------------------------------------------------------------------------------- /cmd/lint/lint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/ast" 7 | "go/token" 8 | "go/types" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "golang.org/x/tools/go/loader" 14 | ) 15 | 16 | var ( 17 | recursivelyIgnoredPkgs arrayFlag 18 | ignoredPkgs arrayFlag 19 | ignoredFiles arrayFlag 20 | allowedFinishers arrayFlag = []string{"Msg", "Msgf"} 21 | rootPkg string 22 | ) 23 | 24 | // parse input flags and args 25 | func init() { 26 | flag.Var(&recursivelyIgnoredPkgs, "ignorePkgRecursively", "ignore the specified package and all subpackages recursively") 27 | flag.Var(&ignoredPkgs, "ignorePkg", "ignore the specified package") 28 | flag.Var(&ignoredFiles, "ignoreFile", "ignore the specified file by its path and/or go path (package/file.go)") 29 | flag.Var(&allowedFinishers, "finisher", "allowed finisher for the event chain") 30 | flag.Parse() 31 | 32 | // add zerolog to recursively ignored packages 33 | recursivelyIgnoredPkgs = append(recursivelyIgnoredPkgs, "github.com/rs/zerolog") 34 | args := flag.Args() 35 | if len(args) != 1 { 36 | fmt.Fprintln(os.Stderr, "you must provide exactly one package path") 37 | os.Exit(1) 38 | } 39 | rootPkg = args[0] 40 | } 41 | 42 | func main() { 43 | // load the package and all its dependencies 44 | conf := loader.Config{} 45 | conf.Import(rootPkg) 46 | p, err := conf.Load() 47 | if err != nil { 48 | fmt.Fprintf(os.Stderr, "Error: unable to load the root package. %s\n", err.Error()) 49 | os.Exit(1) 50 | } 51 | 52 | // get the github.com/rs/zerolog.Event type 53 | event := getEvent(p) 54 | if event == nil { 55 | fmt.Fprintln(os.Stderr, "Error: github.com/rs/zerolog.Event declaration not found, maybe zerolog is not imported in the scanned package?") 56 | os.Exit(1) 57 | } 58 | 59 | // get all selections (function calls) with the github.com/rs/zerolog.Event (or pointer) receiver 60 | selections := getSelectionsWithReceiverType(p, event) 61 | 62 | // print the violations (if any) 63 | hasViolations := false 64 | for _, s := range selections { 65 | if hasBadFinisher(p, s) { 66 | hasViolations = true 67 | fmt.Printf("Error: missing or bad finisher for log chain, last call: %q at: %s:%v\n", s.fn.Name(), p.Fset.File(s.Pos()).Name(), p.Fset.Position(s.Pos()).Line) 68 | } 69 | } 70 | 71 | // if no violations detected, return normally 72 | if !hasViolations { 73 | fmt.Println("No violations found") 74 | return 75 | } 76 | 77 | // if violations were detected, return error code 78 | os.Exit(1) 79 | } 80 | 81 | func getEvent(p *loader.Program) types.Type { 82 | for _, pkg := range p.AllPackages { 83 | if strings.HasSuffix(pkg.Pkg.Path(), "github.com/rs/zerolog") { 84 | for _, d := range pkg.Defs { 85 | if d != nil && d.Name() == "Event" { 86 | return d.Type() 87 | } 88 | } 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func getSelectionsWithReceiverType(p *loader.Program, targetType types.Type) map[token.Pos]selection { 96 | selections := map[token.Pos]selection{} 97 | 98 | for _, z := range p.AllPackages { 99 | for i, t := range z.Selections { 100 | switch o := t.Obj().(type) { 101 | case *types.Func: 102 | // this is not a bug, o.Type() is always *types.Signature, see docs 103 | if vt := o.Type().(*types.Signature).Recv(); vt != nil { 104 | typ := vt.Type() 105 | if pointer, ok := typ.(*types.Pointer); ok { 106 | typ = pointer.Elem() 107 | } 108 | 109 | if typ == targetType { 110 | if s, ok := selections[i.Pos()]; !ok || i.End() > s.End() { 111 | selections[i.Pos()] = selection{i, o, z.Pkg} 112 | } 113 | } 114 | } 115 | default: 116 | // skip 117 | } 118 | } 119 | } 120 | 121 | return selections 122 | } 123 | 124 | func hasBadFinisher(p *loader.Program, s selection) bool { 125 | pkgPath := strings.TrimPrefix(s.pkg.Path(), rootPkg+"/vendor/") 126 | absoluteFilePath := strings.TrimPrefix(p.Fset.File(s.Pos()).Name(), rootPkg+"/vendor/") 127 | goFilePath := pkgPath + "/" + filepath.Base(p.Fset.Position(s.Pos()).Filename) 128 | 129 | for _, f := range allowedFinishers { 130 | if f == s.fn.Name() { 131 | return false 132 | } 133 | } 134 | 135 | for _, ignoredPkg := range recursivelyIgnoredPkgs { 136 | if strings.HasPrefix(pkgPath, ignoredPkg) { 137 | return false 138 | } 139 | } 140 | 141 | for _, ignoredPkg := range ignoredPkgs { 142 | if pkgPath == ignoredPkg { 143 | return false 144 | } 145 | } 146 | 147 | for _, ignoredFile := range ignoredFiles { 148 | if absoluteFilePath == ignoredFile { 149 | return false 150 | } 151 | 152 | if goFilePath == ignoredFile { 153 | return false 154 | } 155 | } 156 | 157 | return true 158 | } 159 | 160 | type arrayFlag []string 161 | 162 | func (i *arrayFlag) String() string { 163 | return fmt.Sprintf("%v", []string(*i)) 164 | } 165 | 166 | func (i *arrayFlag) Set(value string) error { 167 | *i = append(*i, value) 168 | return nil 169 | } 170 | 171 | type selection struct { 172 | *ast.SelectorExpr 173 | fn *types.Func 174 | pkg *types.Package 175 | } 176 | -------------------------------------------------------------------------------- /cmd/prettylog/README.md: -------------------------------------------------------------------------------- 1 | # Zerolog PrettyLog 2 | 3 | This is a basic CLI utility that will colorize and pretty print your structured JSON logs. 4 | 5 | ## Usage 6 | 7 | You can compile it or run it directly. The only issue is that by default Zerolog does not output to `stdout` 8 | but rather to `stderr` so we must pipe `stderr` stream to this CLI tool. 9 | 10 | ### Linux 11 | 12 | These commands will redirect `stderr` to our `prettylog` tool and `stdout` will remain unaffected. 13 | 14 | 1. Compiled version 15 | 16 | ```shell 17 | some_program_with_zerolog 2> >(prettylog) 18 | ``` 19 | 20 | 2. Run it directly with `go run` 21 | 22 | ```shell 23 | some_program_with_zerolog 2> >(go run cmd/prettylog/prettylog.go) 24 | ``` 25 | 26 | ### Windows 27 | 28 | These commands will redirect `stderr` to `stdout` and then pipe it to our `prettylog` tool. 29 | 30 | 1. Compiled version 31 | 32 | ```shell 33 | some_program_with_zerolog 2>&1 | prettylog 34 | ``` 35 | 36 | 2. Run it directly with `go run` 37 | 38 | ```shell 39 | some_program_with_zerolog 2>&1 | go run cmd/prettylog/prettylog.go 40 | ``` 41 | -------------------------------------------------------------------------------- /cmd/prettylog/prettylog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "time" 11 | 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | func isInputFromPipe() bool { 16 | fileInfo, _ := os.Stdin.Stat() 17 | return fileInfo.Mode()&os.ModeCharDevice == 0 18 | } 19 | 20 | func processInput(reader io.Reader, writer io.Writer) error { 21 | scanner := bufio.NewScanner(reader) 22 | for scanner.Scan() { 23 | bytesToWrite := scanner.Bytes() 24 | _, err := writer.Write(bytesToWrite) 25 | if err != nil { 26 | if errors.Is(err, io.EOF) { 27 | break 28 | } 29 | 30 | fmt.Printf("%s\n", bytesToWrite) 31 | } 32 | } 33 | 34 | return scanner.Err() 35 | } 36 | 37 | func main() { 38 | timeFormats := map[string]string{ 39 | "default": time.Kitchen, 40 | "full": time.RFC1123, 41 | } 42 | 43 | timeFormatFlag := flag.String( 44 | "time-format", 45 | "default", 46 | "Time format, either 'default' or 'full'", 47 | ) 48 | 49 | flag.Parse() 50 | 51 | timeFormat, ok := timeFormats[*timeFormatFlag] 52 | if !ok { 53 | panic("Invalid time-format provided") 54 | } 55 | 56 | writer := zerolog.NewConsoleWriter() 57 | writer.TimeFormat = timeFormat 58 | 59 | if isInputFromPipe() { 60 | _ = processInput(os.Stdin, writer) 61 | } else if flag.NArg() >= 1 { 62 | for _, filename := range flag.Args() { 63 | // Scan each line from filename and write it into writer 64 | reader, err := os.Open(filename) 65 | if err != nil { 66 | fmt.Printf("%s open: %v", filename, err) 67 | os.Exit(1) 68 | } 69 | 70 | if err := processInput(reader, writer); err != nil { 71 | fmt.Printf("%s scan: %v", filename, err) 72 | os.Exit(1) 73 | } 74 | } 75 | } else { 76 | fmt.Println("Usage:") 77 | fmt.Println(" app_with_zerolog | 2> >(prettylog)") 78 | fmt.Println(" prettylog zerolog_output.jsonl") 79 | os.Exit(1) 80 | return 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ctx.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var disabledLogger *Logger 8 | 9 | func init() { 10 | SetGlobalLevel(TraceLevel) 11 | l := Nop() 12 | disabledLogger = &l 13 | } 14 | 15 | type ctxKey struct{} 16 | 17 | // WithContext returns a copy of ctx with the receiver attached. The Logger 18 | // attached to the provided Context (if any) will not be effected. If the 19 | // receiver's log level is Disabled it will only be attached to the returned 20 | // Context if the provided Context has a previously attached Logger. If the 21 | // provided Context has no attached Logger, a Disabled Logger will not be 22 | // attached. 23 | // 24 | // Note: to modify the existing Logger attached to a Context (instead of 25 | // replacing it in a new Context), use UpdateContext with the following 26 | // notation: 27 | // 28 | // ctx := r.Context() 29 | // l := zerolog.Ctx(ctx) 30 | // l.UpdateContext(func(c Context) Context { 31 | // return c.Str("bar", "baz") 32 | // }) 33 | // 34 | func (l Logger) WithContext(ctx context.Context) context.Context { 35 | if _, ok := ctx.Value(ctxKey{}).(*Logger); !ok && l.level == Disabled { 36 | // Do not store disabled logger. 37 | return ctx 38 | } 39 | return context.WithValue(ctx, ctxKey{}, &l) 40 | } 41 | 42 | // Ctx returns the Logger associated with the ctx. If no logger 43 | // is associated, DefaultContextLogger is returned, unless DefaultContextLogger 44 | // is nil, in which case a disabled logger is returned. 45 | func Ctx(ctx context.Context) *Logger { 46 | if l, ok := ctx.Value(ctxKey{}).(*Logger); ok { 47 | return l 48 | } else if l = DefaultContextLogger; l != nil { 49 | return l 50 | } 51 | return disabledLogger 52 | } 53 | -------------------------------------------------------------------------------- /ctx_test.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/rs/zerolog/internal/cbor" 11 | ) 12 | 13 | func TestCtx(t *testing.T) { 14 | log := New(io.Discard) 15 | ctx := log.WithContext(context.Background()) 16 | log2 := Ctx(ctx) 17 | if !reflect.DeepEqual(log, *log2) { 18 | t.Error("Ctx did not return the expected logger") 19 | } 20 | 21 | // update 22 | log = log.Level(InfoLevel) 23 | ctx = log.WithContext(ctx) 24 | log2 = Ctx(ctx) 25 | if !reflect.DeepEqual(log, *log2) { 26 | t.Error("Ctx did not return the expected logger") 27 | } 28 | 29 | log2 = Ctx(context.Background()) 30 | if log2 != disabledLogger { 31 | t.Error("Ctx did not return the expected logger") 32 | } 33 | 34 | DefaultContextLogger = &log 35 | t.Cleanup(func() { DefaultContextLogger = nil }) 36 | log2 = Ctx(context.Background()) 37 | if log2 != &log { 38 | t.Error("Ctx did not return the expected logger") 39 | } 40 | } 41 | 42 | func TestCtxDisabled(t *testing.T) { 43 | dl := New(io.Discard).Level(Disabled) 44 | ctx := dl.WithContext(context.Background()) 45 | if ctx != context.Background() { 46 | t.Error("WithContext stored a disabled logger") 47 | } 48 | 49 | l := New(io.Discard).With().Str("foo", "bar").Logger() 50 | ctx = l.WithContext(ctx) 51 | if !reflect.DeepEqual(Ctx(ctx), &l) { 52 | t.Error("WithContext did not store logger") 53 | } 54 | 55 | l.UpdateContext(func(c Context) Context { 56 | return c.Str("bar", "baz") 57 | }) 58 | ctx = l.WithContext(ctx) 59 | if !reflect.DeepEqual(Ctx(ctx), &l) { 60 | t.Error("WithContext did not store updated logger") 61 | } 62 | 63 | l = l.Level(DebugLevel) 64 | ctx = l.WithContext(ctx) 65 | if !reflect.DeepEqual(Ctx(ctx), &l) { 66 | t.Error("WithContext did not store copied logger") 67 | } 68 | 69 | ctx = dl.WithContext(ctx) 70 | if !reflect.DeepEqual(Ctx(ctx), &dl) { 71 | t.Error("WithContext did not override logger with a disabled logger") 72 | } 73 | } 74 | 75 | type logObjectMarshalerImpl struct { 76 | name string 77 | age int 78 | } 79 | 80 | func (t logObjectMarshalerImpl) MarshalZerologObject(e *Event) { 81 | e.Str("name", "custom_value").Int("age", t.age) 82 | } 83 | 84 | func Test_InterfaceLogObjectMarshaler(t *testing.T) { 85 | var buf bytes.Buffer 86 | log := New(&buf) 87 | ctx := log.WithContext(context.Background()) 88 | 89 | log2 := Ctx(ctx) 90 | 91 | withLog := log2.With().Interface("obj", &logObjectMarshalerImpl{ 92 | name: "foo", 93 | age: 29, 94 | }).Logger() 95 | 96 | withLog.Info().Msg("test") 97 | 98 | if got, want := cbor.DecodeIfBinaryToString(buf.Bytes()), `{"level":"info","obj":{"name":"custom_value","age":29},"message":"test"}`+"\n"; got != want { 99 | t.Errorf("got %q, want %q", got, want) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /diode/diode.go: -------------------------------------------------------------------------------- 1 | // Package diode provides a thread-safe, lock-free, non-blocking io.Writer 2 | // wrapper. 3 | package diode 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "sync" 9 | "time" 10 | 11 | "github.com/rs/zerolog/diode/internal/diodes" 12 | ) 13 | 14 | var bufPool = &sync.Pool{ 15 | New: func() interface{} { 16 | return make([]byte, 0, 500) 17 | }, 18 | } 19 | 20 | type Alerter func(missed int) 21 | 22 | type diodeFetcher interface { 23 | diodes.Diode 24 | Next() diodes.GenericDataType 25 | } 26 | 27 | // Writer is a io.Writer wrapper that uses a diode to make Write lock-free, 28 | // non-blocking and thread safe. 29 | type Writer struct { 30 | w io.Writer 31 | d diodeFetcher 32 | c context.CancelFunc 33 | done chan struct{} 34 | } 35 | 36 | // NewWriter creates a writer wrapping w with a many-to-one diode in order to 37 | // never block log producers and drop events if the writer can't keep up with 38 | // the flow of data. 39 | // 40 | // Use a diode.Writer when 41 | // 42 | // wr := diode.NewWriter(w, 1000, 0, func(missed int) { 43 | // log.Printf("Dropped %d messages", missed) 44 | // }) 45 | // log := zerolog.New(wr) 46 | // 47 | // If pollInterval is greater than 0, a poller is used otherwise a waiter is 48 | // used. 49 | // 50 | // See code.cloudfoundry.org/go-diodes for more info on diode. 51 | func NewWriter(w io.Writer, size int, pollInterval time.Duration, f Alerter) Writer { 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | dw := Writer{ 54 | w: w, 55 | c: cancel, 56 | done: make(chan struct{}), 57 | } 58 | if f == nil { 59 | f = func(int) {} 60 | } 61 | d := diodes.NewManyToOne(size, diodes.AlertFunc(f)) 62 | if pollInterval > 0 { 63 | dw.d = diodes.NewPoller(d, 64 | diodes.WithPollingInterval(pollInterval), 65 | diodes.WithPollingContext(ctx)) 66 | } else { 67 | dw.d = diodes.NewWaiter(d, 68 | diodes.WithWaiterContext(ctx)) 69 | } 70 | go dw.poll() 71 | return dw 72 | } 73 | 74 | func (dw Writer) Write(p []byte) (n int, err error) { 75 | // p is pooled in zerolog so we can't hold it passed this call, hence the 76 | // copy. 77 | p = append(bufPool.Get().([]byte), p...) 78 | dw.d.Set(diodes.GenericDataType(&p)) 79 | return len(p), nil 80 | } 81 | 82 | // Close releases the diode poller and call Close on the wrapped writer if 83 | // io.Closer is implemented. 84 | func (dw Writer) Close() error { 85 | dw.c() 86 | <-dw.done 87 | if w, ok := dw.w.(io.Closer); ok { 88 | return w.Close() 89 | } 90 | return nil 91 | } 92 | 93 | func (dw Writer) poll() { 94 | defer close(dw.done) 95 | for { 96 | d := dw.d.Next() 97 | if d == nil { 98 | return 99 | } 100 | p := *(*[]byte)(d) 101 | dw.w.Write(p) 102 | 103 | // Proper usage of a sync.Pool requires each entry to have approximately 104 | // the same memory cost. To obtain this property when the stored type 105 | // contains a variably-sized buffer, we add a hard limit on the maximum buffer 106 | // to place back in the pool. 107 | // 108 | // See https://golang.org/issue/23199 109 | const maxSize = 1 << 16 // 64KiB 110 | if cap(p) <= maxSize { 111 | bufPool.Put(p[:0]) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /diode/diode_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | 3 | package diode_test 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/diode" 11 | ) 12 | 13 | func ExampleNewWriter() { 14 | w := diode.NewWriter(os.Stdout, 1000, 0, func(missed int) { 15 | fmt.Printf("Dropped %d messages\n", missed) 16 | }) 17 | log := zerolog.New(w) 18 | log.Print("test") 19 | 20 | w.Close() 21 | 22 | // Output: {"level":"debug","message":"test"} 23 | } 24 | -------------------------------------------------------------------------------- /diode/diode_test.go: -------------------------------------------------------------------------------- 1 | package diode_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "testing" 11 | "time" 12 | 13 | "github.com/rs/zerolog" 14 | "github.com/rs/zerolog/diode" 15 | "github.com/rs/zerolog/internal/cbor" 16 | ) 17 | 18 | func TestNewWriter(t *testing.T) { 19 | buf := bytes.Buffer{} 20 | w := diode.NewWriter(&buf, 1000, 0, func(missed int) { 21 | fmt.Printf("Dropped %d messages\n", missed) 22 | }) 23 | log := zerolog.New(w) 24 | log.Print("test") 25 | 26 | w.Close() 27 | want := "{\"level\":\"debug\",\"message\":\"test\"}\n" 28 | got := cbor.DecodeIfBinaryToString(buf.Bytes()) 29 | if got != want { 30 | t.Errorf("Diode New Writer Test failed. got:%s, want:%s!", got, want) 31 | } 32 | } 33 | 34 | func TestClose(t *testing.T) { 35 | buf := bytes.Buffer{} 36 | w := diode.NewWriter(&buf, 1000, 0, func(missed int) {}) 37 | log := zerolog.New(w) 38 | log.Print("test") 39 | w.Close() 40 | } 41 | 42 | func TestFatal(t *testing.T) { 43 | if os.Getenv("TEST_FATAL") == "1" { 44 | w := diode.NewWriter(os.Stderr, 1000, 0, func(missed int) { 45 | fmt.Printf("Dropped %d messages\n", missed) 46 | }) 47 | defer w.Close() 48 | log := zerolog.New(w) 49 | log.Fatal().Msg("test") 50 | return 51 | } 52 | 53 | cmd := exec.Command(os.Args[0], "-test.run=TestFatal") 54 | cmd.Env = append(os.Environ(), "TEST_FATAL=1") 55 | stderr, err := cmd.StderrPipe() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | err = cmd.Start() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | slurp, err := io.ReadAll(stderr) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | err = cmd.Wait() 68 | if err == nil { 69 | t.Error("Expected log.Fatal to exit with non-zero status") 70 | } 71 | 72 | want := "{\"level\":\"fatal\",\"message\":\"test\"}\n" 73 | got := cbor.DecodeIfBinaryToString(slurp) 74 | if got != want { 75 | t.Errorf("Diode Fatal Test failed. got:%s, want:%s!", got, want) 76 | } 77 | } 78 | 79 | type SlowWriter struct{} 80 | 81 | func (rw *SlowWriter) Write(p []byte) (n int, err error) { 82 | time.Sleep(200 * time.Millisecond) 83 | fmt.Print(string(p)) 84 | return len(p), nil 85 | } 86 | 87 | func TestFatalWithFilteredLevelWriter(t *testing.T) { 88 | if os.Getenv("TEST_FATAL_SLOW") == "1" { 89 | slowWriter := SlowWriter{} 90 | diodeWriter := diode.NewWriter(&slowWriter, 500, 0, func(missed int) { 91 | fmt.Printf("Missed %d logs\n", missed) 92 | }) 93 | leveledDiodeWriter := zerolog.LevelWriterAdapter{ 94 | Writer: &diodeWriter, 95 | } 96 | filteredDiodeWriter := zerolog.FilteredLevelWriter{ 97 | Writer: &leveledDiodeWriter, 98 | Level: zerolog.InfoLevel, 99 | } 100 | logger := zerolog.New(&filteredDiodeWriter) 101 | logger.Fatal().Msg("test") 102 | return 103 | } 104 | 105 | cmd := exec.Command(os.Args[0], "-test.run=TestFatalWithFilteredLevelWriter") 106 | cmd.Env = append(os.Environ(), "TEST_FATAL_SLOW=1") 107 | stdout, err := cmd.StdoutPipe() 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | err = cmd.Start() 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | slurp, err := io.ReadAll(stdout) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | err = cmd.Wait() 120 | if err == nil { 121 | t.Error("Expected log.Fatal to exit with non-zero status") 122 | } 123 | 124 | got := cbor.DecodeIfBinaryToString(slurp) 125 | want := "{\"level\":\"fatal\",\"message\":\"test\"}\n" 126 | if got != want { 127 | t.Errorf("Expected output %q, got: %q", want, got) 128 | } 129 | } 130 | 131 | func Benchmark(b *testing.B) { 132 | log.SetOutput(io.Discard) 133 | defer log.SetOutput(os.Stderr) 134 | benchs := map[string]time.Duration{ 135 | "Waiter": 0, 136 | "Pooler": 10 * time.Millisecond, 137 | } 138 | for name, interval := range benchs { 139 | b.Run(name, func(b *testing.B) { 140 | w := diode.NewWriter(io.Discard, 100000, interval, nil) 141 | log := zerolog.New(w) 142 | defer w.Close() 143 | 144 | b.SetParallelism(1000) 145 | b.RunParallel(func(pb *testing.PB) { 146 | for pb.Next() { 147 | log.Print("test") 148 | } 149 | }) 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /diode/internal/diodes/README: -------------------------------------------------------------------------------- 1 | Copied from https://github.com/cloudfoundry/go-diodes to avoid test dependencies. 2 | -------------------------------------------------------------------------------- /diode/internal/diodes/many_to_one.go: -------------------------------------------------------------------------------- 1 | package diodes 2 | 3 | import ( 4 | "log" 5 | "sync/atomic" 6 | "unsafe" 7 | ) 8 | 9 | // ManyToOne diode is optimal for many writers (go-routines B-n) and a single 10 | // reader (go-routine A). It is not thread safe for multiple readers. 11 | type ManyToOne struct { 12 | writeIndex uint64 13 | readIndex uint64 14 | buffer []unsafe.Pointer 15 | alerter Alerter 16 | } 17 | 18 | // NewManyToOne creates a new diode (ring buffer). The ManyToOne diode 19 | // is optimized for many writers (on go-routines B-n) and a single reader 20 | // (on go-routine A). The alerter is invoked on the read's go-routine. It is 21 | // called when it notices that the writer go-routine has passed it and wrote 22 | // over data. A nil can be used to ignore alerts. 23 | func NewManyToOne(size int, alerter Alerter) *ManyToOne { 24 | if alerter == nil { 25 | alerter = AlertFunc(func(int) {}) 26 | } 27 | 28 | d := &ManyToOne{ 29 | buffer: make([]unsafe.Pointer, size), 30 | alerter: alerter, 31 | } 32 | 33 | // Start write index at the value before 0 34 | // to allow the first write to use AddUint64 35 | // and still have a beginning index of 0 36 | d.writeIndex = ^d.writeIndex 37 | return d 38 | } 39 | 40 | // Set sets the data in the next slot of the ring buffer. 41 | func (d *ManyToOne) Set(data GenericDataType) { 42 | for { 43 | writeIndex := atomic.AddUint64(&d.writeIndex, 1) 44 | idx := writeIndex % uint64(len(d.buffer)) 45 | old := atomic.LoadPointer(&d.buffer[idx]) 46 | 47 | if old != nil && 48 | (*bucket)(old) != nil && 49 | (*bucket)(old).seq > writeIndex-uint64(len(d.buffer)) { 50 | log.Println("Diode set collision: consider using a larger diode") 51 | continue 52 | } 53 | 54 | newBucket := &bucket{ 55 | data: data, 56 | seq: writeIndex, 57 | } 58 | 59 | if !atomic.CompareAndSwapPointer(&d.buffer[idx], old, unsafe.Pointer(newBucket)) { 60 | log.Println("Diode set collision: consider using a larger diode") 61 | continue 62 | } 63 | 64 | return 65 | } 66 | } 67 | 68 | // TryNext will attempt to read from the next slot of the ring buffer. 69 | // If there is no data available, it will return (nil, false). 70 | func (d *ManyToOne) TryNext() (data GenericDataType, ok bool) { 71 | // Read a value from the ring buffer based on the readIndex. 72 | idx := d.readIndex % uint64(len(d.buffer)) 73 | result := (*bucket)(atomic.SwapPointer(&d.buffer[idx], nil)) 74 | 75 | // When the result is nil that means the writer has not had the 76 | // opportunity to write a value into the diode. This value must be ignored 77 | // and the read head must not increment. 78 | if result == nil { 79 | return nil, false 80 | } 81 | 82 | // When the seq value is less than the current read index that means a 83 | // value was read from idx that was previously written but since has 84 | // been dropped. This value must be ignored and the read head must not 85 | // increment. 86 | // 87 | // The simulation for this scenario assumes the fast forward occurred as 88 | // detailed below. 89 | // 90 | // 5. The reader reads again getting seq 5. It then reads again expecting 91 | // seq 6 but gets seq 2. This is a read of a stale value that was 92 | // effectively "dropped" so the read fails and the read head stays put. 93 | // `| 4 | 5 | 2 | 3 |` r: 7, w: 6 94 | // 95 | if result.seq < d.readIndex { 96 | return nil, false 97 | } 98 | 99 | // When the seq value is greater than the current read index that means a 100 | // value was read from idx that overwrote the value that was expected to 101 | // be at this idx. This happens when the writer has lapped the reader. The 102 | // reader needs to catch up to the writer so it moves its write head to 103 | // the new seq, effectively dropping the messages that were not read in 104 | // between the two values. 105 | // 106 | // Here is a simulation of this scenario: 107 | // 108 | // 1. Both the read and write heads start at 0. 109 | // `| nil | nil | nil | nil |` r: 0, w: 0 110 | // 2. The writer fills the buffer. 111 | // `| 0 | 1 | 2 | 3 |` r: 0, w: 4 112 | // 3. The writer laps the read head. 113 | // `| 4 | 5 | 2 | 3 |` r: 0, w: 6 114 | // 4. The reader reads the first value, expecting a seq of 0 but reads 4, 115 | // this forces the reader to fast forward to 5. 116 | // `| 4 | 5 | 2 | 3 |` r: 5, w: 6 117 | // 118 | if result.seq > d.readIndex { 119 | dropped := result.seq - d.readIndex 120 | d.readIndex = result.seq 121 | d.alerter.Alert(int(dropped)) 122 | } 123 | 124 | // Only increment read index if a regular read occurred (where seq was 125 | // equal to readIndex) or a value was read that caused a fast forward 126 | // (where seq was greater than readIndex). 127 | // 128 | d.readIndex++ 129 | return result.data, true 130 | } 131 | -------------------------------------------------------------------------------- /diode/internal/diodes/one_to_one.go: -------------------------------------------------------------------------------- 1 | package diodes 2 | 3 | import ( 4 | "sync/atomic" 5 | "unsafe" 6 | ) 7 | 8 | // GenericDataType is the data type the diodes operate on. 9 | type GenericDataType unsafe.Pointer 10 | 11 | // Alerter is used to report how many values were overwritten since the 12 | // last write. 13 | type Alerter interface { 14 | Alert(missed int) 15 | } 16 | 17 | // AlertFunc type is an adapter to allow the use of ordinary functions as 18 | // Alert handlers. 19 | type AlertFunc func(missed int) 20 | 21 | // Alert calls f(missed) 22 | func (f AlertFunc) Alert(missed int) { 23 | f(missed) 24 | } 25 | 26 | type bucket struct { 27 | data GenericDataType 28 | seq uint64 // seq is the recorded write index at the time of writing 29 | } 30 | 31 | // OneToOne diode is meant to be used by a single reader and a single writer. 32 | // It is not thread safe if used otherwise. 33 | type OneToOne struct { 34 | writeIndex uint64 35 | readIndex uint64 36 | buffer []unsafe.Pointer 37 | alerter Alerter 38 | } 39 | 40 | // NewOneToOne creates a new diode is meant to be used by a single reader and 41 | // a single writer. The alerter is invoked on the read's go-routine. It is 42 | // called when it notices that the writer go-routine has passed it and wrote 43 | // over data. A nil can be used to ignore alerts. 44 | func NewOneToOne(size int, alerter Alerter) *OneToOne { 45 | if alerter == nil { 46 | alerter = AlertFunc(func(int) {}) 47 | } 48 | 49 | return &OneToOne{ 50 | buffer: make([]unsafe.Pointer, size), 51 | alerter: alerter, 52 | } 53 | } 54 | 55 | // Set sets the data in the next slot of the ring buffer. 56 | func (d *OneToOne) Set(data GenericDataType) { 57 | idx := d.writeIndex % uint64(len(d.buffer)) 58 | 59 | newBucket := &bucket{ 60 | data: data, 61 | seq: d.writeIndex, 62 | } 63 | d.writeIndex++ 64 | 65 | atomic.StorePointer(&d.buffer[idx], unsafe.Pointer(newBucket)) 66 | } 67 | 68 | // TryNext will attempt to read from the next slot of the ring buffer. 69 | // If there is no data available, it will return (nil, false). 70 | func (d *OneToOne) TryNext() (data GenericDataType, ok bool) { 71 | // Read a value from the ring buffer based on the readIndex. 72 | idx := d.readIndex % uint64(len(d.buffer)) 73 | result := (*bucket)(atomic.SwapPointer(&d.buffer[idx], nil)) 74 | 75 | // When the result is nil that means the writer has not had the 76 | // opportunity to write a value into the diode. This value must be ignored 77 | // and the read head must not increment. 78 | if result == nil { 79 | return nil, false 80 | } 81 | 82 | // When the seq value is less than the current read index that means a 83 | // value was read from idx that was previously written but since has 84 | // been dropped. This value must be ignored and the read head must not 85 | // increment. 86 | // 87 | // The simulation for this scenario assumes the fast forward occurred as 88 | // detailed below. 89 | // 90 | // 5. The reader reads again getting seq 5. It then reads again expecting 91 | // seq 6 but gets seq 2. This is a read of a stale value that was 92 | // effectively "dropped" so the read fails and the read head stays put. 93 | // `| 4 | 5 | 2 | 3 |` r: 7, w: 6 94 | // 95 | if result.seq < d.readIndex { 96 | return nil, false 97 | } 98 | 99 | // When the seq value is greater than the current read index that means a 100 | // value was read from idx that overwrote the value that was expected to 101 | // be at this idx. This happens when the writer has lapped the reader. The 102 | // reader needs to catch up to the writer so it moves its write head to 103 | // the new seq, effectively dropping the messages that were not read in 104 | // between the two values. 105 | // 106 | // Here is a simulation of this scenario: 107 | // 108 | // 1. Both the read and write heads start at 0. 109 | // `| nil | nil | nil | nil |` r: 0, w: 0 110 | // 2. The writer fills the buffer. 111 | // `| 0 | 1 | 2 | 3 |` r: 0, w: 4 112 | // 3. The writer laps the read head. 113 | // `| 4 | 5 | 2 | 3 |` r: 0, w: 6 114 | // 4. The reader reads the first value, expecting a seq of 0 but reads 4, 115 | // this forces the reader to fast forward to 5. 116 | // `| 4 | 5 | 2 | 3 |` r: 5, w: 6 117 | // 118 | if result.seq > d.readIndex { 119 | dropped := result.seq - d.readIndex 120 | d.readIndex = result.seq 121 | d.alerter.Alert(int(dropped)) 122 | } 123 | 124 | // Only increment read index if a regular read occurred (where seq was 125 | // equal to readIndex) or a value was read that caused a fast forward 126 | // (where seq was greater than readIndex). 127 | d.readIndex++ 128 | return result.data, true 129 | } 130 | -------------------------------------------------------------------------------- /diode/internal/diodes/poller.go: -------------------------------------------------------------------------------- 1 | package diodes 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Diode is any implementation of a diode. 9 | type Diode interface { 10 | Set(GenericDataType) 11 | TryNext() (GenericDataType, bool) 12 | } 13 | 14 | // Poller will poll a diode until a value is available. 15 | type Poller struct { 16 | Diode 17 | interval time.Duration 18 | ctx context.Context 19 | } 20 | 21 | // PollerConfigOption can be used to setup the poller. 22 | type PollerConfigOption func(*Poller) 23 | 24 | // WithPollingInterval sets the interval at which the diode is queried 25 | // for new data. The default is 10ms. 26 | func WithPollingInterval(interval time.Duration) PollerConfigOption { 27 | return func(c *Poller) { 28 | c.interval = interval 29 | } 30 | } 31 | 32 | // WithPollingContext sets the context to cancel any retrieval (Next()). It 33 | // will not change any results for adding data (Set()). Default is 34 | // context.Background(). 35 | func WithPollingContext(ctx context.Context) PollerConfigOption { 36 | return func(c *Poller) { 37 | c.ctx = ctx 38 | } 39 | } 40 | 41 | // NewPoller returns a new Poller that wraps the given diode. 42 | func NewPoller(d Diode, opts ...PollerConfigOption) *Poller { 43 | p := &Poller{ 44 | Diode: d, 45 | interval: 10 * time.Millisecond, 46 | ctx: context.Background(), 47 | } 48 | 49 | for _, o := range opts { 50 | o(p) 51 | } 52 | 53 | return p 54 | } 55 | 56 | // Next polls the diode until data is available or until the context is done. 57 | // If the context is done, then nil will be returned. 58 | func (p *Poller) Next() GenericDataType { 59 | for { 60 | data, ok := p.Diode.TryNext() 61 | if !ok { 62 | if p.isDone() { 63 | return nil 64 | } 65 | 66 | time.Sleep(p.interval) 67 | continue 68 | } 69 | return data 70 | } 71 | } 72 | 73 | func (p *Poller) isDone() bool { 74 | select { 75 | case <-p.ctx.Done(): 76 | return true 77 | default: 78 | return false 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /diode/internal/diodes/waiter.go: -------------------------------------------------------------------------------- 1 | package diodes 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Waiter will use a conditional mutex to alert the reader to when data is 9 | // available. 10 | type Waiter struct { 11 | Diode 12 | mu sync.Mutex 13 | c *sync.Cond 14 | ctx context.Context 15 | } 16 | 17 | // WaiterConfigOption can be used to setup the waiter. 18 | type WaiterConfigOption func(*Waiter) 19 | 20 | // WithWaiterContext sets the context to cancel any retrieval (Next()). It 21 | // will not change any results for adding data (Set()). Default is 22 | // context.Background(). 23 | func WithWaiterContext(ctx context.Context) WaiterConfigOption { 24 | return func(c *Waiter) { 25 | c.ctx = ctx 26 | } 27 | } 28 | 29 | // NewWaiter returns a new Waiter that wraps the given diode. 30 | func NewWaiter(d Diode, opts ...WaiterConfigOption) *Waiter { 31 | w := new(Waiter) 32 | w.Diode = d 33 | w.c = sync.NewCond(&w.mu) 34 | w.ctx = context.Background() 35 | 36 | for _, opt := range opts { 37 | opt(w) 38 | } 39 | 40 | go func() { 41 | <-w.ctx.Done() 42 | 43 | // Mutex is strictly necessary here to avoid a race in Next() (between 44 | // w.isDone() and w.c.Wait()) and w.c.Broadcast() here. 45 | w.mu.Lock() 46 | w.c.Broadcast() 47 | w.mu.Unlock() 48 | }() 49 | 50 | return w 51 | } 52 | 53 | // Set invokes the wrapped diode's Set with the given data and uses Broadcast 54 | // to wake up any readers. 55 | func (w *Waiter) Set(data GenericDataType) { 56 | w.Diode.Set(data) 57 | w.c.Broadcast() 58 | } 59 | 60 | // Next returns the next data point on the wrapped diode. If there is not any 61 | // new data, it will Wait for set to be called or the context to be done. 62 | // If the context is done, then nil will be returned. 63 | func (w *Waiter) Next() GenericDataType { 64 | w.mu.Lock() 65 | defer w.mu.Unlock() 66 | 67 | for { 68 | data, ok := w.Diode.TryNext() 69 | if !ok { 70 | if w.isDone() { 71 | return nil 72 | } 73 | 74 | w.c.Wait() 75 | continue 76 | } 77 | return data 78 | } 79 | } 80 | 81 | func (w *Waiter) isDone() bool { 82 | select { 83 | case <-w.ctx.Done(): 84 | return true 85 | default: 86 | return false 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type encoder interface { 9 | AppendArrayDelim(dst []byte) []byte 10 | AppendArrayEnd(dst []byte) []byte 11 | AppendArrayStart(dst []byte) []byte 12 | AppendBeginMarker(dst []byte) []byte 13 | AppendBool(dst []byte, val bool) []byte 14 | AppendBools(dst []byte, vals []bool) []byte 15 | AppendBytes(dst, s []byte) []byte 16 | AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, precision int) []byte 17 | AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []byte 18 | AppendEndMarker(dst []byte) []byte 19 | AppendFloat32(dst []byte, val float32, precision int) []byte 20 | AppendFloat64(dst []byte, val float64, precision int) []byte 21 | AppendFloats32(dst []byte, vals []float32, precision int) []byte 22 | AppendFloats64(dst []byte, vals []float64, precision int) []byte 23 | AppendHex(dst, s []byte) []byte 24 | AppendIPAddr(dst []byte, ip net.IP) []byte 25 | AppendIPPrefix(dst []byte, pfx net.IPNet) []byte 26 | AppendInt(dst []byte, val int) []byte 27 | AppendInt16(dst []byte, val int16) []byte 28 | AppendInt32(dst []byte, val int32) []byte 29 | AppendInt64(dst []byte, val int64) []byte 30 | AppendInt8(dst []byte, val int8) []byte 31 | AppendInterface(dst []byte, i interface{}) []byte 32 | AppendInts(dst []byte, vals []int) []byte 33 | AppendInts16(dst []byte, vals []int16) []byte 34 | AppendInts32(dst []byte, vals []int32) []byte 35 | AppendInts64(dst []byte, vals []int64) []byte 36 | AppendInts8(dst []byte, vals []int8) []byte 37 | AppendKey(dst []byte, key string) []byte 38 | AppendLineBreak(dst []byte) []byte 39 | AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte 40 | AppendNil(dst []byte) []byte 41 | AppendObjectData(dst []byte, o []byte) []byte 42 | AppendString(dst []byte, s string) []byte 43 | AppendStrings(dst []byte, vals []string) []byte 44 | AppendTime(dst []byte, t time.Time, format string) []byte 45 | AppendTimes(dst []byte, vals []time.Time, format string) []byte 46 | AppendUint(dst []byte, val uint) []byte 47 | AppendUint16(dst []byte, val uint16) []byte 48 | AppendUint32(dst []byte, val uint32) []byte 49 | AppendUint64(dst []byte, val uint64) []byte 50 | AppendUint8(dst []byte, val uint8) []byte 51 | AppendUints(dst []byte, vals []uint) []byte 52 | AppendUints16(dst []byte, vals []uint16) []byte 53 | AppendUints32(dst []byte, vals []uint32) []byte 54 | AppendUints64(dst []byte, vals []uint64) []byte 55 | AppendUints8(dst []byte, vals []uint8) []byte 56 | } 57 | -------------------------------------------------------------------------------- /encoder_cbor.go: -------------------------------------------------------------------------------- 1 | // +build binary_log 2 | 3 | package zerolog 4 | 5 | // This file contains bindings to do binary encoding. 6 | 7 | import ( 8 | "github.com/rs/zerolog/internal/cbor" 9 | ) 10 | 11 | var ( 12 | _ encoder = (*cbor.Encoder)(nil) 13 | 14 | enc = cbor.Encoder{} 15 | ) 16 | 17 | func init() { 18 | // using closure to reflect the changes at runtime. 19 | cbor.JSONMarshalFunc = func(v interface{}) ([]byte, error) { 20 | return InterfaceMarshalFunc(v) 21 | } 22 | } 23 | 24 | func appendJSON(dst []byte, j []byte) []byte { 25 | return cbor.AppendEmbeddedJSON(dst, j) 26 | } 27 | func appendCBOR(dst []byte, c []byte) []byte { 28 | return cbor.AppendEmbeddedCBOR(dst, c) 29 | } 30 | 31 | // decodeIfBinaryToString - converts a binary formatted log msg to a 32 | // JSON formatted String Log message. 33 | func decodeIfBinaryToString(in []byte) string { 34 | return cbor.DecodeIfBinaryToString(in) 35 | } 36 | 37 | func decodeObjectToStr(in []byte) string { 38 | return cbor.DecodeObjectToStr(in) 39 | } 40 | 41 | // decodeIfBinaryToBytes - converts a binary formatted log msg to a 42 | // JSON formatted Bytes Log message. 43 | func decodeIfBinaryToBytes(in []byte) []byte { 44 | return cbor.DecodeIfBinaryToBytes(in) 45 | } 46 | -------------------------------------------------------------------------------- /encoder_json.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | 3 | package zerolog 4 | 5 | // encoder_json.go file contains bindings to generate 6 | // JSON encoded byte stream. 7 | 8 | import ( 9 | "encoding/base64" 10 | "github.com/rs/zerolog/internal/json" 11 | ) 12 | 13 | var ( 14 | _ encoder = (*json.Encoder)(nil) 15 | 16 | enc = json.Encoder{} 17 | ) 18 | 19 | func init() { 20 | // using closure to reflect the changes at runtime. 21 | json.JSONMarshalFunc = func(v interface{}) ([]byte, error) { 22 | return InterfaceMarshalFunc(v) 23 | } 24 | } 25 | 26 | func appendJSON(dst []byte, j []byte) []byte { 27 | return append(dst, j...) 28 | } 29 | func appendCBOR(dst []byte, cbor []byte) []byte { 30 | dst = append(dst, []byte("\"data:application/cbor;base64,")...) 31 | l := len(dst) 32 | enc := base64.StdEncoding 33 | n := enc.EncodedLen(len(cbor)) 34 | for i := 0; i < n; i++ { 35 | dst = append(dst, '.') 36 | } 37 | enc.Encode(dst[l:], cbor) 38 | return append(dst, '"') 39 | } 40 | 41 | func decodeIfBinaryToString(in []byte) string { 42 | return string(in) 43 | } 44 | 45 | func decodeObjectToStr(in []byte) string { 46 | return string(in) 47 | } 48 | 49 | func decodeIfBinaryToBytes(in []byte) []byte { 50 | return in 51 | } 52 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | 3 | package zerolog 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | type nilError struct{} 13 | 14 | func (nilError) Error() string { 15 | return "" 16 | } 17 | 18 | func TestEvent_AnErr(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | err error 22 | want string 23 | }{ 24 | {"nil", nil, `{}`}, 25 | {"error", errors.New("test"), `{"err":"test"}`}, 26 | {"nil interface", func() *nilError { return nil }(), `{}`}, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | var buf bytes.Buffer 31 | e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) 32 | e.AnErr("err", tt.err) 33 | _ = e.write() 34 | if got, want := strings.TrimSpace(buf.String()), tt.want; got != want { 35 | t.Errorf("Event.AnErr() = %v, want %v", got, want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestEvent_ObjectWithNil(t *testing.T) { 42 | var buf bytes.Buffer 43 | e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) 44 | _ = e.Object("obj", nil) 45 | _ = e.write() 46 | 47 | want := `{"obj":null}` 48 | got := strings.TrimSpace(buf.String()) 49 | if got != want { 50 | t.Errorf("Event.Object() = %q, want %q", got, want) 51 | } 52 | } 53 | 54 | func TestEvent_EmbedObjectWithNil(t *testing.T) { 55 | var buf bytes.Buffer 56 | e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) 57 | _ = e.EmbedObject(nil) 58 | _ = e.write() 59 | 60 | want := "{}" 61 | got := strings.TrimSpace(buf.String()) 62 | if got != want { 63 | t.Errorf("Event.EmbedObject() = %q, want %q", got, want) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example.jsonl: -------------------------------------------------------------------------------- 1 | {"time":"5:41PM","level":"info","message":"Starting listener","listen":":8080","pid":37556} 2 | {"time":"5:41PM","level":"debug","message":"Access","database":"myapp","host":"localhost:4962","pid":37556} 3 | {"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":23} 4 | {"time":"5:41PM","level":"info","message":"Access","method":"POST","path":"/posts","pid":37556,"resp_time":532} 5 | {"time":"5:41PM","level":"warn","message":"Slow request","method":"POST","path":"/posts","pid":37556,"resp_time":532} 6 | {"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":10} 7 | {"time":"5:41PM","level":"error","message":"Database connection lost","database":"myapp","pid":37556,"error":"connection reset by peer"} 8 | -------------------------------------------------------------------------------- /fields.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "sort" 7 | "time" 8 | "unsafe" 9 | ) 10 | 11 | func isNilValue(i interface{}) bool { 12 | return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0 13 | } 14 | 15 | func appendFields(dst []byte, fields interface{}, stack bool) []byte { 16 | switch fields := fields.(type) { 17 | case []interface{}: 18 | if n := len(fields); n&0x1 == 1 { // odd number 19 | fields = fields[:n-1] 20 | } 21 | dst = appendFieldList(dst, fields, stack) 22 | case map[string]interface{}: 23 | keys := make([]string, 0, len(fields)) 24 | for key := range fields { 25 | keys = append(keys, key) 26 | } 27 | sort.Strings(keys) 28 | kv := make([]interface{}, 2) 29 | for _, key := range keys { 30 | kv[0], kv[1] = key, fields[key] 31 | dst = appendFieldList(dst, kv, stack) 32 | } 33 | } 34 | return dst 35 | } 36 | 37 | func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { 38 | for i, n := 0, len(kvList); i < n; i += 2 { 39 | key, val := kvList[i], kvList[i+1] 40 | if key, ok := key.(string); ok { 41 | dst = enc.AppendKey(dst, key) 42 | } else { 43 | continue 44 | } 45 | if val, ok := val.(LogObjectMarshaler); ok { 46 | e := newEvent(nil, 0) 47 | e.buf = e.buf[:0] 48 | e.appendObject(val) 49 | dst = append(dst, e.buf...) 50 | putEvent(e) 51 | continue 52 | } 53 | switch val := val.(type) { 54 | case string: 55 | dst = enc.AppendString(dst, val) 56 | case []byte: 57 | dst = enc.AppendBytes(dst, val) 58 | case error: 59 | switch m := ErrorMarshalFunc(val).(type) { 60 | case LogObjectMarshaler: 61 | e := newEvent(nil, 0) 62 | e.buf = e.buf[:0] 63 | e.appendObject(m) 64 | dst = append(dst, e.buf...) 65 | putEvent(e) 66 | case error: 67 | if m == nil || isNilValue(m) { 68 | dst = enc.AppendNil(dst) 69 | } else { 70 | dst = enc.AppendString(dst, m.Error()) 71 | } 72 | case string: 73 | dst = enc.AppendString(dst, m) 74 | default: 75 | dst = enc.AppendInterface(dst, m) 76 | } 77 | 78 | if stack && ErrorStackMarshaler != nil { 79 | dst = enc.AppendKey(dst, ErrorStackFieldName) 80 | switch m := ErrorStackMarshaler(val).(type) { 81 | case nil: 82 | case error: 83 | if m != nil && !isNilValue(m) { 84 | dst = enc.AppendString(dst, m.Error()) 85 | } 86 | case string: 87 | dst = enc.AppendString(dst, m) 88 | default: 89 | dst = enc.AppendInterface(dst, m) 90 | } 91 | } 92 | case []error: 93 | dst = enc.AppendArrayStart(dst) 94 | for i, err := range val { 95 | switch m := ErrorMarshalFunc(err).(type) { 96 | case LogObjectMarshaler: 97 | e := newEvent(nil, 0) 98 | e.buf = e.buf[:0] 99 | e.appendObject(m) 100 | dst = append(dst, e.buf...) 101 | putEvent(e) 102 | case error: 103 | if m == nil || isNilValue(m) { 104 | dst = enc.AppendNil(dst) 105 | } else { 106 | dst = enc.AppendString(dst, m.Error()) 107 | } 108 | case string: 109 | dst = enc.AppendString(dst, m) 110 | default: 111 | dst = enc.AppendInterface(dst, m) 112 | } 113 | 114 | if i < (len(val) - 1) { 115 | enc.AppendArrayDelim(dst) 116 | } 117 | } 118 | dst = enc.AppendArrayEnd(dst) 119 | case bool: 120 | dst = enc.AppendBool(dst, val) 121 | case int: 122 | dst = enc.AppendInt(dst, val) 123 | case int8: 124 | dst = enc.AppendInt8(dst, val) 125 | case int16: 126 | dst = enc.AppendInt16(dst, val) 127 | case int32: 128 | dst = enc.AppendInt32(dst, val) 129 | case int64: 130 | dst = enc.AppendInt64(dst, val) 131 | case uint: 132 | dst = enc.AppendUint(dst, val) 133 | case uint8: 134 | dst = enc.AppendUint8(dst, val) 135 | case uint16: 136 | dst = enc.AppendUint16(dst, val) 137 | case uint32: 138 | dst = enc.AppendUint32(dst, val) 139 | case uint64: 140 | dst = enc.AppendUint64(dst, val) 141 | case float32: 142 | dst = enc.AppendFloat32(dst, val, FloatingPointPrecision) 143 | case float64: 144 | dst = enc.AppendFloat64(dst, val, FloatingPointPrecision) 145 | case time.Time: 146 | dst = enc.AppendTime(dst, val, TimeFieldFormat) 147 | case time.Duration: 148 | dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision) 149 | case *string: 150 | if val != nil { 151 | dst = enc.AppendString(dst, *val) 152 | } else { 153 | dst = enc.AppendNil(dst) 154 | } 155 | case *bool: 156 | if val != nil { 157 | dst = enc.AppendBool(dst, *val) 158 | } else { 159 | dst = enc.AppendNil(dst) 160 | } 161 | case *int: 162 | if val != nil { 163 | dst = enc.AppendInt(dst, *val) 164 | } else { 165 | dst = enc.AppendNil(dst) 166 | } 167 | case *int8: 168 | if val != nil { 169 | dst = enc.AppendInt8(dst, *val) 170 | } else { 171 | dst = enc.AppendNil(dst) 172 | } 173 | case *int16: 174 | if val != nil { 175 | dst = enc.AppendInt16(dst, *val) 176 | } else { 177 | dst = enc.AppendNil(dst) 178 | } 179 | case *int32: 180 | if val != nil { 181 | dst = enc.AppendInt32(dst, *val) 182 | } else { 183 | dst = enc.AppendNil(dst) 184 | } 185 | case *int64: 186 | if val != nil { 187 | dst = enc.AppendInt64(dst, *val) 188 | } else { 189 | dst = enc.AppendNil(dst) 190 | } 191 | case *uint: 192 | if val != nil { 193 | dst = enc.AppendUint(dst, *val) 194 | } else { 195 | dst = enc.AppendNil(dst) 196 | } 197 | case *uint8: 198 | if val != nil { 199 | dst = enc.AppendUint8(dst, *val) 200 | } else { 201 | dst = enc.AppendNil(dst) 202 | } 203 | case *uint16: 204 | if val != nil { 205 | dst = enc.AppendUint16(dst, *val) 206 | } else { 207 | dst = enc.AppendNil(dst) 208 | } 209 | case *uint32: 210 | if val != nil { 211 | dst = enc.AppendUint32(dst, *val) 212 | } else { 213 | dst = enc.AppendNil(dst) 214 | } 215 | case *uint64: 216 | if val != nil { 217 | dst = enc.AppendUint64(dst, *val) 218 | } else { 219 | dst = enc.AppendNil(dst) 220 | } 221 | case *float32: 222 | if val != nil { 223 | dst = enc.AppendFloat32(dst, *val, FloatingPointPrecision) 224 | } else { 225 | dst = enc.AppendNil(dst) 226 | } 227 | case *float64: 228 | if val != nil { 229 | dst = enc.AppendFloat64(dst, *val, FloatingPointPrecision) 230 | } else { 231 | dst = enc.AppendNil(dst) 232 | } 233 | case *time.Time: 234 | if val != nil { 235 | dst = enc.AppendTime(dst, *val, TimeFieldFormat) 236 | } else { 237 | dst = enc.AppendNil(dst) 238 | } 239 | case *time.Duration: 240 | if val != nil { 241 | dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision) 242 | } else { 243 | dst = enc.AppendNil(dst) 244 | } 245 | case []string: 246 | dst = enc.AppendStrings(dst, val) 247 | case []bool: 248 | dst = enc.AppendBools(dst, val) 249 | case []int: 250 | dst = enc.AppendInts(dst, val) 251 | case []int8: 252 | dst = enc.AppendInts8(dst, val) 253 | case []int16: 254 | dst = enc.AppendInts16(dst, val) 255 | case []int32: 256 | dst = enc.AppendInts32(dst, val) 257 | case []int64: 258 | dst = enc.AppendInts64(dst, val) 259 | case []uint: 260 | dst = enc.AppendUints(dst, val) 261 | // case []uint8: 262 | // dst = enc.AppendUints8(dst, val) 263 | case []uint16: 264 | dst = enc.AppendUints16(dst, val) 265 | case []uint32: 266 | dst = enc.AppendUints32(dst, val) 267 | case []uint64: 268 | dst = enc.AppendUints64(dst, val) 269 | case []float32: 270 | dst = enc.AppendFloats32(dst, val, FloatingPointPrecision) 271 | case []float64: 272 | dst = enc.AppendFloats64(dst, val, FloatingPointPrecision) 273 | case []time.Time: 274 | dst = enc.AppendTimes(dst, val, TimeFieldFormat) 275 | case []time.Duration: 276 | dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision) 277 | case nil: 278 | dst = enc.AppendNil(dst) 279 | case net.IP: 280 | dst = enc.AppendIPAddr(dst, val) 281 | case net.IPNet: 282 | dst = enc.AppendIPPrefix(dst, val) 283 | case net.HardwareAddr: 284 | dst = enc.AppendMACAddr(dst, val) 285 | case json.RawMessage: 286 | dst = appendJSON(dst, val) 287 | default: 288 | dst = enc.AppendInterface(dst, val) 289 | } 290 | } 291 | return dst 292 | } 293 | -------------------------------------------------------------------------------- /globals.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "strconv" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | const ( 12 | // TimeFormatUnix defines a time format that makes time fields to be 13 | // serialized as Unix timestamp integers. 14 | TimeFormatUnix = "" 15 | 16 | // TimeFormatUnixMs defines a time format that makes time fields to be 17 | // serialized as Unix timestamp integers in milliseconds. 18 | TimeFormatUnixMs = "UNIXMS" 19 | 20 | // TimeFormatUnixMicro defines a time format that makes time fields to be 21 | // serialized as Unix timestamp integers in microseconds. 22 | TimeFormatUnixMicro = "UNIXMICRO" 23 | 24 | // TimeFormatUnixNano defines a time format that makes time fields to be 25 | // serialized as Unix timestamp integers in nanoseconds. 26 | TimeFormatUnixNano = "UNIXNANO" 27 | ) 28 | 29 | var ( 30 | // TimestampFieldName is the field name used for the timestamp field. 31 | TimestampFieldName = "time" 32 | 33 | // LevelFieldName is the field name used for the level field. 34 | LevelFieldName = "level" 35 | 36 | // LevelTraceValue is the value used for the trace level field. 37 | LevelTraceValue = "trace" 38 | // LevelDebugValue is the value used for the debug level field. 39 | LevelDebugValue = "debug" 40 | // LevelInfoValue is the value used for the info level field. 41 | LevelInfoValue = "info" 42 | // LevelWarnValue is the value used for the warn level field. 43 | LevelWarnValue = "warn" 44 | // LevelErrorValue is the value used for the error level field. 45 | LevelErrorValue = "error" 46 | // LevelFatalValue is the value used for the fatal level field. 47 | LevelFatalValue = "fatal" 48 | // LevelPanicValue is the value used for the panic level field. 49 | LevelPanicValue = "panic" 50 | 51 | // LevelFieldMarshalFunc allows customization of global level field marshaling. 52 | LevelFieldMarshalFunc = func(l Level) string { 53 | return l.String() 54 | } 55 | 56 | // MessageFieldName is the field name used for the message field. 57 | MessageFieldName = "message" 58 | 59 | // ErrorFieldName is the field name used for error fields. 60 | ErrorFieldName = "error" 61 | 62 | // CallerFieldName is the field name used for caller field. 63 | CallerFieldName = "caller" 64 | 65 | // CallerSkipFrameCount is the number of stack frames to skip to find the caller. 66 | CallerSkipFrameCount = 2 67 | 68 | // CallerMarshalFunc allows customization of global caller marshaling 69 | CallerMarshalFunc = func(pc uintptr, file string, line int) string { 70 | return file + ":" + strconv.Itoa(line) 71 | } 72 | 73 | // ErrorStackFieldName is the field name used for error stacks. 74 | ErrorStackFieldName = "stack" 75 | 76 | // ErrorStackMarshaler extract the stack from err if any. 77 | ErrorStackMarshaler func(err error) interface{} 78 | 79 | // ErrorMarshalFunc allows customization of global error marshaling 80 | ErrorMarshalFunc = func(err error) interface{} { 81 | return err 82 | } 83 | 84 | // InterfaceMarshalFunc allows customization of interface marshaling. 85 | // Default: "encoding/json.Marshal" with disabled HTML escaping 86 | InterfaceMarshalFunc = func(v interface{}) ([]byte, error) { 87 | var buf bytes.Buffer 88 | encoder := json.NewEncoder(&buf) 89 | encoder.SetEscapeHTML(false) 90 | err := encoder.Encode(v) 91 | if err != nil { 92 | return nil, err 93 | } 94 | b := buf.Bytes() 95 | if len(b) > 0 { 96 | // Remove trailing \n which is added by Encode. 97 | return b[:len(b)-1], nil 98 | } 99 | return b, nil 100 | } 101 | 102 | // TimeFieldFormat defines the time format of the Time field type. If set to 103 | // TimeFormatUnix, TimeFormatUnixMs, TimeFormatUnixMicro or TimeFormatUnixNano, the time is formatted as a UNIX 104 | // timestamp as integer. 105 | TimeFieldFormat = time.RFC3339 106 | 107 | // TimestampFunc defines the function called to generate a timestamp. 108 | TimestampFunc = time.Now 109 | 110 | // DurationFieldUnit defines the unit for time.Duration type fields added 111 | // using the Dur method. 112 | DurationFieldUnit = time.Millisecond 113 | 114 | // DurationFieldInteger renders Dur fields as integer instead of float if 115 | // set to true. 116 | DurationFieldInteger = false 117 | 118 | // ErrorHandler is called whenever zerolog fails to write an event on its 119 | // output. If not set, an error is printed on the stderr. This handler must 120 | // be thread safe and non-blocking. 121 | ErrorHandler func(err error) 122 | 123 | // DefaultContextLogger is returned from Ctx() if there is no logger associated 124 | // with the context. 125 | DefaultContextLogger *Logger 126 | 127 | // LevelColors are used by ConsoleWriter's consoleDefaultFormatLevel to color 128 | // log levels. 129 | LevelColors = map[Level]int{ 130 | TraceLevel: colorBlue, 131 | DebugLevel: 0, 132 | InfoLevel: colorGreen, 133 | WarnLevel: colorYellow, 134 | ErrorLevel: colorRed, 135 | FatalLevel: colorRed, 136 | PanicLevel: colorRed, 137 | } 138 | 139 | // FormattedLevels are used by ConsoleWriter's consoleDefaultFormatLevel 140 | // for a short level name. 141 | FormattedLevels = map[Level]string{ 142 | TraceLevel: "TRC", 143 | DebugLevel: "DBG", 144 | InfoLevel: "INF", 145 | WarnLevel: "WRN", 146 | ErrorLevel: "ERR", 147 | FatalLevel: "FTL", 148 | PanicLevel: "PNC", 149 | } 150 | 151 | // TriggerLevelWriterBufferReuseLimit is a limit in bytes that a buffer is dropped 152 | // from the TriggerLevelWriter buffer pool if the buffer grows above the limit. 153 | TriggerLevelWriterBufferReuseLimit = 64 * 1024 154 | 155 | // FloatingPointPrecision, if set to a value other than -1, controls the number 156 | // of digits when formatting float numbers in JSON. See strconv.FormatFloat for 157 | // more details. 158 | FloatingPointPrecision = -1 159 | ) 160 | 161 | var ( 162 | gLevel = new(int32) 163 | disableSampling = new(int32) 164 | ) 165 | 166 | // SetGlobalLevel sets the global override for log level. If this 167 | // values is raised, all Loggers will use at least this value. 168 | // 169 | // To globally disable logs, set GlobalLevel to Disabled. 170 | func SetGlobalLevel(l Level) { 171 | atomic.StoreInt32(gLevel, int32(l)) 172 | } 173 | 174 | // GlobalLevel returns the current global log level 175 | func GlobalLevel() Level { 176 | return Level(atomic.LoadInt32(gLevel)) 177 | } 178 | 179 | // DisableSampling will disable sampling in all Loggers if true. 180 | func DisableSampling(v bool) { 181 | var i int32 182 | if v { 183 | i = 1 184 | } 185 | atomic.StoreInt32(disableSampling, i) 186 | } 187 | 188 | func samplingDisabled() bool { 189 | return atomic.LoadInt32(disableSampling) == 1 190 | } 191 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rs/zerolog 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/coreos/go-systemd/v22 v22.5.0 7 | github.com/mattn/go-colorable v0.1.13 8 | github.com/mattn/go-isatty v0.0.19 // indirect 9 | github.com/pkg/errors v0.9.1 10 | github.com/rs/xid v1.6.0 11 | golang.org/x/sys v0.12.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 2 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 3 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 4 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 5 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 6 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 7 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 8 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 12 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 13 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 16 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | -------------------------------------------------------------------------------- /go112.go: -------------------------------------------------------------------------------- 1 | // +build go1.12 2 | 3 | package zerolog 4 | 5 | // Since go 1.12, some auto generated init functions are hidden from 6 | // runtime.Caller. 7 | const contextCallerSkipFrameCount = 2 8 | -------------------------------------------------------------------------------- /hlog/hlog_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | 3 | package hlog_test 4 | 5 | import ( 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "net/http/httptest" 11 | 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/hlog" 14 | ) 15 | 16 | // fake alice to avoid dep 17 | type middleware func(http.Handler) http.Handler 18 | type alice struct { 19 | m []middleware 20 | } 21 | 22 | func (a alice) Append(m middleware) alice { 23 | a.m = append(a.m, m) 24 | return a 25 | } 26 | func (a alice) Then(h http.Handler) http.Handler { 27 | for i := range a.m { 28 | h = a.m[len(a.m)-1-i](h) 29 | } 30 | return h 31 | } 32 | 33 | func init() { 34 | zerolog.TimestampFunc = func() time.Time { 35 | return time.Date(2001, time.February, 3, 4, 5, 6, 7, time.UTC) 36 | } 37 | } 38 | 39 | func Example_handler() { 40 | log := zerolog.New(os.Stdout).With(). 41 | Timestamp(). 42 | Str("role", "my-service"). 43 | Str("host", "local-hostname"). 44 | Logger() 45 | 46 | c := alice{} 47 | 48 | // Install the logger handler with default output on the console 49 | c = c.Append(hlog.NewHandler(log)) 50 | 51 | // Install some provided extra handlers to set some request's context fields. 52 | // Thanks to those handlers, all our logs will come with some pre-populated fields. 53 | c = c.Append(hlog.RemoteAddrHandler("ip")) 54 | c = c.Append(hlog.UserAgentHandler("user_agent")) 55 | c = c.Append(hlog.RefererHandler("referer")) 56 | //c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) 57 | 58 | // Here is your final handler 59 | h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 | // Get the logger from the request's context. You can safely assume it 61 | // will be always there: if the handler is removed, hlog.FromRequest 62 | // will return a no-op logger. 63 | hlog.FromRequest(r).Info(). 64 | Str("user", "current user"). 65 | Str("status", "ok"). 66 | Msg("Something happened") 67 | })) 68 | http.Handle("/", h) 69 | 70 | h.ServeHTTP(httptest.NewRecorder(), &http.Request{}) 71 | 72 | // Output: {"level":"info","role":"my-service","host":"local-hostname","user":"current user","status":"ok","time":"2001-02-03T04:05:06Z","message":"Something happened"} 73 | } 74 | -------------------------------------------------------------------------------- /hlog/internal/mutil/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, 2015, 2016 Carl Jackson (carl@avtok.com) 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /hlog/internal/mutil/mutil.go: -------------------------------------------------------------------------------- 1 | // Package mutil contains various functions that are helpful when writing http 2 | // middleware. 3 | // 4 | // It has been vendored from Goji v1.0, with the exception of the code for Go 1.8: 5 | // https://github.com/zenazn/goji/ 6 | package mutil 7 | -------------------------------------------------------------------------------- /hlog/internal/mutil/writer_proxy.go: -------------------------------------------------------------------------------- 1 | package mutil 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "net" 7 | "net/http" 8 | ) 9 | 10 | // WriterProxy is a proxy around an http.ResponseWriter that allows you to hook 11 | // into various parts of the response process. 12 | type WriterProxy interface { 13 | http.ResponseWriter 14 | // Status returns the HTTP status of the request, or 0 if one has not 15 | // yet been sent. 16 | Status() int 17 | // BytesWritten returns the total number of bytes sent to the client. 18 | BytesWritten() int 19 | // Tee causes the response body to be written to the given io.Writer in 20 | // addition to proxying the writes through. Only one io.Writer can be 21 | // tee'd to at once: setting a second one will overwrite the first. 22 | // Writes will be sent to the proxy before being written to this 23 | // io.Writer. It is illegal for the tee'd writer to be modified 24 | // concurrently with writes. 25 | Tee(io.Writer) 26 | // Unwrap returns the original proxied target. 27 | Unwrap() http.ResponseWriter 28 | } 29 | 30 | // WrapWriter wraps an http.ResponseWriter, returning a proxy that allows you to 31 | // hook into various parts of the response process. 32 | func WrapWriter(w http.ResponseWriter) WriterProxy { 33 | _, cn := w.(http.CloseNotifier) 34 | _, fl := w.(http.Flusher) 35 | _, hj := w.(http.Hijacker) 36 | _, rf := w.(io.ReaderFrom) 37 | 38 | bw := basicWriter{ResponseWriter: w} 39 | if cn && fl && hj && rf { 40 | return &fancyWriter{bw} 41 | } 42 | if fl { 43 | return &flushWriter{bw} 44 | } 45 | return &bw 46 | } 47 | 48 | // basicWriter wraps a http.ResponseWriter that implements the minimal 49 | // http.ResponseWriter interface. 50 | type basicWriter struct { 51 | http.ResponseWriter 52 | wroteHeader bool 53 | code int 54 | bytes int 55 | tee io.Writer 56 | } 57 | 58 | func (b *basicWriter) WriteHeader(code int) { 59 | if !b.wroteHeader { 60 | b.code = code 61 | b.wroteHeader = true 62 | b.ResponseWriter.WriteHeader(code) 63 | } 64 | } 65 | 66 | func (b *basicWriter) Write(buf []byte) (int, error) { 67 | b.WriteHeader(http.StatusOK) 68 | n, err := b.ResponseWriter.Write(buf) 69 | if b.tee != nil { 70 | _, err2 := b.tee.Write(buf[:n]) 71 | // Prefer errors generated by the proxied writer. 72 | if err == nil { 73 | err = err2 74 | } 75 | } 76 | b.bytes += n 77 | return n, err 78 | } 79 | 80 | func (b *basicWriter) maybeWriteHeader() { 81 | if !b.wroteHeader { 82 | b.WriteHeader(http.StatusOK) 83 | } 84 | } 85 | 86 | func (b *basicWriter) Status() int { 87 | return b.code 88 | } 89 | 90 | func (b *basicWriter) BytesWritten() int { 91 | return b.bytes 92 | } 93 | 94 | func (b *basicWriter) Tee(w io.Writer) { 95 | b.tee = w 96 | } 97 | 98 | func (b *basicWriter) Unwrap() http.ResponseWriter { 99 | return b.ResponseWriter 100 | } 101 | 102 | // fancyWriter is a writer that additionally satisfies http.CloseNotifier, 103 | // http.Flusher, http.Hijacker, and io.ReaderFrom. It exists for the common case 104 | // of wrapping the http.ResponseWriter that package http gives you, in order to 105 | // make the proxied object support the full method set of the proxied object. 106 | type fancyWriter struct { 107 | basicWriter 108 | } 109 | 110 | func (f *fancyWriter) CloseNotify() <-chan bool { 111 | cn := f.basicWriter.ResponseWriter.(http.CloseNotifier) 112 | return cn.CloseNotify() 113 | } 114 | 115 | func (f *fancyWriter) Flush() { 116 | fl := f.basicWriter.ResponseWriter.(http.Flusher) 117 | fl.Flush() 118 | } 119 | 120 | func (f *fancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 121 | hj := f.basicWriter.ResponseWriter.(http.Hijacker) 122 | return hj.Hijack() 123 | } 124 | 125 | func (f *fancyWriter) ReadFrom(r io.Reader) (int64, error) { 126 | if f.basicWriter.tee != nil { 127 | n, err := io.Copy(&f.basicWriter, r) 128 | f.bytes += int(n) 129 | return n, err 130 | } 131 | rf := f.basicWriter.ResponseWriter.(io.ReaderFrom) 132 | f.basicWriter.maybeWriteHeader() 133 | 134 | n, err := rf.ReadFrom(r) 135 | f.bytes += int(n) 136 | return n, err 137 | } 138 | 139 | type flushWriter struct { 140 | basicWriter 141 | } 142 | 143 | func (f *flushWriter) Flush() { 144 | fl := f.basicWriter.ResponseWriter.(http.Flusher) 145 | fl.Flush() 146 | } 147 | 148 | var ( 149 | _ http.CloseNotifier = &fancyWriter{} 150 | _ http.Flusher = &fancyWriter{} 151 | _ http.Hijacker = &fancyWriter{} 152 | _ io.ReaderFrom = &fancyWriter{} 153 | _ http.Flusher = &flushWriter{} 154 | ) 155 | -------------------------------------------------------------------------------- /hook.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | // Hook defines an interface to a log hook. 4 | type Hook interface { 5 | // Run runs the hook with the event. 6 | Run(e *Event, level Level, message string) 7 | } 8 | 9 | // HookFunc is an adaptor to allow the use of an ordinary function 10 | // as a Hook. 11 | type HookFunc func(e *Event, level Level, message string) 12 | 13 | // Run implements the Hook interface. 14 | func (h HookFunc) Run(e *Event, level Level, message string) { 15 | h(e, level, message) 16 | } 17 | 18 | // LevelHook applies a different hook for each level. 19 | type LevelHook struct { 20 | NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook 21 | } 22 | 23 | // Run implements the Hook interface. 24 | func (h LevelHook) Run(e *Event, level Level, message string) { 25 | switch level { 26 | case TraceLevel: 27 | if h.TraceHook != nil { 28 | h.TraceHook.Run(e, level, message) 29 | } 30 | case DebugLevel: 31 | if h.DebugHook != nil { 32 | h.DebugHook.Run(e, level, message) 33 | } 34 | case InfoLevel: 35 | if h.InfoHook != nil { 36 | h.InfoHook.Run(e, level, message) 37 | } 38 | case WarnLevel: 39 | if h.WarnHook != nil { 40 | h.WarnHook.Run(e, level, message) 41 | } 42 | case ErrorLevel: 43 | if h.ErrorHook != nil { 44 | h.ErrorHook.Run(e, level, message) 45 | } 46 | case FatalLevel: 47 | if h.FatalHook != nil { 48 | h.FatalHook.Run(e, level, message) 49 | } 50 | case PanicLevel: 51 | if h.PanicHook != nil { 52 | h.PanicHook.Run(e, level, message) 53 | } 54 | case NoLevel: 55 | if h.NoLevelHook != nil { 56 | h.NoLevelHook.Run(e, level, message) 57 | } 58 | } 59 | } 60 | 61 | // NewLevelHook returns a new LevelHook. 62 | func NewLevelHook() LevelHook { 63 | return LevelHook{} 64 | } 65 | -------------------------------------------------------------------------------- /hook_test.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | type contextKeyType int 11 | 12 | var contextKey contextKeyType 13 | 14 | var ( 15 | levelNameHook = HookFunc(func(e *Event, level Level, msg string) { 16 | levelName := level.String() 17 | if level == NoLevel { 18 | levelName = "nolevel" 19 | } 20 | e.Str("level_name", levelName) 21 | }) 22 | simpleHook = HookFunc(func(e *Event, level Level, msg string) { 23 | e.Bool("has_level", level != NoLevel) 24 | e.Str("test", "logged") 25 | }) 26 | copyHook = HookFunc(func(e *Event, level Level, msg string) { 27 | hasLevel := level != NoLevel 28 | e.Bool("copy_has_level", hasLevel) 29 | if hasLevel { 30 | e.Str("copy_level", level.String()) 31 | } 32 | e.Str("copy_msg", msg) 33 | }) 34 | nopHook = HookFunc(func(e *Event, level Level, message string) { 35 | }) 36 | discardHook = HookFunc(func(e *Event, level Level, message string) { 37 | e.Discard() 38 | }) 39 | contextHook = HookFunc(func(e *Event, level Level, message string) { 40 | contextData, ok := e.GetCtx().Value(contextKey).(string) 41 | if ok { 42 | e.Str("context-data", contextData) 43 | } 44 | }) 45 | ) 46 | 47 | func TestHook(t *testing.T) { 48 | tests := []struct { 49 | name string 50 | want string 51 | test func(log Logger) 52 | }{ 53 | {"Message", `{"level_name":"nolevel","message":"test message"}` + "\n", func(log Logger) { 54 | log = log.Hook(levelNameHook) 55 | log.Log().Msg("test message") 56 | }}, 57 | {"NoLevel", `{"level_name":"nolevel"}` + "\n", func(log Logger) { 58 | log = log.Hook(levelNameHook) 59 | log.Log().Msg("") 60 | }}, 61 | {"Print", `{"level":"debug","level_name":"debug"}` + "\n", func(log Logger) { 62 | log = log.Hook(levelNameHook) 63 | log.Print("") 64 | }}, 65 | {"Error", `{"level":"error","level_name":"error"}` + "\n", func(log Logger) { 66 | log = log.Hook(levelNameHook) 67 | log.Error().Msg("") 68 | }}, 69 | {"Copy/1", `{"copy_has_level":false,"copy_msg":""}` + "\n", func(log Logger) { 70 | log = log.Hook(copyHook) 71 | log.Log().Msg("") 72 | }}, 73 | {"Copy/2", `{"level":"info","copy_has_level":true,"copy_level":"info","copy_msg":"a message","message":"a message"}` + "\n", func(log Logger) { 74 | log = log.Hook(copyHook) 75 | log.Info().Msg("a message") 76 | }}, 77 | {"Multi", `{"level":"error","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 78 | log = log.Hook(levelNameHook).Hook(simpleHook) 79 | log.Error().Msg("") 80 | }}, 81 | {"Multi/Message", `{"level":"error","level_name":"error","has_level":true,"test":"logged","message":"a message"}` + "\n", func(log Logger) { 82 | log = log.Hook(levelNameHook).Hook(simpleHook) 83 | log.Error().Msg("a message") 84 | }}, 85 | {"Output/single/pre", `{"level":"error","level_name":"error"}` + "\n", func(log Logger) { 86 | ignored := &bytes.Buffer{} 87 | log = New(ignored).Hook(levelNameHook).Output(log.w) 88 | log.Error().Msg("") 89 | }}, 90 | {"Output/single/post", `{"level":"error","level_name":"error"}` + "\n", func(log Logger) { 91 | ignored := &bytes.Buffer{} 92 | log = New(ignored).Output(log.w).Hook(levelNameHook) 93 | log.Error().Msg("") 94 | }}, 95 | {"Output/multi/pre", `{"level":"error","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 96 | ignored := &bytes.Buffer{} 97 | log = New(ignored).Hook(levelNameHook).Hook(simpleHook).Output(log.w) 98 | log.Error().Msg("") 99 | }}, 100 | {"Output/multi/post", `{"level":"error","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 101 | ignored := &bytes.Buffer{} 102 | log = New(ignored).Output(log.w).Hook(levelNameHook).Hook(simpleHook) 103 | log.Error().Msg("") 104 | }}, 105 | {"Output/mixed", `{"level":"error","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 106 | ignored := &bytes.Buffer{} 107 | log = New(ignored).Hook(levelNameHook).Output(log.w).Hook(simpleHook) 108 | log.Error().Msg("") 109 | }}, 110 | {"With/single/pre", `{"level":"error","with":"pre","level_name":"error"}` + "\n", func(log Logger) { 111 | log = log.Hook(levelNameHook).With().Str("with", "pre").Logger() 112 | log.Error().Msg("") 113 | }}, 114 | {"With/single/post", `{"level":"error","with":"post","level_name":"error"}` + "\n", func(log Logger) { 115 | log = log.With().Str("with", "post").Logger().Hook(levelNameHook) 116 | log.Error().Msg("") 117 | }}, 118 | {"With/multi/pre", `{"level":"error","with":"pre","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 119 | log = log.Hook(levelNameHook).Hook(simpleHook).With().Str("with", "pre").Logger() 120 | log.Error().Msg("") 121 | }}, 122 | {"With/multi/post", `{"level":"error","with":"post","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 123 | log = log.With().Str("with", "post").Logger().Hook(levelNameHook).Hook(simpleHook) 124 | log.Error().Msg("") 125 | }}, 126 | {"With/mixed", `{"level":"error","with":"mixed","level_name":"error","has_level":true,"test":"logged"}` + "\n", func(log Logger) { 127 | log = log.Hook(levelNameHook).With().Str("with", "mixed").Logger().Hook(simpleHook) 128 | log.Error().Msg("") 129 | }}, 130 | {"Discard", "", func(log Logger) { 131 | log = log.Hook(discardHook) 132 | log.Log().Msg("test message") 133 | }}, 134 | {"Context/Background", `{"level":"info","message":"test message"}` + "\n", func(log Logger) { 135 | log = log.Hook(contextHook) 136 | log.Info().Ctx(context.Background()).Msg("test message") 137 | }}, 138 | {"Context/nil", `{"level":"info","message":"test message"}` + "\n", func(log Logger) { 139 | // passing `nil` where a context is wanted is against 140 | // the rules, but people still do it. 141 | log = log.Hook(contextHook) 142 | log.Info().Ctx(nil).Msg("test message") // nolint 143 | }}, 144 | {"Context/valid", `{"level":"info","context-data":"12345abcdef","message":"test message"}` + "\n", func(log Logger) { 145 | ctx := context.Background() 146 | ctx = context.WithValue(ctx, contextKey, "12345abcdef") 147 | log = log.Hook(contextHook) 148 | log.Info().Ctx(ctx).Msg("test message") 149 | }}, 150 | {"Context/With/valid", `{"level":"info","context-data":"12345abcdef","message":"test message"}` + "\n", func(log Logger) { 151 | ctx := context.Background() 152 | ctx = context.WithValue(ctx, contextKey, "12345abcdef") 153 | log = log.Hook(contextHook) 154 | log = log.With().Ctx(ctx).Logger() 155 | log.Info().Msg("test message") 156 | }}, 157 | {"None", `{"level":"error"}` + "\n", func(log Logger) { 158 | log.Error().Msg("") 159 | }}, 160 | } 161 | for _, tt := range tests { 162 | tt := tt 163 | t.Run(tt.name, func(t *testing.T) { 164 | out := &bytes.Buffer{} 165 | log := New(out) 166 | tt.test(log) 167 | if got, want := decodeIfBinaryToString(out.Bytes()), tt.want; got != want { 168 | t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) 169 | } 170 | }) 171 | } 172 | } 173 | 174 | func BenchmarkHooks(b *testing.B) { 175 | logger := New(io.Discard) 176 | b.ResetTimer() 177 | b.Run("Nop/Single", func(b *testing.B) { 178 | log := logger.Hook(nopHook) 179 | b.RunParallel(func(pb *testing.PB) { 180 | for pb.Next() { 181 | log.Log().Msg("") 182 | } 183 | }) 184 | }) 185 | b.Run("Nop/Multi", func(b *testing.B) { 186 | log := logger.Hook(nopHook).Hook(nopHook) 187 | b.RunParallel(func(pb *testing.PB) { 188 | for pb.Next() { 189 | log.Log().Msg("") 190 | } 191 | }) 192 | }) 193 | b.Run("Simple", func(b *testing.B) { 194 | log := logger.Hook(simpleHook) 195 | b.RunParallel(func(pb *testing.PB) { 196 | for pb.Next() { 197 | log.Log().Msg("") 198 | } 199 | }) 200 | }) 201 | } 202 | -------------------------------------------------------------------------------- /internal/cbor/README.md: -------------------------------------------------------------------------------- 1 | ## Reference: 2 | CBOR Encoding is described in [RFC7049](https://tools.ietf.org/html/rfc7049) 3 | 4 | ## Comparison of JSON vs CBOR 5 | 6 | Two main areas of reduction are: 7 | 8 | 1. CPU usage to write a log msg 9 | 2. Size (in bytes) of log messages. 10 | 11 | 12 | CPU Usage savings are below: 13 | ``` 14 | name JSON time/op CBOR time/op delta 15 | Info-32 15.3ns ± 1% 11.7ns ± 3% -23.78% (p=0.000 n=9+10) 16 | ContextFields-32 16.2ns ± 2% 12.3ns ± 3% -23.97% (p=0.000 n=9+9) 17 | ContextAppend-32 6.70ns ± 0% 6.20ns ± 0% -7.44% (p=0.000 n=9+9) 18 | LogFields-32 66.4ns ± 0% 24.6ns ± 2% -62.89% (p=0.000 n=10+9) 19 | LogArrayObject-32 911ns ±11% 768ns ± 6% -15.64% (p=0.000 n=10+10) 20 | LogFieldType/Floats-32 70.3ns ± 2% 29.5ns ± 1% -57.98% (p=0.000 n=10+10) 21 | LogFieldType/Err-32 14.0ns ± 3% 12.1ns ± 8% -13.20% (p=0.000 n=8+10) 22 | LogFieldType/Dur-32 17.2ns ± 2% 13.1ns ± 1% -24.27% (p=0.000 n=10+9) 23 | LogFieldType/Object-32 54.3ns ±11% 52.3ns ± 7% ~ (p=0.239 n=10+10) 24 | LogFieldType/Ints-32 20.3ns ± 2% 15.1ns ± 2% -25.50% (p=0.000 n=9+10) 25 | LogFieldType/Interfaces-32 642ns ±11% 621ns ± 9% ~ (p=0.118 n=10+10) 26 | LogFieldType/Interface(Objects)-32 635ns ±13% 632ns ± 9% ~ (p=0.592 n=10+10) 27 | LogFieldType/Times-32 294ns ± 0% 27ns ± 1% -90.71% (p=0.000 n=10+9) 28 | LogFieldType/Durs-32 121ns ± 0% 33ns ± 2% -72.44% (p=0.000 n=9+9) 29 | LogFieldType/Interface(Object)-32 56.6ns ± 8% 52.3ns ± 8% -7.54% (p=0.007 n=10+10) 30 | LogFieldType/Errs-32 17.8ns ± 3% 16.1ns ± 2% -9.71% (p=0.000 n=10+9) 31 | LogFieldType/Time-32 40.5ns ± 1% 12.7ns ± 6% -68.66% (p=0.000 n=8+9) 32 | LogFieldType/Bool-32 12.0ns ± 5% 10.2ns ± 2% -15.18% (p=0.000 n=10+8) 33 | LogFieldType/Bools-32 17.2ns ± 2% 12.6ns ± 4% -26.63% (p=0.000 n=10+10) 34 | LogFieldType/Int-32 12.3ns ± 2% 11.2ns ± 4% -9.27% (p=0.000 n=9+10) 35 | LogFieldType/Float-32 16.7ns ± 1% 12.6ns ± 2% -24.42% (p=0.000 n=7+9) 36 | LogFieldType/Str-32 12.7ns ± 7% 11.3ns ± 7% -10.88% (p=0.000 n=10+9) 37 | LogFieldType/Strs-32 20.3ns ± 3% 18.2ns ± 3% -10.25% (p=0.000 n=9+10) 38 | LogFieldType/Interface-32 183ns ±12% 175ns ± 9% ~ (p=0.078 n=10+10) 39 | ``` 40 | 41 | Log message size savings is greatly dependent on the number and type of fields in the log message. 42 | Assuming this log message (with an Integer, timestamp and string, in addition to level). 43 | 44 | `{"level":"error","Fault":41650,"time":"2018-04-01T15:18:19-07:00","message":"Some Message"}` 45 | 46 | Two measurements were done for the log file sizes - one without any compression, second 47 | using [compress/zlib](https://golang.org/pkg/compress/zlib/). 48 | 49 | Results for 10,000 log messages: 50 | 51 | | Log Format | Plain File Size (in KB) | Compressed File Size (in KB) | 52 | | :--- | :---: | :---: | 53 | | JSON | 920 | 28 | 54 | | CBOR | 550 | 28 | 55 | 56 | The example used to calculate the above data is available in [Examples](examples). 57 | -------------------------------------------------------------------------------- /internal/cbor/base.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | // JSONMarshalFunc is used to marshal interface to JSON encoded byte slice. 4 | // Making it package level instead of embedded in Encoder brings 5 | // some extra efforts at importing, but avoids value copy when the functions 6 | // of Encoder being invoked. 7 | // DO REMEMBER to set this variable at importing, or 8 | // you might get a nil pointer dereference panic at runtime. 9 | var JSONMarshalFunc func(v interface{}) ([]byte, error) 10 | 11 | type Encoder struct{} 12 | 13 | // AppendKey adds a key (string) to the binary encoded log message 14 | func (e Encoder) AppendKey(dst []byte, key string) []byte { 15 | if len(dst) < 1 { 16 | dst = e.AppendBeginMarker(dst) 17 | } 18 | return e.AppendString(dst, key) 19 | } 20 | -------------------------------------------------------------------------------- /internal/cbor/cbor.go: -------------------------------------------------------------------------------- 1 | // Package cbor provides primitives for storing different data 2 | // in the CBOR (binary) format. CBOR is defined in RFC7049. 3 | package cbor 4 | 5 | import "time" 6 | 7 | const ( 8 | majorOffset = 5 9 | additionalMax = 23 10 | 11 | // Non Values. 12 | additionalTypeBoolFalse byte = 20 13 | additionalTypeBoolTrue byte = 21 14 | additionalTypeNull byte = 22 15 | 16 | // Integer (+ve and -ve) Sub-types. 17 | additionalTypeIntUint8 byte = 24 18 | additionalTypeIntUint16 byte = 25 19 | additionalTypeIntUint32 byte = 26 20 | additionalTypeIntUint64 byte = 27 21 | 22 | // Float Sub-types. 23 | additionalTypeFloat16 byte = 25 24 | additionalTypeFloat32 byte = 26 25 | additionalTypeFloat64 byte = 27 26 | additionalTypeBreak byte = 31 27 | 28 | // Tag Sub-types. 29 | additionalTypeTimestamp byte = 01 30 | additionalTypeEmbeddedCBOR byte = 63 31 | 32 | // Extended Tags - from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml 33 | additionalTypeTagNetworkAddr uint16 = 260 34 | additionalTypeTagNetworkPrefix uint16 = 261 35 | additionalTypeEmbeddedJSON uint16 = 262 36 | additionalTypeTagHexString uint16 = 263 37 | 38 | // Unspecified number of elements. 39 | additionalTypeInfiniteCount byte = 31 40 | ) 41 | const ( 42 | majorTypeUnsignedInt byte = iota << majorOffset // Major type 0 43 | majorTypeNegativeInt // Major type 1 44 | majorTypeByteString // Major type 2 45 | majorTypeUtf8String // Major type 3 46 | majorTypeArray // Major type 4 47 | majorTypeMap // Major type 5 48 | majorTypeTags // Major type 6 49 | majorTypeSimpleAndFloat // Major type 7 50 | ) 51 | 52 | const ( 53 | maskOutAdditionalType byte = (7 << majorOffset) 54 | maskOutMajorType byte = 31 55 | ) 56 | 57 | const ( 58 | float32Nan = "\xfa\x7f\xc0\x00\x00" 59 | float32PosInfinity = "\xfa\x7f\x80\x00\x00" 60 | float32NegInfinity = "\xfa\xff\x80\x00\x00" 61 | float64Nan = "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00" 62 | float64PosInfinity = "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00" 63 | float64NegInfinity = "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00" 64 | ) 65 | 66 | // IntegerTimeFieldFormat indicates the format of timestamp decoded 67 | // from an integer (time in seconds). 68 | var IntegerTimeFieldFormat = time.RFC3339 69 | 70 | // NanoTimeFieldFormat indicates the format of timestamp decoded 71 | // from a float value (time in seconds and nanoseconds). 72 | var NanoTimeFieldFormat = time.RFC3339Nano 73 | 74 | func appendCborTypePrefix(dst []byte, major byte, number uint64) []byte { 75 | byteCount := 8 76 | var minor byte 77 | switch { 78 | case number < 256: 79 | byteCount = 1 80 | minor = additionalTypeIntUint8 81 | 82 | case number < 65536: 83 | byteCount = 2 84 | minor = additionalTypeIntUint16 85 | 86 | case number < 4294967296: 87 | byteCount = 4 88 | minor = additionalTypeIntUint32 89 | 90 | default: 91 | byteCount = 8 92 | minor = additionalTypeIntUint64 93 | 94 | } 95 | 96 | dst = append(dst, major|minor) 97 | byteCount-- 98 | for ; byteCount >= 0; byteCount-- { 99 | dst = append(dst, byte(number>>(uint(byteCount)*8))) 100 | } 101 | return dst 102 | } 103 | -------------------------------------------------------------------------------- /internal/cbor/decoder_test.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDecodeInteger(t *testing.T) { 11 | for _, tc := range integerTestCases { 12 | gotv := decodeInteger(getReader(tc.binary)) 13 | if gotv != int64(tc.val) { 14 | t.Errorf("decodeInteger(0x%s)=0x%d, want: 0x%d", 15 | hex.EncodeToString([]byte(tc.binary)), gotv, tc.val) 16 | } 17 | } 18 | } 19 | 20 | func TestDecodeString(t *testing.T) { 21 | for _, tt := range encodeStringTests { 22 | got := decodeUTF8String(getReader(tt.binary)) 23 | if string(got) != "\""+tt.json+"\"" { 24 | t.Errorf("DecodeString(0x%s)=%s, want:\"%s\"\n", hex.EncodeToString([]byte(tt.binary)), string(got), 25 | hex.EncodeToString([]byte(tt.json))) 26 | } 27 | } 28 | } 29 | 30 | func TestDecodeArray(t *testing.T) { 31 | for _, tc := range integerArrayTestCases { 32 | buf := bytes.NewBuffer([]byte{}) 33 | array2Json(getReader(tc.binary), buf) 34 | if buf.String() != tc.json { 35 | t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.binary)), buf.String(), tc.json) 36 | } 37 | } 38 | //Unspecified Length Array 39 | var infiniteArrayTestCases = []struct { 40 | in string 41 | out string 42 | }{ 43 | {"\x9f\x20\x00\x18\xc8\x14\xff", "[-1,0,200,20]"}, 44 | {"\x9f\x38\xc7\x29\x18\xc8\x19\x01\x90\xff", "[-200,-10,200,400]"}, 45 | {"\x9f\x01\x02\x03\xff", "[1,2,3]"}, 46 | {"\x9f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x18\x18\x19\xff", 47 | "[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]"}, 48 | } 49 | for _, tc := range infiniteArrayTestCases { 50 | buf := bytes.NewBuffer([]byte{}) 51 | array2Json(getReader(tc.in), buf) 52 | if buf.String() != tc.out { 53 | t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.out)), buf.String(), tc.out) 54 | } 55 | } 56 | for _, tc := range booleanArrayTestCases { 57 | buf := bytes.NewBuffer([]byte{}) 58 | array2Json(getReader(tc.binary), buf) 59 | if buf.String() != tc.json { 60 | t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.binary)), buf.String(), tc.json) 61 | } 62 | } 63 | //TODO add cases for arrays of other types 64 | } 65 | 66 | var infiniteMapDecodeTestCases = []struct { 67 | bin []byte 68 | json string 69 | }{ 70 | {[]byte("\xbf\x64IETF\x20\xff"), "{\"IETF\":-1}"}, 71 | {[]byte("\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), "{\"Array\":[-1,0,200,20]}"}, 72 | } 73 | 74 | var mapDecodeTestCases = []struct { 75 | bin []byte 76 | json string 77 | }{ 78 | {[]byte("\xa2\x64IETF\x20"), "{\"IETF\":-1}"}, 79 | {[]byte("\xa2\x65Array\x84\x20\x00\x18\xc8\x14"), "{\"Array\":[-1,0,200,20]}"}, 80 | } 81 | 82 | func TestDecodeMap(t *testing.T) { 83 | for _, tc := range mapDecodeTestCases { 84 | buf := bytes.NewBuffer([]byte{}) 85 | map2Json(getReader(string(tc.bin)), buf) 86 | if buf.String() != tc.json { 87 | t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.bin), buf.String(), tc.json) 88 | } 89 | } 90 | for _, tc := range infiniteMapDecodeTestCases { 91 | buf := bytes.NewBuffer([]byte{}) 92 | map2Json(getReader(string(tc.bin)), buf) 93 | if buf.String() != tc.json { 94 | t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.bin), buf.String(), tc.json) 95 | } 96 | } 97 | } 98 | 99 | func TestDecodeBool(t *testing.T) { 100 | for _, tc := range booleanTestCases { 101 | got := decodeSimpleFloat(getReader(tc.binary)) 102 | if string(got) != tc.json { 103 | t.Errorf("decodeSimpleFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), string(got), tc.json) 104 | } 105 | } 106 | } 107 | 108 | func TestDecodeFloat(t *testing.T) { 109 | for _, tc := range float32TestCases { 110 | got, _ := decodeFloat(getReader(tc.binary)) 111 | if got != float64(tc.val) { 112 | t.Errorf("decodeFloat(0x%s)=%f, want:%f", hex.EncodeToString([]byte(tc.binary)), got, tc.val) 113 | } 114 | } 115 | } 116 | 117 | func TestDecodeTimestamp(t *testing.T) { 118 | decodeTimeZone, _ = time.LoadLocation("UTC") 119 | for _, tc := range timeIntegerTestcases { 120 | tm := decodeTagData(getReader(tc.binary)) 121 | if string(tm) != "\""+tc.rfcStr+"\"" { 122 | t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), tm, tc.rfcStr) 123 | } 124 | } 125 | for _, tc := range timeFloatTestcases { 126 | tm := decodeTagData(getReader(tc.out)) 127 | //Since we convert to float and back - it may be slightly off - so 128 | //we cannot check for exact equality instead, we'll check it is 129 | //very close to each other Less than a Microsecond (lets not yet do nanosec) 130 | 131 | got, _ := time.Parse(string(tm), string(tm)) 132 | want, _ := time.Parse(tc.rfcStr, tc.rfcStr) 133 | if got.Sub(want) > time.Microsecond { 134 | t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.out)), tm, tc.rfcStr) 135 | } 136 | } 137 | } 138 | 139 | func TestDecodeNetworkAddr(t *testing.T) { 140 | for _, tc := range ipAddrTestCases { 141 | d1 := decodeTagData(getReader(tc.binary)) 142 | if string(d1) != tc.text { 143 | t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), d1, tc.text) 144 | } 145 | } 146 | } 147 | 148 | func TestDecodeMACAddr(t *testing.T) { 149 | for _, tc := range macAddrTestCases { 150 | d1 := decodeTagData(getReader(tc.binary)) 151 | if string(d1) != tc.text { 152 | t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), d1, tc.text) 153 | } 154 | } 155 | } 156 | 157 | func TestDecodeIPPrefix(t *testing.T) { 158 | for _, tc := range IPPrefixTestCases { 159 | d1 := decodeTagData(getReader(tc.binary)) 160 | if string(d1) != tc.text { 161 | t.Errorf("decodeIPPrefix(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), d1, tc.text) 162 | } 163 | } 164 | } 165 | 166 | var compositeCborTestCases = []struct { 167 | binary []byte 168 | json string 169 | }{ 170 | {[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14\xff\xff"), "{\"IETF\":-1,\"Array\":[-1,0,200,20]}\n"}, 171 | {[]byte("\xbf\x64IETF\x64YES!\x65Array\x9f\x20\x00\x18\xc8\x14\xff\xff"), "{\"IETF\":\"YES!\",\"Array\":[-1,0,200,20]}\n"}, 172 | } 173 | 174 | func TestDecodeCbor2Json(t *testing.T) { 175 | for _, tc := range compositeCborTestCases { 176 | buf := bytes.NewBuffer([]byte{}) 177 | err := Cbor2JsonManyObjects(getReader(string(tc.binary)), buf) 178 | if buf.String() != tc.json || err != nil { 179 | t.Errorf("cbor2JsonManyObjects(0x%s)=%s, want: %s, err:%s", hex.EncodeToString(tc.binary), buf.String(), tc.json, err.Error()) 180 | } 181 | } 182 | } 183 | 184 | var negativeCborTestCases = []struct { 185 | binary []byte 186 | errStr string 187 | }{ 188 | {[]byte("\xb9\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "Tried to Read 18 Bytes.. But hit end of file"}, 189 | {[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "EOF"}, 190 | {[]byte("\xbf\x14IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "Tried to Read 40736 Bytes.. But hit end of file"}, 191 | {[]byte("\xbf\x64IETF"), "EOF"}, 192 | {[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\xff\xff\xff"), "Invalid Additional Type: 31 in decodeSimpleFloat"}, 193 | {[]byte("\xbf\x64IETF\x20\x65Array"), "EOF"}, 194 | {[]byte("\xbf\x64"), "Tried to Read 4 Bytes.. But hit end of file"}, 195 | } 196 | 197 | func TestDecodeNegativeCbor2Json(t *testing.T) { 198 | for _, tc := range negativeCborTestCases { 199 | buf := bytes.NewBuffer([]byte{}) 200 | err := Cbor2JsonManyObjects(getReader(string(tc.binary)), buf) 201 | if err == nil || err.Error() != tc.errStr { 202 | t.Errorf("Expected error got:%s, want:%s", err, tc.errStr) 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /internal/cbor/examples/genLog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/zlib" 5 | "flag" 6 | "io" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/rs/zerolog" 12 | ) 13 | 14 | func writeLog(fname string, count int, useCompress bool) { 15 | opFile := os.Stdout 16 | if fname != "" { 17 | fil, _ := os.Create(fname) 18 | opFile = fil 19 | defer func() { 20 | if err := fil.Close(); err != nil { 21 | log.Fatal(err) 22 | } 23 | }() 24 | } 25 | 26 | var f io.WriteCloser = opFile 27 | if useCompress { 28 | f = zlib.NewWriter(f) 29 | defer func() { 30 | if err := f.Close(); err != nil { 31 | log.Fatal(err) 32 | } 33 | }() 34 | 35 | } 36 | 37 | zerolog.TimestampFunc = func() time.Time { return time.Now().Round(time.Second) } 38 | log := zerolog.New(f).With(). 39 | Timestamp(). 40 | Logger() 41 | for i := 0; i < count; i++ { 42 | log.Error(). 43 | Int("Fault", 41650+i).Msg("Some Message") 44 | } 45 | } 46 | 47 | func main() { 48 | outFile := flag.String("out", "", "Output File to which logs will be written to (WILL overwrite if already present).") 49 | numLogs := flag.Int("num", 10, "Number of log messages to generate.") 50 | doCompress := flag.Bool("compress", false, "Enable inline compressed writer") 51 | 52 | flag.Parse() 53 | 54 | writeLog(*outFile, *numLogs, *doCompress) 55 | } 56 | -------------------------------------------------------------------------------- /internal/cbor/examples/makefile: -------------------------------------------------------------------------------- 1 | all: genLogJSON genLogCBOR 2 | 3 | genLogJSON: genLog.go 4 | go build -o genLogJSON genLog.go 5 | 6 | genLogCBOR: genLog.go 7 | go build -tags binary_log -o genLogCBOR genLog.go 8 | 9 | clean: 10 | rm -f genLogJSON genLogCBOR 11 | -------------------------------------------------------------------------------- /internal/cbor/string.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import "fmt" 4 | 5 | // AppendStrings encodes and adds an array of strings to the dst byte array. 6 | func (e Encoder) AppendStrings(dst []byte, vals []string) []byte { 7 | major := majorTypeArray 8 | l := len(vals) 9 | if l <= additionalMax { 10 | lb := byte(l) 11 | dst = append(dst, major|lb) 12 | } else { 13 | dst = appendCborTypePrefix(dst, major, uint64(l)) 14 | } 15 | for _, v := range vals { 16 | dst = e.AppendString(dst, v) 17 | } 18 | return dst 19 | } 20 | 21 | // AppendString encodes and adds a string to the dst byte array. 22 | func (Encoder) AppendString(dst []byte, s string) []byte { 23 | major := majorTypeUtf8String 24 | 25 | l := len(s) 26 | if l <= additionalMax { 27 | lb := byte(l) 28 | dst = append(dst, major|lb) 29 | } else { 30 | dst = appendCborTypePrefix(dst, majorTypeUtf8String, uint64(l)) 31 | } 32 | return append(dst, s...) 33 | } 34 | 35 | // AppendStringers encodes and adds an array of Stringer values 36 | // to the dst byte array. 37 | func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte { 38 | if len(vals) == 0 { 39 | return e.AppendArrayEnd(e.AppendArrayStart(dst)) 40 | } 41 | dst = e.AppendArrayStart(dst) 42 | dst = e.AppendStringer(dst, vals[0]) 43 | if len(vals) > 1 { 44 | for _, val := range vals[1:] { 45 | dst = e.AppendStringer(dst, val) 46 | } 47 | } 48 | return e.AppendArrayEnd(dst) 49 | } 50 | 51 | // AppendStringer encodes and adds the Stringer value to the dst 52 | // byte array. 53 | func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte { 54 | if val == nil { 55 | return e.AppendNil(dst) 56 | } 57 | return e.AppendString(dst, val.String()) 58 | } 59 | 60 | // AppendBytes encodes and adds an array of bytes to the dst byte array. 61 | func (Encoder) AppendBytes(dst, s []byte) []byte { 62 | major := majorTypeByteString 63 | 64 | l := len(s) 65 | if l <= additionalMax { 66 | lb := byte(l) 67 | dst = append(dst, major|lb) 68 | } else { 69 | dst = appendCborTypePrefix(dst, major, uint64(l)) 70 | } 71 | return append(dst, s...) 72 | } 73 | 74 | // AppendEmbeddedJSON adds a tag and embeds input JSON as such. 75 | func AppendEmbeddedJSON(dst, s []byte) []byte { 76 | major := majorTypeTags 77 | minor := additionalTypeEmbeddedJSON 78 | 79 | // Append the TAG to indicate this is Embedded JSON. 80 | dst = append(dst, major|additionalTypeIntUint16) 81 | dst = append(dst, byte(minor>>8)) 82 | dst = append(dst, byte(minor&0xff)) 83 | 84 | // Append the JSON Object as Byte String. 85 | major = majorTypeByteString 86 | 87 | l := len(s) 88 | if l <= additionalMax { 89 | lb := byte(l) 90 | dst = append(dst, major|lb) 91 | } else { 92 | dst = appendCborTypePrefix(dst, major, uint64(l)) 93 | } 94 | return append(dst, s...) 95 | } 96 | 97 | // AppendEmbeddedCBOR adds a tag and embeds input CBOR as such. 98 | func AppendEmbeddedCBOR(dst, s []byte) []byte { 99 | major := majorTypeTags 100 | minor := additionalTypeEmbeddedCBOR 101 | 102 | // Append the TAG to indicate this is Embedded JSON. 103 | dst = append(dst, major|additionalTypeIntUint8) 104 | dst = append(dst, minor) 105 | 106 | // Append the CBOR Object as Byte String. 107 | major = majorTypeByteString 108 | 109 | l := len(s) 110 | if l <= additionalMax { 111 | lb := byte(l) 112 | dst = append(dst, major|lb) 113 | } else { 114 | dst = appendCborTypePrefix(dst, major, uint64(l)) 115 | } 116 | return append(dst, s...) 117 | } 118 | -------------------------------------------------------------------------------- /internal/cbor/string_test.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var encodeStringTests = []struct { 9 | plain string 10 | binary string 11 | json string //begin and end quotes are implied 12 | }{ 13 | {"", "\x60", ""}, 14 | {"\\", "\x61\x5c", "\\\\"}, 15 | {"\x00", "\x61\x00", "\\u0000"}, 16 | {"\x01", "\x61\x01", "\\u0001"}, 17 | {"\x02", "\x61\x02", "\\u0002"}, 18 | {"\x03", "\x61\x03", "\\u0003"}, 19 | {"\x04", "\x61\x04", "\\u0004"}, 20 | {"*", "\x61*", "*"}, 21 | {"a", "\x61a", "a"}, 22 | {"IETF", "\x64IETF", "IETF"}, 23 | {"abcdefghijklmnopqrstuvwxyzABCD", "\x78\x1eabcdefghijklmnopqrstuvwxyzABCD", "abcdefghijklmnopqrstuvwxyzABCD"}, 24 | {"<------------------------------------ This is a 100 character string ----------------------------->" + 25 | "<------------------------------------ This is a 100 character string ----------------------------->" + 26 | "<------------------------------------ This is a 100 character string ----------------------------->", 27 | "\x79\x01\x2c<------------------------------------ This is a 100 character string ----------------------------->" + 28 | "<------------------------------------ This is a 100 character string ----------------------------->" + 29 | "<------------------------------------ This is a 100 character string ----------------------------->", 30 | "<------------------------------------ This is a 100 character string ----------------------------->" + 31 | "<------------------------------------ This is a 100 character string ----------------------------->" + 32 | "<------------------------------------ This is a 100 character string ----------------------------->"}, 33 | {"emoji \u2764\ufe0f!", "\x6demoji ❤️!", "emoji \u2764\ufe0f!"}, 34 | } 35 | 36 | var encodeByteTests = []struct { 37 | plain []byte 38 | binary string 39 | }{ 40 | {[]byte{}, "\x40"}, 41 | {[]byte("\\"), "\x41\x5c"}, 42 | {[]byte("\x00"), "\x41\x00"}, 43 | {[]byte("\x01"), "\x41\x01"}, 44 | {[]byte("\x02"), "\x41\x02"}, 45 | {[]byte("\x03"), "\x41\x03"}, 46 | {[]byte("\x04"), "\x41\x04"}, 47 | {[]byte("*"), "\x41*"}, 48 | {[]byte("a"), "\x41a"}, 49 | {[]byte("IETF"), "\x44IETF"}, 50 | {[]byte("abcdefghijklmnopqrstuvwxyzABCD"), "\x58\x1eabcdefghijklmnopqrstuvwxyzABCD"}, 51 | {[]byte("<------------------------------------ This is a 100 character string ----------------------------->" + 52 | "<------------------------------------ This is a 100 character string ----------------------------->" + 53 | "<------------------------------------ This is a 100 character string ----------------------------->"), 54 | "\x59\x01\x2c<------------------------------------ This is a 100 character string ----------------------------->" + 55 | "<------------------------------------ This is a 100 character string ----------------------------->" + 56 | "<------------------------------------ This is a 100 character string ----------------------------->"}, 57 | {[]byte("emoji \u2764\ufe0f!"), "\x4demoji ❤️!"}, 58 | } 59 | 60 | func TestAppendString(t *testing.T) { 61 | for _, tt := range encodeStringTests { 62 | b := enc.AppendString([]byte{}, tt.plain) 63 | if got, want := string(b), tt.binary; got != want { 64 | t.Errorf("appendString(%q) = %#q, want %#q", tt.plain, got, want) 65 | } 66 | } 67 | //Test a large string > 65535 length 68 | 69 | var buffer bytes.Buffer 70 | for i := 0; i < 0x00011170; i++ { //70,000 character string 71 | buffer.WriteString("a") 72 | } 73 | inp := buffer.String() 74 | want := "\x7a\x00\x01\x11\x70" + inp 75 | b := enc.AppendString([]byte{}, inp) 76 | if got := string(b); got != want { 77 | t.Errorf("appendString(%q) = %#q, want %#q", inp, got, want) 78 | } 79 | } 80 | 81 | func TestAppendBytes(t *testing.T) { 82 | for _, tt := range encodeByteTests { 83 | b := enc.AppendBytes([]byte{}, tt.plain) 84 | if got, want := string(b), tt.binary; got != want { 85 | t.Errorf("appendString(%q) = %#q, want %#q", tt.plain, got, want) 86 | } 87 | } 88 | //Test a large string > 65535 length 89 | 90 | inp := []byte{} 91 | for i := 0; i < 0x00011170; i++ { //70,000 character string 92 | inp = append(inp, byte('a')) 93 | } 94 | want := "\x5a\x00\x01\x11\x70" + string(inp) 95 | b := enc.AppendBytes([]byte{}, inp) 96 | if got := string(b); got != want { 97 | t.Errorf("appendString(%q) = %#q, want %#q", inp, got, want) 98 | } 99 | } 100 | func BenchmarkAppendString(b *testing.B) { 101 | tests := map[string]string{ 102 | "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 103 | "EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 104 | "EncodingMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa"aaaaaaaaaaaaaaaaaaaaaaaa`, 105 | "EncodingLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`, 106 | "MultiBytesFirst": `❤️aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 107 | "MultiBytesMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa❤️aaaaaaaaaaaaaaaaaaaaaaaa`, 108 | "MultiBytesLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa❤️`, 109 | } 110 | for name, str := range tests { 111 | b.Run(name, func(b *testing.B) { 112 | buf := make([]byte, 0, 120) 113 | for i := 0; i < b.N; i++ { 114 | _ = enc.AppendString(buf, str) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/cbor/time.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func appendIntegerTimestamp(dst []byte, t time.Time) []byte { 8 | major := majorTypeTags 9 | minor := additionalTypeTimestamp 10 | dst = append(dst, major|minor) 11 | secs := t.Unix() 12 | var val uint64 13 | if secs < 0 { 14 | major = majorTypeNegativeInt 15 | val = uint64(-secs - 1) 16 | } else { 17 | major = majorTypeUnsignedInt 18 | val = uint64(secs) 19 | } 20 | dst = appendCborTypePrefix(dst, major, val) 21 | return dst 22 | } 23 | 24 | func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte { 25 | major := majorTypeTags 26 | minor := additionalTypeTimestamp 27 | dst = append(dst, major|minor) 28 | secs := t.Unix() 29 | nanos := t.Nanosecond() 30 | var val float64 31 | val = float64(secs)*1.0 + float64(nanos)*1e-9 32 | return e.AppendFloat64(dst, val, -1) 33 | } 34 | 35 | // AppendTime encodes and adds a timestamp to the dst byte array. 36 | func (e Encoder) AppendTime(dst []byte, t time.Time, unused string) []byte { 37 | utc := t.UTC() 38 | if utc.Nanosecond() == 0 { 39 | return appendIntegerTimestamp(dst, utc) 40 | } 41 | return e.appendFloatTimestamp(dst, utc) 42 | } 43 | 44 | // AppendTimes encodes and adds an array of timestamps to the dst byte array. 45 | func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte { 46 | major := majorTypeArray 47 | l := len(vals) 48 | if l == 0 { 49 | return e.AppendArrayEnd(e.AppendArrayStart(dst)) 50 | } 51 | if l <= additionalMax { 52 | lb := byte(l) 53 | dst = append(dst, major|lb) 54 | } else { 55 | dst = appendCborTypePrefix(dst, major, uint64(l)) 56 | } 57 | 58 | for _, t := range vals { 59 | dst = e.AppendTime(dst, t, unused) 60 | } 61 | return dst 62 | } 63 | 64 | // AppendDuration encodes and adds a duration to the dst byte array. 65 | // useInt field indicates whether to store the duration as seconds (integer) or 66 | // as seconds+nanoseconds (float). 67 | func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, unused int) []byte { 68 | if useInt { 69 | return e.AppendInt64(dst, int64(d/unit)) 70 | } 71 | return e.AppendFloat64(dst, float64(d)/float64(unit), unused) 72 | } 73 | 74 | // AppendDurations encodes and adds an array of durations to the dst byte array. 75 | // useInt field indicates whether to store the duration as seconds (integer) or 76 | // as seconds+nanoseconds (float). 77 | func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, unused int) []byte { 78 | major := majorTypeArray 79 | l := len(vals) 80 | if l == 0 { 81 | return e.AppendArrayEnd(e.AppendArrayStart(dst)) 82 | } 83 | if l <= additionalMax { 84 | lb := byte(l) 85 | dst = append(dst, major|lb) 86 | } else { 87 | dst = appendCborTypePrefix(dst, major, uint64(l)) 88 | } 89 | for _, d := range vals { 90 | dst = e.AppendDuration(dst, d, unit, useInt, unused) 91 | } 92 | return dst 93 | } 94 | -------------------------------------------------------------------------------- /internal/cbor/time_test.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "math" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestAppendTimeNow(t *testing.T) { 12 | tm := time.Now() 13 | s := enc.AppendTime([]byte{}, tm, "unused") 14 | got := string(s) 15 | 16 | tm1 := float64(tm.Unix()) + float64(tm.Nanosecond())*1E-9 17 | tm2 := math.Float64bits(tm1) 18 | var tm3 [8]byte 19 | for i := uint(0); i < 8; i++ { 20 | tm3[i] = byte(tm2 >> ((8 - i - 1) * 8)) 21 | } 22 | want := append([]byte{0xc1, 0xfb}, tm3[:]...) 23 | if got != string(want) { 24 | t.Errorf("Appendtime(%s)=0x%s, want: 0x%s", 25 | "time.Now()", hex.EncodeToString(s), 26 | hex.EncodeToString(want)) 27 | } 28 | } 29 | 30 | var timeIntegerTestcases = []struct { 31 | txt string 32 | binary string 33 | rfcStr string 34 | }{ 35 | {"2013-02-03T19:54:00-08:00", "\xc1\x1a\x51\x0f\x30\xd8", "2013-02-04T03:54:00Z"}, 36 | {"1950-02-03T19:54:00-08:00", "\xc1\x3a\x25\x71\x93\xa7", "1950-02-04T03:54:00Z"}, 37 | } 38 | 39 | func TestAppendTimePastPresentInteger(t *testing.T) { 40 | for _, tt := range timeIntegerTestcases { 41 | tin, err := time.Parse(time.RFC3339, tt.txt) 42 | if err != nil { 43 | fmt.Println("Cannot parse input", tt.txt, ".. Skipping!", err) 44 | continue 45 | } 46 | b := enc.AppendTime([]byte{}, tin, "unused") 47 | if got, want := string(b), tt.binary; got != want { 48 | t.Errorf("appendString(%s) = 0x%s, want 0x%s", tt.txt, 49 | hex.EncodeToString(b), 50 | hex.EncodeToString([]byte(want))) 51 | } 52 | } 53 | } 54 | 55 | var timeFloatTestcases = []struct { 56 | rfcStr string 57 | out string 58 | }{ 59 | {"2006-01-02T15:04:05.999999-08:00", "\xc1\xfb\x41\xd0\xee\x6c\x59\x7f\xff\xfc"}, 60 | {"1956-01-02T15:04:05.999999-08:00", "\xc1\xfb\xc1\xba\x53\x81\x1a\x00\x00\x11"}, 61 | } 62 | 63 | func TestAppendTimePastPresentFloat(t *testing.T) { 64 | const timeFloatFmt = "2006-01-02T15:04:05.999999-07:00" 65 | for _, tt := range timeFloatTestcases { 66 | tin, err := time.Parse(timeFloatFmt, tt.rfcStr) 67 | if err != nil { 68 | fmt.Println("Cannot parse input", tt.rfcStr, ".. Skipping!") 69 | continue 70 | } 71 | b := enc.AppendTime([]byte{}, tin, "unused") 72 | if got, want := string(b), tt.out; got != want { 73 | t.Errorf("appendString(%s) = 0x%s, want 0x%s", tt.rfcStr, 74 | hex.EncodeToString(b), 75 | hex.EncodeToString([]byte(want))) 76 | } 77 | } 78 | } 79 | 80 | func BenchmarkAppendTime(b *testing.B) { 81 | tests := map[string]string{ 82 | "Integer": "Feb 3, 2013 at 7:54pm (PST)", 83 | "Float": "2006-01-02T15:04:05.999999-08:00", 84 | } 85 | const timeFloatFmt = "2006-01-02T15:04:05.999999-07:00" 86 | 87 | for name, str := range tests { 88 | t, err := time.Parse(time.RFC3339, str) 89 | if err != nil { 90 | t, _ = time.Parse(timeFloatFmt, str) 91 | } 92 | b.Run(name, func(b *testing.B) { 93 | buf := make([]byte, 0, 100) 94 | for i := 0; i < b.N; i++ { 95 | _ = enc.AppendTime(buf, t, "unused") 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/cbor/types_64_test.go: -------------------------------------------------------------------------------- 1 | // +build !386 2 | 3 | package cbor 4 | 5 | import ( 6 | "encoding/hex" 7 | "testing" 8 | ) 9 | 10 | var enc2 = Encoder{} 11 | 12 | var integerTestCases_64bit = []struct { 13 | val int 14 | binary string 15 | }{ 16 | // Value in 8 bytes. 17 | {0xabcd100000000, "\x1b\x00\x0a\xbc\xd1\x00\x00\x00\x00"}, 18 | {1000000000000, "\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"}, 19 | // Value in 8 bytes. 20 | {-0xabcd100000001, "\x3b\x00\x0a\xbc\xd1\x00\x00\x00\x00"}, 21 | {-1000000000001, "\x3b\x00\x00\x00\xe8\xd4\xa5\x10\x00"}, 22 | } 23 | 24 | func TestAppendInt_64bit(t *testing.T) { 25 | for _, tc := range integerTestCases_64bit { 26 | s := enc2.AppendInt([]byte{}, tc.val) 27 | got := string(s) 28 | if got != tc.binary { 29 | t.Errorf("AppendInt(0x%x)=0x%s, want: 0x%s", 30 | tc.val, hex.EncodeToString(s), 31 | hex.EncodeToString([]byte(tc.binary))) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/cbor/types_test.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | "encoding/hex" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | var enc = Encoder{} 10 | 11 | func TestAppendNil(t *testing.T) { 12 | s := enc.AppendNil([]byte{}) 13 | got := string(s) 14 | want := "\xf6" 15 | if got != want { 16 | t.Errorf("appendNull() = 0x%s, want: 0x%s", hex.EncodeToString(s), 17 | hex.EncodeToString([]byte(want))) 18 | } 19 | } 20 | 21 | var booleanTestCases = []struct { 22 | val bool 23 | binary string 24 | json string 25 | }{ 26 | {true, "\xf5", "true"}, 27 | {false, "\xf4", "false"}, 28 | } 29 | 30 | func TestAppendBool(t *testing.T) { 31 | for _, tc := range booleanTestCases { 32 | s := enc.AppendBool([]byte{}, tc.val) 33 | got := string(s) 34 | if got != tc.binary { 35 | t.Errorf("AppendBool(%s)=0x%s, want: 0x%s", 36 | tc.json, hex.EncodeToString(s), 37 | hex.EncodeToString([]byte(tc.binary))) 38 | } 39 | } 40 | } 41 | 42 | var booleanArrayTestCases = []struct { 43 | val []bool 44 | binary string 45 | json string 46 | }{ 47 | {[]bool{true, false, true}, "\x83\xf5\xf4\xf5", "[true,false,true]"}, 48 | {[]bool{true, false, false, true, false, true}, "\x86\xf5\xf4\xf4\xf5\xf4\xf5", "[true,false,false,true,false,true]"}, 49 | } 50 | 51 | func TestAppendBoolArray(t *testing.T) { 52 | for _, tc := range booleanArrayTestCases { 53 | s := enc.AppendBools([]byte{}, tc.val) 54 | got := string(s) 55 | if got != tc.binary { 56 | t.Errorf("AppendBools(%s)=0x%s, want: 0x%s", 57 | tc.json, hex.EncodeToString(s), 58 | hex.EncodeToString([]byte(tc.binary))) 59 | } 60 | } 61 | } 62 | 63 | var integerTestCases = []struct { 64 | val int 65 | binary string 66 | }{ 67 | // Value included in the type. 68 | {0, "\x00"}, 69 | {1, "\x01"}, 70 | {2, "\x02"}, 71 | {3, "\x03"}, 72 | {8, "\x08"}, 73 | {9, "\x09"}, 74 | {10, "\x0a"}, 75 | {22, "\x16"}, 76 | {23, "\x17"}, 77 | // Value in 1 byte. 78 | {24, "\x18\x18"}, 79 | {25, "\x18\x19"}, 80 | {26, "\x18\x1a"}, 81 | {100, "\x18\x64"}, 82 | {254, "\x18\xfe"}, 83 | {255, "\x18\xff"}, 84 | // Value in 2 bytes. 85 | {256, "\x19\x01\x00"}, 86 | {257, "\x19\x01\x01"}, 87 | {1000, "\x19\x03\xe8"}, 88 | {0xFFFF, "\x19\xff\xff"}, 89 | // Value in 4 bytes. 90 | {0x10000, "\x1a\x00\x01\x00\x00"}, 91 | {0x7FFFFFFE, "\x1a\x7f\xff\xff\xfe"}, 92 | {1000000, "\x1a\x00\x0f\x42\x40"}, 93 | // Negative number test cases. 94 | // Value included in the type. 95 | {-1, "\x20"}, 96 | {-2, "\x21"}, 97 | {-3, "\x22"}, 98 | {-10, "\x29"}, 99 | {-21, "\x34"}, 100 | {-22, "\x35"}, 101 | {-23, "\x36"}, 102 | {-24, "\x37"}, 103 | // Value in 1 byte. 104 | {-25, "\x38\x18"}, 105 | {-26, "\x38\x19"}, 106 | {-100, "\x38\x63"}, 107 | {-254, "\x38\xfd"}, 108 | {-255, "\x38\xfe"}, 109 | {-256, "\x38\xff"}, 110 | // Value in 2 bytes. 111 | {-257, "\x39\x01\x00"}, 112 | {-258, "\x39\x01\x01"}, 113 | {-1000, "\x39\x03\xe7"}, 114 | // Value in 4 bytes. 115 | {-0x10001, "\x3a\x00\x01\x00\x00"}, 116 | {-0x7FFFFFFE, "\x3a\x7f\xff\xff\xfd"}, 117 | {-1000000, "\x3a\x00\x0f\x42\x3f"}, 118 | } 119 | 120 | func TestAppendInt(t *testing.T) { 121 | for _, tc := range integerTestCases { 122 | s := enc.AppendInt([]byte{}, tc.val) 123 | got := string(s) 124 | if got != tc.binary { 125 | t.Errorf("AppendInt(0x%x)=0x%s, want: 0x%s", 126 | tc.val, hex.EncodeToString(s), 127 | hex.EncodeToString([]byte(tc.binary))) 128 | } 129 | } 130 | } 131 | 132 | var integerArrayTestCases = []struct { 133 | val []int 134 | binary string 135 | json string 136 | }{ 137 | {[]int{-1, 0, 200, 20}, "\x84\x20\x00\x18\xc8\x14", "[-1,0,200,20]"}, 138 | {[]int{-200, -10, 200, 400}, "\x84\x38\xc7\x29\x18\xc8\x19\x01\x90", "[-200,-10,200,400]"}, 139 | {[]int{1, 2, 3}, "\x83\x01\x02\x03", "[1,2,3]"}, 140 | {[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}, 141 | "\x98\x19\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x18\x18\x19", 142 | "[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]"}, 143 | } 144 | 145 | func TestAppendIntArray(t *testing.T) { 146 | for _, tc := range integerArrayTestCases { 147 | s := enc.AppendInts([]byte{}, tc.val) 148 | got := string(s) 149 | if got != tc.binary { 150 | t.Errorf("AppendInts(%s)=0x%s, want: 0x%s", 151 | tc.json, hex.EncodeToString(s), 152 | hex.EncodeToString([]byte(tc.binary))) 153 | } 154 | } 155 | } 156 | 157 | var float32TestCases = []struct { 158 | val float32 159 | binary string 160 | }{ 161 | {0.0, "\xfa\x00\x00\x00\x00"}, 162 | {-0.0, "\xfa\x00\x00\x00\x00"}, 163 | {1.0, "\xfa\x3f\x80\x00\x00"}, 164 | {1.5, "\xfa\x3f\xc0\x00\x00"}, 165 | {65504.0, "\xfa\x47\x7f\xe0\x00"}, 166 | {-4.0, "\xfa\xc0\x80\x00\x00"}, 167 | {0.00006103515625, "\xfa\x38\x80\x00\x00"}, 168 | } 169 | 170 | func TestAppendFloat32(t *testing.T) { 171 | for _, tc := range float32TestCases { 172 | s := enc.AppendFloat32([]byte{}, tc.val, -1) 173 | got := string(s) 174 | if got != tc.binary { 175 | t.Errorf("AppendFloat32(%f)=0x%s, want: 0x%s", 176 | tc.val, hex.EncodeToString(s), 177 | hex.EncodeToString([]byte(tc.binary))) 178 | } 179 | } 180 | } 181 | 182 | var ipAddrTestCases = []struct { 183 | ipaddr net.IP 184 | text string // ASCII representation of ipaddr 185 | binary string // CBOR representation of ipaddr 186 | }{ 187 | {net.IP{10, 0, 0, 1}, "\"10.0.0.1\"", "\xd9\x01\x04\x44\x0a\x00\x00\x01"}, 188 | {net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}, 189 | "\"2001:db8:85a3::8a2e:370:7334\"", 190 | "\xd9\x01\x04\x50\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34"}, 191 | } 192 | 193 | func TestAppendNetworkAddr(t *testing.T) { 194 | for _, tc := range ipAddrTestCases { 195 | s := enc.AppendIPAddr([]byte{}, tc.ipaddr) 196 | got := string(s) 197 | if got != tc.binary { 198 | t.Errorf("AppendIPAddr(%s)=0x%s, want: 0x%s", 199 | tc.ipaddr, hex.EncodeToString(s), 200 | hex.EncodeToString([]byte(tc.binary))) 201 | } 202 | } 203 | } 204 | 205 | var macAddrTestCases = []struct { 206 | macaddr net.HardwareAddr 207 | text string // ASCII representation of macaddr 208 | binary string // CBOR representation of macaddr 209 | }{ 210 | {net.HardwareAddr{0x12, 0x34, 0x56, 0x78, 0x90, 0xab}, "\"12:34:56:78:90:ab\"", "\xd9\x01\x04\x46\x12\x34\x56\x78\x90\xab"}, 211 | {net.HardwareAddr{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3}, "\"20:01:0d:b8:85:a3\"", "\xd9\x01\x04\x46\x20\x01\x0d\xb8\x85\xa3"}, 212 | } 213 | 214 | func TestAppendMACAddr(t *testing.T) { 215 | for _, tc := range macAddrTestCases { 216 | s := enc.AppendMACAddr([]byte{}, tc.macaddr) 217 | got := string(s) 218 | if got != tc.binary { 219 | t.Errorf("AppendMACAddr(%s)=0x%s, want: 0x%s", 220 | tc.macaddr.String(), hex.EncodeToString(s), 221 | hex.EncodeToString([]byte(tc.binary))) 222 | } 223 | } 224 | } 225 | 226 | var IPPrefixTestCases = []struct { 227 | pfx net.IPNet 228 | text string // ASCII representation of pfx 229 | binary string // CBOR representation of pfx 230 | }{ 231 | {net.IPNet{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(0, 32)}, "\"0.0.0.0/0\"", "\xd9\x01\x05\xa1\x44\x00\x00\x00\x00\x00"}, 232 | {net.IPNet{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}, "\"192.168.0.100/24\"", 233 | "\xd9\x01\x05\xa1\x44\xc0\xa8\x00\x64\x18\x18"}, 234 | } 235 | 236 | func TestAppendIPPrefix(t *testing.T) { 237 | for _, tc := range IPPrefixTestCases { 238 | s := enc.AppendIPPrefix([]byte{}, tc.pfx) 239 | got := string(s) 240 | if got != tc.binary { 241 | t.Errorf("AppendIPPrefix(%s)=0x%s, want: 0x%s", 242 | tc.pfx.String(), hex.EncodeToString(s), 243 | hex.EncodeToString([]byte(tc.binary))) 244 | } 245 | } 246 | } 247 | 248 | func BenchmarkAppendInt(b *testing.B) { 249 | type st struct { 250 | sz byte 251 | val int64 252 | } 253 | tests := map[string]st{ 254 | "int-Positive": {sz: 0, val: 10000}, 255 | "int-Negative": {sz: 0, val: -10000}, 256 | "uint8": {sz: 1, val: 100}, 257 | "uint16": {sz: 2, val: 0xfff}, 258 | "uint32": {sz: 4, val: 0xffffff}, 259 | "uint64": {sz: 8, val: 0xffffffffff}, 260 | "int8": {sz: 21, val: -120}, 261 | "int16": {sz: 22, val: -1200}, 262 | "int32": {sz: 23, val: 32000}, 263 | "int64": {sz: 24, val: 0xffffffffff}, 264 | } 265 | for name, str := range tests { 266 | b.Run(name, func(b *testing.B) { 267 | buf := make([]byte, 0, 100) 268 | for i := 0; i < b.N; i++ { 269 | switch str.sz { 270 | case 0: 271 | _ = enc.AppendInt(buf, int(str.val)) 272 | case 1: 273 | _ = enc.AppendUint8(buf, uint8(str.val)) 274 | case 2: 275 | _ = enc.AppendUint16(buf, uint16(str.val)) 276 | case 4: 277 | _ = enc.AppendUint32(buf, uint32(str.val)) 278 | case 8: 279 | _ = enc.AppendUint64(buf, uint64(str.val)) 280 | case 21: 281 | _ = enc.AppendInt8(buf, int8(str.val)) 282 | case 22: 283 | _ = enc.AppendInt16(buf, int16(str.val)) 284 | case 23: 285 | _ = enc.AppendInt32(buf, int32(str.val)) 286 | case 24: 287 | _ = enc.AppendInt64(buf, int64(str.val)) 288 | } 289 | } 290 | }) 291 | } 292 | } 293 | 294 | func BenchmarkAppendFloat(b *testing.B) { 295 | type st struct { 296 | sz byte 297 | val float64 298 | } 299 | tests := map[string]st{ 300 | "Float32": {sz: 4, val: 10000.12345}, 301 | "Float64": {sz: 8, val: -10000.54321}, 302 | } 303 | for name, str := range tests { 304 | b.Run(name, func(b *testing.B) { 305 | buf := make([]byte, 0, 100) 306 | for i := 0; i < b.N; i++ { 307 | switch str.sz { 308 | case 4: 309 | _ = enc.AppendFloat32(buf, float32(str.val), -1) 310 | case 8: 311 | _ = enc.AppendFloat64(buf, str.val, -1) 312 | } 313 | } 314 | }) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /internal/json/base.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | // JSONMarshalFunc is used to marshal interface to JSON encoded byte slice. 4 | // Making it package level instead of embedded in Encoder brings 5 | // some extra efforts at importing, but avoids value copy when the functions 6 | // of Encoder being invoked. 7 | // DO REMEMBER to set this variable at importing, or 8 | // you might get a nil pointer dereference panic at runtime. 9 | var JSONMarshalFunc func(v interface{}) ([]byte, error) 10 | 11 | type Encoder struct{} 12 | 13 | // AppendKey appends a new key to the output JSON. 14 | func (e Encoder) AppendKey(dst []byte, key string) []byte { 15 | if dst[len(dst)-1] != '{' { 16 | dst = append(dst, ',') 17 | } 18 | return append(e.AppendString(dst, key), ':') 19 | } 20 | -------------------------------------------------------------------------------- /internal/json/bytes.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import "unicode/utf8" 4 | 5 | // AppendBytes is a mirror of appendString with []byte arg 6 | func (Encoder) AppendBytes(dst, s []byte) []byte { 7 | dst = append(dst, '"') 8 | for i := 0; i < len(s); i++ { 9 | if !noEscapeTable[s[i]] { 10 | dst = appendBytesComplex(dst, s, i) 11 | return append(dst, '"') 12 | } 13 | } 14 | dst = append(dst, s...) 15 | return append(dst, '"') 16 | } 17 | 18 | // AppendHex encodes the input bytes to a hex string and appends 19 | // the encoded string to the input byte slice. 20 | // 21 | // The operation loops though each byte and encodes it as hex using 22 | // the hex lookup table. 23 | func (Encoder) AppendHex(dst, s []byte) []byte { 24 | dst = append(dst, '"') 25 | for _, v := range s { 26 | dst = append(dst, hex[v>>4], hex[v&0x0f]) 27 | } 28 | return append(dst, '"') 29 | } 30 | 31 | // appendBytesComplex is a mirror of the appendStringComplex 32 | // with []byte arg 33 | func appendBytesComplex(dst, s []byte, i int) []byte { 34 | start := 0 35 | for i < len(s) { 36 | b := s[i] 37 | if b >= utf8.RuneSelf { 38 | r, size := utf8.DecodeRune(s[i:]) 39 | if r == utf8.RuneError && size == 1 { 40 | if start < i { 41 | dst = append(dst, s[start:i]...) 42 | } 43 | dst = append(dst, `\ufffd`...) 44 | i += size 45 | start = i 46 | continue 47 | } 48 | i += size 49 | continue 50 | } 51 | if noEscapeTable[b] { 52 | i++ 53 | continue 54 | } 55 | // We encountered a character that needs to be encoded. 56 | // Let's append the previous simple characters to the byte slice 57 | // and switch our operation to read and encode the remainder 58 | // characters byte-by-byte. 59 | if start < i { 60 | dst = append(dst, s[start:i]...) 61 | } 62 | switch b { 63 | case '"', '\\': 64 | dst = append(dst, '\\', b) 65 | case '\b': 66 | dst = append(dst, '\\', 'b') 67 | case '\f': 68 | dst = append(dst, '\\', 'f') 69 | case '\n': 70 | dst = append(dst, '\\', 'n') 71 | case '\r': 72 | dst = append(dst, '\\', 'r') 73 | case '\t': 74 | dst = append(dst, '\\', 't') 75 | default: 76 | dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF]) 77 | } 78 | i++ 79 | start = i 80 | } 81 | if start < len(s) { 82 | dst = append(dst, s[start:]...) 83 | } 84 | return dst 85 | } 86 | -------------------------------------------------------------------------------- /internal/json/bytes_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | "unicode" 6 | ) 7 | 8 | var enc = Encoder{} 9 | 10 | func TestAppendBytes(t *testing.T) { 11 | for _, tt := range encodeStringTests { 12 | b := enc.AppendBytes([]byte{}, []byte(tt.in)) 13 | if got, want := string(b), tt.out; got != want { 14 | t.Errorf("appendBytes(%q) = %#q, want %#q", tt.in, got, want) 15 | } 16 | } 17 | } 18 | 19 | func TestAppendHex(t *testing.T) { 20 | for _, tt := range encodeHexTests { 21 | b := enc.AppendHex([]byte{}, []byte{tt.in}) 22 | if got, want := string(b), tt.out; got != want { 23 | t.Errorf("appendHex(%x) = %s, want %s", tt.in, got, want) 24 | } 25 | } 26 | } 27 | 28 | func TestStringBytes(t *testing.T) { 29 | t.Parallel() 30 | // Test that encodeState.stringBytes and encodeState.string use the same encoding. 31 | var r []rune 32 | for i := '\u0000'; i <= unicode.MaxRune; i++ { 33 | r = append(r, i) 34 | } 35 | s := string(r) + "\xff\xff\xffhello" // some invalid UTF-8 too 36 | 37 | encStr := string(enc.AppendString([]byte{}, s)) 38 | encBytes := string(enc.AppendBytes([]byte{}, []byte(s))) 39 | 40 | if encStr != encBytes { 41 | i := 0 42 | for i < len(encStr) && i < len(encBytes) && encStr[i] == encBytes[i] { 43 | i++ 44 | } 45 | encStr = encStr[i:] 46 | encBytes = encBytes[i:] 47 | i = 0 48 | for i < len(encStr) && i < len(encBytes) && encStr[len(encStr)-i-1] == encBytes[len(encBytes)-i-1] { 49 | i++ 50 | } 51 | encStr = encStr[:len(encStr)-i] 52 | encBytes = encBytes[:len(encBytes)-i] 53 | 54 | if len(encStr) > 20 { 55 | encStr = encStr[:20] + "..." 56 | } 57 | if len(encBytes) > 20 { 58 | encBytes = encBytes[:20] + "..." 59 | } 60 | 61 | t.Errorf("encodings differ at %#q vs %#q", encStr, encBytes) 62 | } 63 | } 64 | 65 | func BenchmarkAppendBytes(b *testing.B) { 66 | tests := map[string]string{ 67 | "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 68 | "EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 69 | "EncodingMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa"aaaaaaaaaaaaaaaaaaaaaaaa`, 70 | "EncodingLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`, 71 | "MultiBytesFirst": `❤️aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 72 | "MultiBytesMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa❤️aaaaaaaaaaaaaaaaaaaaaaaa`, 73 | "MultiBytesLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa❤️`, 74 | } 75 | for name, str := range tests { 76 | byt := []byte(str) 77 | b.Run(name, func(b *testing.B) { 78 | buf := make([]byte, 0, 100) 79 | for i := 0; i < b.N; i++ { 80 | _ = enc.AppendBytes(buf, byt) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/json/string.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "unicode/utf8" 6 | ) 7 | 8 | const hex = "0123456789abcdef" 9 | 10 | var noEscapeTable = [256]bool{} 11 | 12 | func init() { 13 | for i := 0; i <= 0x7e; i++ { 14 | noEscapeTable[i] = i >= 0x20 && i != '\\' && i != '"' 15 | } 16 | } 17 | 18 | // AppendStrings encodes the input strings to json and 19 | // appends the encoded string list to the input byte slice. 20 | func (e Encoder) AppendStrings(dst []byte, vals []string) []byte { 21 | if len(vals) == 0 { 22 | return append(dst, '[', ']') 23 | } 24 | dst = append(dst, '[') 25 | dst = e.AppendString(dst, vals[0]) 26 | if len(vals) > 1 { 27 | for _, val := range vals[1:] { 28 | dst = e.AppendString(append(dst, ','), val) 29 | } 30 | } 31 | dst = append(dst, ']') 32 | return dst 33 | } 34 | 35 | // AppendString encodes the input string to json and appends 36 | // the encoded string to the input byte slice. 37 | // 38 | // The operation loops though each byte in the string looking 39 | // for characters that need json or utf8 encoding. If the string 40 | // does not need encoding, then the string is appended in its 41 | // entirety to the byte slice. 42 | // If we encounter a byte that does need encoding, switch up 43 | // the operation and perform a byte-by-byte read-encode-append. 44 | func (Encoder) AppendString(dst []byte, s string) []byte { 45 | // Start with a double quote. 46 | dst = append(dst, '"') 47 | // Loop through each character in the string. 48 | for i := 0; i < len(s); i++ { 49 | // Check if the character needs encoding. Control characters, slashes, 50 | // and the double quote need json encoding. Bytes above the ascii 51 | // boundary needs utf8 encoding. 52 | if !noEscapeTable[s[i]] { 53 | // We encountered a character that needs to be encoded. Switch 54 | // to complex version of the algorithm. 55 | dst = appendStringComplex(dst, s, i) 56 | return append(dst, '"') 57 | } 58 | } 59 | // The string has no need for encoding and therefore is directly 60 | // appended to the byte slice. 61 | dst = append(dst, s...) 62 | // End with a double quote 63 | return append(dst, '"') 64 | } 65 | 66 | // AppendStringers encodes the provided Stringer list to json and 67 | // appends the encoded Stringer list to the input byte slice. 68 | func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte { 69 | if len(vals) == 0 { 70 | return append(dst, '[', ']') 71 | } 72 | dst = append(dst, '[') 73 | dst = e.AppendStringer(dst, vals[0]) 74 | if len(vals) > 1 { 75 | for _, val := range vals[1:] { 76 | dst = e.AppendStringer(append(dst, ','), val) 77 | } 78 | } 79 | return append(dst, ']') 80 | } 81 | 82 | // AppendStringer encodes the input Stringer to json and appends the 83 | // encoded Stringer value to the input byte slice. 84 | func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte { 85 | if val == nil { 86 | return e.AppendInterface(dst, nil) 87 | } 88 | return e.AppendString(dst, val.String()) 89 | } 90 | 91 | // appendStringComplex is used by appendString to take over an in 92 | // progress JSON string encoding that encountered a character that needs 93 | // to be encoded. 94 | func appendStringComplex(dst []byte, s string, i int) []byte { 95 | start := 0 96 | for i < len(s) { 97 | b := s[i] 98 | if b >= utf8.RuneSelf { 99 | r, size := utf8.DecodeRuneInString(s[i:]) 100 | if r == utf8.RuneError && size == 1 { 101 | // In case of error, first append previous simple characters to 102 | // the byte slice if any and append a replacement character code 103 | // in place of the invalid sequence. 104 | if start < i { 105 | dst = append(dst, s[start:i]...) 106 | } 107 | dst = append(dst, `\ufffd`...) 108 | i += size 109 | start = i 110 | continue 111 | } 112 | i += size 113 | continue 114 | } 115 | if noEscapeTable[b] { 116 | i++ 117 | continue 118 | } 119 | // We encountered a character that needs to be encoded. 120 | // Let's append the previous simple characters to the byte slice 121 | // and switch our operation to read and encode the remainder 122 | // characters byte-by-byte. 123 | if start < i { 124 | dst = append(dst, s[start:i]...) 125 | } 126 | switch b { 127 | case '"', '\\': 128 | dst = append(dst, '\\', b) 129 | case '\b': 130 | dst = append(dst, '\\', 'b') 131 | case '\f': 132 | dst = append(dst, '\\', 'f') 133 | case '\n': 134 | dst = append(dst, '\\', 'n') 135 | case '\r': 136 | dst = append(dst, '\\', 'r') 137 | case '\t': 138 | dst = append(dst, '\\', 't') 139 | default: 140 | dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF]) 141 | } 142 | i++ 143 | start = i 144 | } 145 | if start < len(s) { 146 | dst = append(dst, s[start:]...) 147 | } 148 | return dst 149 | } 150 | -------------------------------------------------------------------------------- /internal/json/string_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var encodeStringTests = []struct { 8 | in string 9 | out string 10 | }{ 11 | {"", `""`}, 12 | {"\\", `"\\"`}, 13 | {"\x00", `"\u0000"`}, 14 | {"\x01", `"\u0001"`}, 15 | {"\x02", `"\u0002"`}, 16 | {"\x03", `"\u0003"`}, 17 | {"\x04", `"\u0004"`}, 18 | {"\x05", `"\u0005"`}, 19 | {"\x06", `"\u0006"`}, 20 | {"\x07", `"\u0007"`}, 21 | {"\x08", `"\b"`}, 22 | {"\x09", `"\t"`}, 23 | {"\x0a", `"\n"`}, 24 | {"\x0b", `"\u000b"`}, 25 | {"\x0c", `"\f"`}, 26 | {"\x0d", `"\r"`}, 27 | {"\x0e", `"\u000e"`}, 28 | {"\x0f", `"\u000f"`}, 29 | {"\x10", `"\u0010"`}, 30 | {"\x11", `"\u0011"`}, 31 | {"\x12", `"\u0012"`}, 32 | {"\x13", `"\u0013"`}, 33 | {"\x14", `"\u0014"`}, 34 | {"\x15", `"\u0015"`}, 35 | {"\x16", `"\u0016"`}, 36 | {"\x17", `"\u0017"`}, 37 | {"\x18", `"\u0018"`}, 38 | {"\x19", `"\u0019"`}, 39 | {"\x1a", `"\u001a"`}, 40 | {"\x1b", `"\u001b"`}, 41 | {"\x1c", `"\u001c"`}, 42 | {"\x1d", `"\u001d"`}, 43 | {"\x1e", `"\u001e"`}, 44 | {"\x1f", `"\u001f"`}, 45 | {"✭", `"✭"`}, 46 | {"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence 47 | {"ascii", `"ascii"`}, 48 | {"\"a", `"\"a"`}, 49 | {"\x1fa", `"\u001fa"`}, 50 | {"foo\"bar\"baz", `"foo\"bar\"baz"`}, 51 | {"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`}, 52 | {"emoji \u2764\ufe0f!", `"emoji ❤️!"`}, 53 | } 54 | 55 | var encodeHexTests = []struct { 56 | in byte 57 | out string 58 | }{ 59 | {0x00, `"00"`}, 60 | {0x0f, `"0f"`}, 61 | {0x10, `"10"`}, 62 | {0xf0, `"f0"`}, 63 | {0xff, `"ff"`}, 64 | } 65 | 66 | func TestAppendString(t *testing.T) { 67 | for _, tt := range encodeStringTests { 68 | b := enc.AppendString([]byte{}, tt.in) 69 | if got, want := string(b), tt.out; got != want { 70 | t.Errorf("appendString(%q) = %#q, want %#q", tt.in, got, want) 71 | } 72 | } 73 | } 74 | 75 | func BenchmarkAppendString(b *testing.B) { 76 | tests := map[string]string{ 77 | "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 78 | "EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 79 | "EncodingMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa"aaaaaaaaaaaaaaaaaaaaaaaa`, 80 | "EncodingLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`, 81 | "MultiBytesFirst": `❤️aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 82 | "MultiBytesMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa❤️aaaaaaaaaaaaaaaaaaaaaaaa`, 83 | "MultiBytesLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa❤️`, 84 | } 85 | for name, str := range tests { 86 | b.Run(name, func(b *testing.B) { 87 | buf := make([]byte, 0, 100) 88 | for i := 0; i < b.N; i++ { 89 | _ = enc.AppendString(buf, str) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/json/time.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // Import from zerolog/global.go 10 | timeFormatUnix = "" 11 | timeFormatUnixMs = "UNIXMS" 12 | timeFormatUnixMicro = "UNIXMICRO" 13 | timeFormatUnixNano = "UNIXNANO" 14 | ) 15 | 16 | // AppendTime formats the input time with the given format 17 | // and appends the encoded string to the input byte slice. 18 | func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte { 19 | switch format { 20 | case timeFormatUnix: 21 | return e.AppendInt64(dst, t.Unix()) 22 | case timeFormatUnixMs: 23 | return e.AppendInt64(dst, t.UnixNano()/1000000) 24 | case timeFormatUnixMicro: 25 | return e.AppendInt64(dst, t.UnixNano()/1000) 26 | case timeFormatUnixNano: 27 | return e.AppendInt64(dst, t.UnixNano()) 28 | } 29 | return append(t.AppendFormat(append(dst, '"'), format), '"') 30 | } 31 | 32 | // AppendTimes converts the input times with the given format 33 | // and appends the encoded string list to the input byte slice. 34 | func (Encoder) AppendTimes(dst []byte, vals []time.Time, format string) []byte { 35 | switch format { 36 | case timeFormatUnix: 37 | return appendUnixTimes(dst, vals) 38 | case timeFormatUnixMs: 39 | return appendUnixNanoTimes(dst, vals, 1000000) 40 | case timeFormatUnixMicro: 41 | return appendUnixNanoTimes(dst, vals, 1000) 42 | case timeFormatUnixNano: 43 | return appendUnixNanoTimes(dst, vals, 1) 44 | } 45 | if len(vals) == 0 { 46 | return append(dst, '[', ']') 47 | } 48 | dst = append(dst, '[') 49 | dst = append(vals[0].AppendFormat(append(dst, '"'), format), '"') 50 | if len(vals) > 1 { 51 | for _, t := range vals[1:] { 52 | dst = append(t.AppendFormat(append(dst, ',', '"'), format), '"') 53 | } 54 | } 55 | dst = append(dst, ']') 56 | return dst 57 | } 58 | 59 | func appendUnixTimes(dst []byte, vals []time.Time) []byte { 60 | if len(vals) == 0 { 61 | return append(dst, '[', ']') 62 | } 63 | dst = append(dst, '[') 64 | dst = strconv.AppendInt(dst, vals[0].Unix(), 10) 65 | if len(vals) > 1 { 66 | for _, t := range vals[1:] { 67 | dst = strconv.AppendInt(append(dst, ','), t.Unix(), 10) 68 | } 69 | } 70 | dst = append(dst, ']') 71 | return dst 72 | } 73 | 74 | func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte { 75 | if len(vals) == 0 { 76 | return append(dst, '[', ']') 77 | } 78 | dst = append(dst, '[') 79 | dst = strconv.AppendInt(dst, vals[0].UnixNano()/div, 10) 80 | if len(vals) > 1 { 81 | for _, t := range vals[1:] { 82 | dst = strconv.AppendInt(append(dst, ','), t.UnixNano()/div, 10) 83 | } 84 | } 85 | dst = append(dst, ']') 86 | return dst 87 | } 88 | 89 | // AppendDuration formats the input duration with the given unit & format 90 | // and appends the encoded string to the input byte slice. 91 | func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, precision int) []byte { 92 | if useInt { 93 | return strconv.AppendInt(dst, int64(d/unit), 10) 94 | } 95 | return e.AppendFloat64(dst, float64(d)/float64(unit), precision) 96 | } 97 | 98 | // AppendDurations formats the input durations with the given unit & format 99 | // and appends the encoded string list to the input byte slice. 100 | func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []byte { 101 | if len(vals) == 0 { 102 | return append(dst, '[', ']') 103 | } 104 | dst = append(dst, '[') 105 | dst = e.AppendDuration(dst, vals[0], unit, useInt, precision) 106 | if len(vals) > 1 { 107 | for _, d := range vals[1:] { 108 | dst = e.AppendDuration(append(dst, ','), d, unit, useInt, precision) 109 | } 110 | } 111 | dst = append(dst, ']') 112 | return dst 113 | } 114 | -------------------------------------------------------------------------------- /journald/journald.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | // Package journald provides a io.Writer to send the logs 5 | // to journalD component of systemd. 6 | 7 | package journald 8 | 9 | // This file provides a zerolog writer so that logs printed 10 | // using zerolog library can be sent to a journalD. 11 | 12 | // Zerolog's Top level key/Value Pairs are translated to 13 | // journald's args - all Values are sent to journald as strings. 14 | // And all key strings are converted to uppercase before sending 15 | // to journald (as required by journald). 16 | 17 | // In addition, entire log message (all Key Value Pairs), is also 18 | // sent to journald under the key "JSON". 19 | 20 | import ( 21 | "bytes" 22 | "encoding/json" 23 | "fmt" 24 | "io" 25 | "strings" 26 | 27 | "github.com/coreos/go-systemd/v22/journal" 28 | "github.com/rs/zerolog" 29 | "github.com/rs/zerolog/internal/cbor" 30 | ) 31 | 32 | const defaultJournalDPrio = journal.PriNotice 33 | 34 | // NewJournalDWriter returns a zerolog log destination 35 | // to be used as parameter to New() calls. Writing logs 36 | // to this writer will send the log messages to journalD 37 | // running in this system. 38 | func NewJournalDWriter() io.Writer { 39 | return journalWriter{} 40 | } 41 | 42 | type journalWriter struct { 43 | } 44 | 45 | // levelToJPrio converts zerolog Level string into 46 | // journalD's priority values. JournalD has more 47 | // priorities than zerolog. 48 | func levelToJPrio(zLevel string) journal.Priority { 49 | lvl, _ := zerolog.ParseLevel(zLevel) 50 | 51 | switch lvl { 52 | case zerolog.TraceLevel: 53 | return journal.PriDebug 54 | case zerolog.DebugLevel: 55 | return journal.PriDebug 56 | case zerolog.InfoLevel: 57 | return journal.PriInfo 58 | case zerolog.WarnLevel: 59 | return journal.PriWarning 60 | case zerolog.ErrorLevel: 61 | return journal.PriErr 62 | case zerolog.FatalLevel: 63 | return journal.PriCrit 64 | case zerolog.PanicLevel: 65 | return journal.PriEmerg 66 | case zerolog.NoLevel: 67 | return journal.PriNotice 68 | } 69 | return defaultJournalDPrio 70 | } 71 | 72 | func (w journalWriter) Write(p []byte) (n int, err error) { 73 | var event map[string]interface{} 74 | origPLen := len(p) 75 | p = cbor.DecodeIfBinaryToBytes(p) 76 | d := json.NewDecoder(bytes.NewReader(p)) 77 | d.UseNumber() 78 | err = d.Decode(&event) 79 | jPrio := defaultJournalDPrio 80 | args := make(map[string]string) 81 | if err != nil { 82 | return 83 | } 84 | if l, ok := event[zerolog.LevelFieldName].(string); ok { 85 | jPrio = levelToJPrio(l) 86 | } 87 | 88 | msg := "" 89 | for key, value := range event { 90 | jKey := strings.ToUpper(key) 91 | switch key { 92 | case zerolog.LevelFieldName, zerolog.TimestampFieldName: 93 | continue 94 | case zerolog.MessageFieldName: 95 | msg, _ = value.(string) 96 | continue 97 | } 98 | 99 | switch v := value.(type) { 100 | case string: 101 | args[jKey] = v 102 | case json.Number: 103 | args[jKey] = fmt.Sprint(value) 104 | default: 105 | b, err := zerolog.InterfaceMarshalFunc(value) 106 | if err != nil { 107 | args[jKey] = fmt.Sprintf("[error: %v]", err) 108 | } else { 109 | args[jKey] = string(b) 110 | } 111 | } 112 | } 113 | args["JSON"] = string(p) 114 | err = journal.Send(msg, jPrio, args) 115 | 116 | if err == nil { 117 | n = origPLen 118 | } 119 | 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /journald/journald_test.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package journald_test 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "testing" 9 | 10 | "github.com/rs/zerolog" 11 | "github.com/rs/zerolog/journald" 12 | ) 13 | 14 | func ExampleNewJournalDWriter() { 15 | log := zerolog.New(journald.NewJournalDWriter()) 16 | log.Info().Str("foo", "bar").Uint64("small", 123).Float64("float", 3.14).Uint64("big", 1152921504606846976).Msg("Journal Test") 17 | // Output: 18 | } 19 | 20 | /* 21 | 22 | There is no automated way to verify the output - since the output is sent 23 | to journald process and method to retrieve is journalctl. Will find a way 24 | to automate the process and fix this test. 25 | 26 | $ journalctl -o verbose -f 27 | 28 | Thu 2018-04-26 22:30:20.768136 PDT [s=3284d695bde946e4b5017c77a399237f;i=329f0;b=98c0dca0debc4b98a5b9534e910e7dd6;m=7a702e35dd4;t=56acdccd2ed0a;x=4690034cf0348614] 29 | PRIORITY=6 30 | _AUDIT_LOGINUID=1000 31 | _BOOT_ID=98c0dca0debc4b98a5b9534e910e7dd6 32 | _MACHINE_ID=926ed67eb4744580948de70fb474975e 33 | _HOSTNAME=sprint 34 | _UID=1000 35 | _GID=1000 36 | _CAP_EFFECTIVE=0 37 | _SYSTEMD_SLICE=-.slice 38 | _TRANSPORT=journal 39 | _SYSTEMD_CGROUP=/ 40 | _AUDIT_SESSION=2945 41 | MESSAGE=Journal Test 42 | FOO=bar 43 | BIG=1152921504606846976 44 | _COMM=journald.test 45 | SMALL=123 46 | FLOAT=3.14 47 | JSON={"level":"info","foo":"bar","small":123,"float":3.14,"big":1152921504606846976,"message":"Journal Test"} 48 | _PID=27103 49 | _SOURCE_REALTIME_TIMESTAMP=1524807020768136 50 | */ 51 | 52 | func TestWriteReturnsNoOfWrittenBytes(t *testing.T) { 53 | input := []byte(`{"level":"info","time":1570912626,"message":"Starting..."}`) 54 | wr := journald.NewJournalDWriter() 55 | want := len(input) 56 | got, err := wr.Write(input) 57 | 58 | if err != nil { 59 | t.Errorf("Unexpected error %v", err) 60 | } 61 | 62 | if want != got { 63 | t.Errorf("Expected %d bytes to be written got %d", want, got) 64 | } 65 | } 66 | 67 | func TestMultiWrite(t *testing.T) { 68 | var ( 69 | w1 = new(bytes.Buffer) 70 | w2 = new(bytes.Buffer) 71 | w3 = journald.NewJournalDWriter() 72 | ) 73 | 74 | zerolog.ErrorHandler = func(err error) { 75 | if err == io.ErrShortWrite { 76 | t.Errorf("Unexpected ShortWriteError") 77 | t.FailNow() 78 | } 79 | } 80 | 81 | log := zerolog.New(io.MultiWriter(w1, w2, w3)).With().Logger() 82 | 83 | for i := 0; i < 10; i++ { 84 | log.Info().Msg("Tick!") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Package log provides a global logger for zerolog. 2 | package log 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/rs/zerolog" 11 | ) 12 | 13 | // Logger is the global logger. 14 | var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() 15 | 16 | // Output duplicates the global logger and sets w as its output. 17 | func Output(w io.Writer) zerolog.Logger { 18 | return Logger.Output(w) 19 | } 20 | 21 | // With creates a child logger with the field added to its context. 22 | func With() zerolog.Context { 23 | return Logger.With() 24 | } 25 | 26 | // Level creates a child logger with the minimum accepted level set to level. 27 | func Level(level zerolog.Level) zerolog.Logger { 28 | return Logger.Level(level) 29 | } 30 | 31 | // Sample returns a logger with the s sampler. 32 | func Sample(s zerolog.Sampler) zerolog.Logger { 33 | return Logger.Sample(s) 34 | } 35 | 36 | // Hook returns a logger with the h Hook. 37 | func Hook(h zerolog.Hook) zerolog.Logger { 38 | return Logger.Hook(h) 39 | } 40 | 41 | // Err starts a new message with error level with err as a field if not nil or 42 | // with info level if err is nil. 43 | // 44 | // You must call Msg on the returned event in order to send the event. 45 | func Err(err error) *zerolog.Event { 46 | return Logger.Err(err) 47 | } 48 | 49 | // Trace starts a new message with trace level. 50 | // 51 | // You must call Msg on the returned event in order to send the event. 52 | func Trace() *zerolog.Event { 53 | return Logger.Trace() 54 | } 55 | 56 | // Debug starts a new message with debug level. 57 | // 58 | // You must call Msg on the returned event in order to send the event. 59 | func Debug() *zerolog.Event { 60 | return Logger.Debug() 61 | } 62 | 63 | // Info starts a new message with info level. 64 | // 65 | // You must call Msg on the returned event in order to send the event. 66 | func Info() *zerolog.Event { 67 | return Logger.Info() 68 | } 69 | 70 | // Warn starts a new message with warn level. 71 | // 72 | // You must call Msg on the returned event in order to send the event. 73 | func Warn() *zerolog.Event { 74 | return Logger.Warn() 75 | } 76 | 77 | // Error starts a new message with error level. 78 | // 79 | // You must call Msg on the returned event in order to send the event. 80 | func Error() *zerolog.Event { 81 | return Logger.Error() 82 | } 83 | 84 | // Fatal starts a new message with fatal level. The os.Exit(1) function 85 | // is called by the Msg method. 86 | // 87 | // You must call Msg on the returned event in order to send the event. 88 | func Fatal() *zerolog.Event { 89 | return Logger.Fatal() 90 | } 91 | 92 | // Panic starts a new message with panic level. The message is also sent 93 | // to the panic function. 94 | // 95 | // You must call Msg on the returned event in order to send the event. 96 | func Panic() *zerolog.Event { 97 | return Logger.Panic() 98 | } 99 | 100 | // WithLevel starts a new message with level. 101 | // 102 | // You must call Msg on the returned event in order to send the event. 103 | func WithLevel(level zerolog.Level) *zerolog.Event { 104 | return Logger.WithLevel(level) 105 | } 106 | 107 | // Log starts a new message with no level. Setting zerolog.GlobalLevel to 108 | // zerolog.Disabled will still disable events produced by this method. 109 | // 110 | // You must call Msg on the returned event in order to send the event. 111 | func Log() *zerolog.Event { 112 | return Logger.Log() 113 | } 114 | 115 | // Print sends a log event using debug level and no extra field. 116 | // Arguments are handled in the manner of fmt.Print. 117 | func Print(v ...interface{}) { 118 | Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...)) 119 | } 120 | 121 | // Printf sends a log event using debug level and no extra field. 122 | // Arguments are handled in the manner of fmt.Printf. 123 | func Printf(format string, v ...interface{}) { 124 | Logger.Debug().CallerSkipFrame(1).Msgf(format, v...) 125 | } 126 | 127 | // Ctx returns the Logger associated with the ctx. If no logger 128 | // is associated, a disabled logger is returned. 129 | func Ctx(ctx context.Context) *zerolog.Logger { 130 | return zerolog.Ctx(ctx) 131 | } 132 | -------------------------------------------------------------------------------- /log/log_example_test.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | 3 | package log_test 4 | 5 | import ( 6 | "errors" 7 | "flag" 8 | "os" 9 | "time" 10 | 11 | "github.com/rs/zerolog" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | // setup would normally be an init() function, however, there seems 16 | // to be something awry with the testing framework when we set the 17 | // global Logger from an init() 18 | func setup() { 19 | // UNIX Time is faster and smaller than most timestamps 20 | // If you set zerolog.TimeFieldFormat to an empty string, 21 | // logs will write with UNIX time 22 | zerolog.TimeFieldFormat = "" 23 | // In order to always output a static time to stdout for these 24 | // examples to pass, we need to override zerolog.TimestampFunc 25 | // and log.Logger globals -- you would not normally need to do this 26 | zerolog.TimestampFunc = func() time.Time { 27 | return time.Date(2008, 1, 8, 17, 5, 05, 0, time.UTC) 28 | } 29 | log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger() 30 | } 31 | 32 | // Simple logging example using the Print function in the log package 33 | // Note that both Print and Printf are at the debug log level by default 34 | func ExamplePrint() { 35 | setup() 36 | 37 | log.Print("hello world") 38 | // Output: {"level":"debug","time":1199811905,"message":"hello world"} 39 | } 40 | 41 | // Simple logging example using the Printf function in the log package 42 | func ExamplePrintf() { 43 | setup() 44 | 45 | log.Printf("hello %s", "world") 46 | // Output: {"level":"debug","time":1199811905,"message":"hello world"} 47 | } 48 | 49 | // Example of a log with no particular "level" 50 | func ExampleLog() { 51 | setup() 52 | log.Log().Msg("hello world") 53 | 54 | // Output: {"time":1199811905,"message":"hello world"} 55 | } 56 | 57 | // Example of a conditional level based on the presence of an error. 58 | func ExampleErr() { 59 | setup() 60 | err := errors.New("some error") 61 | log.Err(err).Msg("hello world") 62 | log.Err(nil).Msg("hello world") 63 | 64 | // Output: {"level":"error","error":"some error","time":1199811905,"message":"hello world"} 65 | // {"level":"info","time":1199811905,"message":"hello world"} 66 | } 67 | 68 | // Example of a log at a particular "level" (in this case, "trace") 69 | func ExampleTrace() { 70 | setup() 71 | log.Trace().Msg("hello world") 72 | 73 | // Output: {"level":"trace","time":1199811905,"message":"hello world"} 74 | } 75 | 76 | // Example of a log at a particular "level" (in this case, "debug") 77 | func ExampleDebug() { 78 | setup() 79 | log.Debug().Msg("hello world") 80 | 81 | // Output: {"level":"debug","time":1199811905,"message":"hello world"} 82 | } 83 | 84 | // Example of a log at a particular "level" (in this case, "info") 85 | func ExampleInfo() { 86 | setup() 87 | log.Info().Msg("hello world") 88 | 89 | // Output: {"level":"info","time":1199811905,"message":"hello world"} 90 | } 91 | 92 | // Example of a log at a particular "level" (in this case, "warn") 93 | func ExampleWarn() { 94 | setup() 95 | log.Warn().Msg("hello world") 96 | 97 | // Output: {"level":"warn","time":1199811905,"message":"hello world"} 98 | } 99 | 100 | // Example of a log at a particular "level" (in this case, "error") 101 | func ExampleError() { 102 | setup() 103 | log.Error().Msg("hello world") 104 | 105 | // Output: {"level":"error","time":1199811905,"message":"hello world"} 106 | } 107 | 108 | // Example of a log at a particular "level" (in this case, "fatal") 109 | func ExampleFatal() { 110 | setup() 111 | err := errors.New("A repo man spends his life getting into tense situations") 112 | service := "myservice" 113 | 114 | log.Fatal(). 115 | Err(err). 116 | Str("service", service). 117 | Msgf("Cannot start %s", service) 118 | 119 | // Outputs: {"level":"fatal","time":1199811905,"error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"} 120 | } 121 | 122 | // TODO: Panic 123 | 124 | // This example uses command-line flags to demonstrate various outputs 125 | // depending on the chosen log level. 126 | func Example() { 127 | setup() 128 | debug := flag.Bool("debug", false, "sets log level to debug") 129 | 130 | flag.Parse() 131 | 132 | // Default level for this example is info, unless debug flag is present 133 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 134 | if *debug { 135 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 136 | } 137 | 138 | log.Debug().Msg("This message appears only when log level set to Debug") 139 | log.Info().Msg("This message appears when log level set to Debug or Info") 140 | 141 | if e := log.Debug(); e.Enabled() { 142 | // Compute log output only if enabled. 143 | value := "bar" 144 | e.Str("foo", value).Msg("some debug message") 145 | } 146 | 147 | // Output: {"level":"info","time":1199811905,"message":"This message appears when log level set to Debug or Info"} 148 | } 149 | 150 | // TODO: Output 151 | 152 | // TODO: With 153 | 154 | // TODO: Level 155 | 156 | // TODO: Sample 157 | 158 | // TODO: Hook 159 | 160 | // TODO: WithLevel 161 | 162 | // TODO: Ctx 163 | -------------------------------------------------------------------------------- /not_go112.go: -------------------------------------------------------------------------------- 1 | // +build !go1.12 2 | 3 | package zerolog 4 | 5 | const contextCallerSkipFrameCount = 3 6 | -------------------------------------------------------------------------------- /pkgerrors/stacktrace.go: -------------------------------------------------------------------------------- 1 | package pkgerrors 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | var ( 8 | StackSourceFileName = "source" 9 | StackSourceLineName = "line" 10 | StackSourceFunctionName = "func" 11 | ) 12 | 13 | type state struct { 14 | b []byte 15 | } 16 | 17 | // Write implement fmt.Formatter interface. 18 | func (s *state) Write(b []byte) (n int, err error) { 19 | s.b = b 20 | return len(b), nil 21 | } 22 | 23 | // Width implement fmt.Formatter interface. 24 | func (s *state) Width() (wid int, ok bool) { 25 | return 0, false 26 | } 27 | 28 | // Precision implement fmt.Formatter interface. 29 | func (s *state) Precision() (prec int, ok bool) { 30 | return 0, false 31 | } 32 | 33 | // Flag implement fmt.Formatter interface. 34 | func (s *state) Flag(c int) bool { 35 | return false 36 | } 37 | 38 | func frameField(f errors.Frame, s *state, c rune) string { 39 | f.Format(s, c) 40 | return string(s.b) 41 | } 42 | 43 | // MarshalStack implements pkg/errors stack trace marshaling. 44 | // 45 | // zerolog.ErrorStackMarshaler = MarshalStack 46 | func MarshalStack(err error) interface{} { 47 | type stackTracer interface { 48 | StackTrace() errors.StackTrace 49 | } 50 | var sterr stackTracer 51 | var ok bool 52 | for err != nil { 53 | sterr, ok = err.(stackTracer) 54 | if ok { 55 | break 56 | } 57 | 58 | u, ok := err.(interface { 59 | Unwrap() error 60 | }) 61 | if !ok { 62 | return nil 63 | } 64 | 65 | err = u.Unwrap() 66 | } 67 | if sterr == nil { 68 | return nil 69 | } 70 | 71 | st := sterr.StackTrace() 72 | s := &state{} 73 | out := make([]map[string]string, 0, len(st)) 74 | for _, frame := range st { 75 | out = append(out, map[string]string{ 76 | StackSourceFileName: frameField(frame, s, 's'), 77 | StackSourceLineName: frameField(frame, s, 'd'), 78 | StackSourceFunctionName: frameField(frame, s, 'n'), 79 | }) 80 | } 81 | return out 82 | } 83 | -------------------------------------------------------------------------------- /pkgerrors/stacktrace_test.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | 3 | package pkgerrors 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | func TestLogStack(t *testing.T) { 16 | zerolog.ErrorStackMarshaler = MarshalStack 17 | 18 | out := &bytes.Buffer{} 19 | log := zerolog.New(out) 20 | 21 | err := fmt.Errorf("from error: %w", errors.New("error message")) 22 | log.Log().Stack().Err(err).Msg("") 23 | 24 | got := out.String() 25 | want := `\{"stack":\[\{"func":"TestLogStack","line":"21","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n` 26 | if ok, _ := regexp.MatchString(want, got); !ok { 27 | t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) 28 | } 29 | } 30 | 31 | func TestLogStackFields(t *testing.T) { 32 | zerolog.ErrorStackMarshaler = MarshalStack 33 | 34 | out := &bytes.Buffer{} 35 | log := zerolog.New(out) 36 | 37 | err := fmt.Errorf("from error: %w", errors.New("error message")) 38 | log.Log().Stack().Fields([]interface{}{"error", err}).Msg("") 39 | 40 | got := out.String() 41 | want := `\{"error":"from error: error message","stack":\[\{"func":"TestLogStackFields","line":"37","source":"stacktrace_test.go"\},.*\]\}\n` 42 | if ok, _ := regexp.MatchString(want, got); !ok { 43 | t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) 44 | } 45 | } 46 | 47 | func TestLogStackFromContext(t *testing.T) { 48 | zerolog.ErrorStackMarshaler = MarshalStack 49 | 50 | out := &bytes.Buffer{} 51 | log := zerolog.New(out).With().Stack().Logger() // calling Stack() on log context instead of event 52 | 53 | err := fmt.Errorf("from error: %w", errors.New("error message")) 54 | log.Log().Err(err).Msg("") // not explicitly calling Stack() 55 | 56 | got := out.String() 57 | want := `\{"stack":\[\{"func":"TestLogStackFromContext","line":"53","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n` 58 | if ok, _ := regexp.MatchString(want, got); !ok { 59 | t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) 60 | } 61 | } 62 | 63 | func TestLogStackFromContextWith(t *testing.T) { 64 | zerolog.ErrorStackMarshaler = MarshalStack 65 | 66 | err := fmt.Errorf("from error: %w", errors.New("error message")) 67 | out := &bytes.Buffer{} 68 | log := zerolog.New(out).With().Stack().Err(err).Logger() // calling Stack() on log context instead of event 69 | 70 | log.Error().Msg("") 71 | 72 | got := out.String() 73 | want := `\{"level":"error","stack":\[\{"func":"TestLogStackFromContextWith","line":"66","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n` 74 | if ok, _ := regexp.MatchString(want, got); !ok { 75 | t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) 76 | } 77 | } 78 | 79 | func BenchmarkLogStack(b *testing.B) { 80 | zerolog.ErrorStackMarshaler = MarshalStack 81 | out := &bytes.Buffer{} 82 | log := zerolog.New(out) 83 | err := errors.Wrap(errors.New("error message"), "from error") 84 | b.ReportAllocs() 85 | 86 | for i := 0; i < b.N; i++ { 87 | log.Log().Stack().Err(err).Msg("") 88 | out.Reset() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pretty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rs/zerolog/9dacc014f38d60f563c2ab18719aec11fc06765c/pretty.png -------------------------------------------------------------------------------- /sampler.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // Often samples log every ~ 10 events. 11 | Often = RandomSampler(10) 12 | // Sometimes samples log every ~ 100 events. 13 | Sometimes = RandomSampler(100) 14 | // Rarely samples log every ~ 1000 events. 15 | Rarely = RandomSampler(1000) 16 | ) 17 | 18 | // Sampler defines an interface to a log sampler. 19 | type Sampler interface { 20 | // Sample returns true if the event should be part of the sample, false if 21 | // the event should be dropped. 22 | Sample(lvl Level) bool 23 | } 24 | 25 | // RandomSampler use a PRNG to randomly sample an event out of N events, 26 | // regardless of their level. 27 | type RandomSampler uint32 28 | 29 | // Sample implements the Sampler interface. 30 | func (s RandomSampler) Sample(lvl Level) bool { 31 | if s <= 0 { 32 | return false 33 | } 34 | if rand.Intn(int(s)) != 0 { 35 | return false 36 | } 37 | return true 38 | } 39 | 40 | // BasicSampler is a sampler that will send every Nth events, regardless of 41 | // their level. 42 | type BasicSampler struct { 43 | N uint32 44 | counter uint32 45 | } 46 | 47 | // Sample implements the Sampler interface. 48 | func (s *BasicSampler) Sample(lvl Level) bool { 49 | n := s.N 50 | if n == 0 { 51 | return false 52 | } 53 | if n == 1 { 54 | return true 55 | } 56 | c := atomic.AddUint32(&s.counter, 1) 57 | return c%n == 1 58 | } 59 | 60 | // BurstSampler lets Burst events pass per Period then pass the decision to 61 | // NextSampler. If Sampler is not set, all subsequent events are rejected. 62 | type BurstSampler struct { 63 | // Burst is the maximum number of event per period allowed before calling 64 | // NextSampler. 65 | Burst uint32 66 | // Period defines the burst period. If 0, NextSampler is always called. 67 | Period time.Duration 68 | // NextSampler is the sampler used after the burst is reached. If nil, 69 | // events are always rejected after the burst. 70 | NextSampler Sampler 71 | 72 | counter uint32 73 | resetAt int64 74 | } 75 | 76 | // Sample implements the Sampler interface. 77 | func (s *BurstSampler) Sample(lvl Level) bool { 78 | if s.Burst > 0 && s.Period > 0 { 79 | if s.inc() <= s.Burst { 80 | return true 81 | } 82 | } 83 | if s.NextSampler == nil { 84 | return false 85 | } 86 | return s.NextSampler.Sample(lvl) 87 | } 88 | 89 | func (s *BurstSampler) inc() uint32 { 90 | now := TimestampFunc().UnixNano() 91 | resetAt := atomic.LoadInt64(&s.resetAt) 92 | var c uint32 93 | if now >= resetAt { 94 | c = 1 95 | atomic.StoreUint32(&s.counter, c) 96 | newResetAt := now + s.Period.Nanoseconds() 97 | reset := atomic.CompareAndSwapInt64(&s.resetAt, resetAt, newResetAt) 98 | if !reset { 99 | // Lost the race with another goroutine trying to reset. 100 | c = atomic.AddUint32(&s.counter, 1) 101 | } 102 | } else { 103 | c = atomic.AddUint32(&s.counter, 1) 104 | } 105 | return c 106 | } 107 | 108 | // LevelSampler applies a different sampler for each level. 109 | type LevelSampler struct { 110 | TraceSampler, DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler 111 | } 112 | 113 | func (s LevelSampler) Sample(lvl Level) bool { 114 | switch lvl { 115 | case TraceLevel: 116 | if s.TraceSampler != nil { 117 | return s.TraceSampler.Sample(lvl) 118 | } 119 | case DebugLevel: 120 | if s.DebugSampler != nil { 121 | return s.DebugSampler.Sample(lvl) 122 | } 123 | case InfoLevel: 124 | if s.InfoSampler != nil { 125 | return s.InfoSampler.Sample(lvl) 126 | } 127 | case WarnLevel: 128 | if s.WarnSampler != nil { 129 | return s.WarnSampler.Sample(lvl) 130 | } 131 | case ErrorLevel: 132 | if s.ErrorSampler != nil { 133 | return s.ErrorSampler.Sample(lvl) 134 | } 135 | } 136 | return true 137 | } 138 | -------------------------------------------------------------------------------- /sampler_test.go: -------------------------------------------------------------------------------- 1 | //go:build !binary_log 2 | // +build !binary_log 3 | 4 | package zerolog 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var samplers = []struct { 12 | name string 13 | sampler func() Sampler 14 | total int 15 | wantMin int 16 | wantMax int 17 | }{ 18 | { 19 | "BasicSampler_1", 20 | func() Sampler { 21 | return &BasicSampler{N: 1} 22 | }, 23 | 100, 100, 100, 24 | }, 25 | { 26 | "BasicSampler_5", 27 | func() Sampler { 28 | return &BasicSampler{N: 5} 29 | }, 30 | 100, 20, 20, 31 | }, 32 | { 33 | "BasicSampler_0", 34 | func() Sampler { 35 | return &BasicSampler{N: 0} 36 | }, 37 | 100, 0, 0, 38 | }, 39 | { 40 | "RandomSampler", 41 | func() Sampler { 42 | return RandomSampler(5) 43 | }, 44 | 100, 10, 30, 45 | }, 46 | { 47 | "RandomSampler_0", 48 | func() Sampler { 49 | return RandomSampler(0) 50 | }, 51 | 100, 0, 0, 52 | }, 53 | { 54 | "BurstSampler", 55 | func() Sampler { 56 | return &BurstSampler{Burst: 20, Period: time.Second} 57 | }, 58 | 100, 20, 20, 59 | }, 60 | { 61 | "BurstSampler_0", 62 | func() Sampler { 63 | return &BurstSampler{Burst: 0, Period: time.Second} 64 | }, 65 | 100, 0, 0, 66 | }, 67 | { 68 | "BurstSamplerNext", 69 | func() Sampler { 70 | return &BurstSampler{Burst: 20, Period: time.Second, NextSampler: &BasicSampler{N: 5}} 71 | }, 72 | 120, 40, 40, 73 | }, 74 | } 75 | 76 | func TestSamplers(t *testing.T) { 77 | for i := range samplers { 78 | s := samplers[i] 79 | t.Run(s.name, func(t *testing.T) { 80 | sampler := s.sampler() 81 | got := 0 82 | for t := s.total; t > 0; t-- { 83 | if sampler.Sample(0) { 84 | got++ 85 | } 86 | } 87 | if got < s.wantMin || got > s.wantMax { 88 | t.Errorf("%s.Sample(0) == true %d on %d, want [%d, %d]", s.name, got, s.total, s.wantMin, s.wantMax) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func BenchmarkSamplers(b *testing.B) { 95 | for i := range samplers { 96 | s := samplers[i] 97 | b.Run(s.name, func(b *testing.B) { 98 | sampler := s.sampler() 99 | b.RunParallel(func(pb *testing.PB) { 100 | for pb.Next() { 101 | sampler.Sample(0) 102 | } 103 | }) 104 | }) 105 | } 106 | } 107 | 108 | func TestBurst(t *testing.T) { 109 | sampler := &BurstSampler{Burst: 1, Period: time.Second} 110 | 111 | t0 := time.Now() 112 | now := t0 113 | mockedTime := func() time.Time { 114 | return now 115 | } 116 | 117 | TimestampFunc = mockedTime 118 | defer func() { TimestampFunc = time.Now }() 119 | 120 | scenario := []struct { 121 | tm time.Time 122 | want bool 123 | }{ 124 | {t0, true}, 125 | {t0.Add(time.Second - time.Nanosecond), false}, 126 | {t0.Add(time.Second), true}, 127 | {t0.Add(time.Second + time.Nanosecond), false}, 128 | } 129 | 130 | for i, step := range scenario { 131 | now = step.tm 132 | got := sampler.Sample(NoLevel) 133 | if got != step.want { 134 | t.Errorf("step %d (t=%s): expect %t got %t", i, step.tm, step.want, got) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /syslog.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | // +build !binary_log 3 | 4 | package zerolog 5 | 6 | import ( 7 | "io" 8 | ) 9 | 10 | // See http://cee.mitre.org/language/1.0-beta1/clt.html#syslog 11 | // or https://www.rsyslog.com/json-elasticsearch/ 12 | const ceePrefix = "@cee:" 13 | 14 | // SyslogWriter is an interface matching a syslog.Writer struct. 15 | type SyslogWriter interface { 16 | io.Writer 17 | Debug(m string) error 18 | Info(m string) error 19 | Warning(m string) error 20 | Err(m string) error 21 | Emerg(m string) error 22 | Crit(m string) error 23 | } 24 | 25 | type syslogWriter struct { 26 | w SyslogWriter 27 | prefix string 28 | } 29 | 30 | // SyslogLevelWriter wraps a SyslogWriter and call the right syslog level 31 | // method matching the zerolog level. 32 | func SyslogLevelWriter(w SyslogWriter) LevelWriter { 33 | return syslogWriter{w, ""} 34 | } 35 | 36 | // SyslogCEEWriter wraps a SyslogWriter with a SyslogLevelWriter that adds a 37 | // MITRE CEE prefix for JSON syslog entries, compatible with rsyslog 38 | // and syslog-ng JSON logging support. 39 | // See https://www.rsyslog.com/json-elasticsearch/ 40 | func SyslogCEEWriter(w SyslogWriter) LevelWriter { 41 | return syslogWriter{w, ceePrefix} 42 | } 43 | 44 | func (sw syslogWriter) Write(p []byte) (n int, err error) { 45 | var pn int 46 | if sw.prefix != "" { 47 | pn, err = sw.w.Write([]byte(sw.prefix)) 48 | if err != nil { 49 | return pn, err 50 | } 51 | } 52 | n, err = sw.w.Write(p) 53 | return pn + n, err 54 | } 55 | 56 | // WriteLevel implements LevelWriter interface. 57 | func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) { 58 | switch level { 59 | case TraceLevel: 60 | case DebugLevel: 61 | err = sw.w.Debug(sw.prefix + string(p)) 62 | case InfoLevel: 63 | err = sw.w.Info(sw.prefix + string(p)) 64 | case WarnLevel: 65 | err = sw.w.Warning(sw.prefix + string(p)) 66 | case ErrorLevel: 67 | err = sw.w.Err(sw.prefix + string(p)) 68 | case FatalLevel: 69 | err = sw.w.Emerg(sw.prefix + string(p)) 70 | case PanicLevel: 71 | err = sw.w.Crit(sw.prefix + string(p)) 72 | case NoLevel: 73 | err = sw.w.Info(sw.prefix + string(p)) 74 | default: 75 | panic("invalid level") 76 | } 77 | // Any CEE prefix is not part of the message, so we don't include its length 78 | n = len(p) 79 | return 80 | } 81 | 82 | // Call the underlying writer's Close method if it is an io.Closer. Otherwise 83 | // does nothing. 84 | func (sw syslogWriter) Close() error { 85 | if c, ok := sw.w.(io.Closer); ok { 86 | return c.Close() 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /syslog_test.go: -------------------------------------------------------------------------------- 1 | // +build !binary_log 2 | // +build !windows 3 | 4 | package zerolog 5 | 6 | import ( 7 | "bytes" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type syslogEvent struct { 14 | level string 15 | msg string 16 | } 17 | type syslogTestWriter struct { 18 | events []syslogEvent 19 | } 20 | 21 | func (w *syslogTestWriter) Write(p []byte) (int, error) { 22 | return 0, nil 23 | } 24 | func (w *syslogTestWriter) Trace(m string) error { 25 | w.events = append(w.events, syslogEvent{"Trace", m}) 26 | return nil 27 | } 28 | func (w *syslogTestWriter) Debug(m string) error { 29 | w.events = append(w.events, syslogEvent{"Debug", m}) 30 | return nil 31 | } 32 | func (w *syslogTestWriter) Info(m string) error { 33 | w.events = append(w.events, syslogEvent{"Info", m}) 34 | return nil 35 | } 36 | func (w *syslogTestWriter) Warning(m string) error { 37 | w.events = append(w.events, syslogEvent{"Warning", m}) 38 | return nil 39 | } 40 | func (w *syslogTestWriter) Err(m string) error { 41 | w.events = append(w.events, syslogEvent{"Err", m}) 42 | return nil 43 | } 44 | func (w *syslogTestWriter) Emerg(m string) error { 45 | w.events = append(w.events, syslogEvent{"Emerg", m}) 46 | return nil 47 | } 48 | func (w *syslogTestWriter) Crit(m string) error { 49 | w.events = append(w.events, syslogEvent{"Crit", m}) 50 | return nil 51 | } 52 | 53 | func TestSyslogWriter(t *testing.T) { 54 | sw := &syslogTestWriter{} 55 | log := New(SyslogLevelWriter(sw)) 56 | log.Trace().Msg("trace") 57 | log.Debug().Msg("debug") 58 | log.Info().Msg("info") 59 | log.Warn().Msg("warn") 60 | log.Error().Msg("error") 61 | log.Log().Msg("nolevel") 62 | want := []syslogEvent{ 63 | {"Debug", `{"level":"debug","message":"debug"}` + "\n"}, 64 | {"Info", `{"level":"info","message":"info"}` + "\n"}, 65 | {"Warning", `{"level":"warn","message":"warn"}` + "\n"}, 66 | {"Err", `{"level":"error","message":"error"}` + "\n"}, 67 | {"Info", `{"message":"nolevel"}` + "\n"}, 68 | } 69 | if got := sw.events; !reflect.DeepEqual(got, want) { 70 | t.Errorf("Invalid syslog message routing: want %v, got %v", want, got) 71 | } 72 | } 73 | 74 | type testCEEwriter struct { 75 | buf *bytes.Buffer 76 | } 77 | 78 | // Only implement one method as we're just testing the prefixing 79 | func (c testCEEwriter) Debug(m string) error { return nil } 80 | 81 | func (c testCEEwriter) Info(m string) error { 82 | _, err := c.buf.Write([]byte(m)) 83 | return err 84 | } 85 | 86 | func (c testCEEwriter) Warning(m string) error { return nil } 87 | 88 | func (c testCEEwriter) Err(m string) error { return nil } 89 | 90 | func (c testCEEwriter) Emerg(m string) error { return nil } 91 | 92 | func (c testCEEwriter) Crit(m string) error { return nil } 93 | 94 | func (c testCEEwriter) Write(b []byte) (int, error) { 95 | return c.buf.Write(b) 96 | } 97 | 98 | func TestSyslogWriter_WithCEE(t *testing.T) { 99 | var buf bytes.Buffer 100 | sw := testCEEwriter{&buf} 101 | log := New(SyslogCEEWriter(sw)) 102 | log.Info().Str("key", "value").Msg("message string") 103 | got := buf.String() 104 | want := "@cee:{" 105 | if !strings.HasPrefix(got, want) { 106 | t.Errorf("Bad CEE message start: want %v, got %v", want, got) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "path" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | // LevelWriter defines as interface a writer may implement in order 14 | // to receive level information with payload. 15 | type LevelWriter interface { 16 | io.Writer 17 | WriteLevel(level Level, p []byte) (n int, err error) 18 | } 19 | 20 | // LevelWriterAdapter adapts an io.Writer to support the LevelWriter interface. 21 | type LevelWriterAdapter struct { 22 | io.Writer 23 | } 24 | 25 | // WriteLevel simply writes everything to the adapted writer, ignoring the level. 26 | func (lw LevelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) { 27 | return lw.Write(p) 28 | } 29 | 30 | // Call the underlying writer's Close method if it is an io.Closer. Otherwise 31 | // does nothing. 32 | func (lw LevelWriterAdapter) Close() error { 33 | if closer, ok := lw.Writer.(io.Closer); ok { 34 | return closer.Close() 35 | } 36 | return nil 37 | } 38 | 39 | type syncWriter struct { 40 | mu sync.Mutex 41 | lw LevelWriter 42 | } 43 | 44 | // SyncWriter wraps w so that each call to Write is synchronized with a mutex. 45 | // This syncer can be used to wrap the call to writer's Write method if it is 46 | // not thread safe. Note that you do not need this wrapper for os.File Write 47 | // operations on POSIX and Windows systems as they are already thread-safe. 48 | func SyncWriter(w io.Writer) io.Writer { 49 | if lw, ok := w.(LevelWriter); ok { 50 | return &syncWriter{lw: lw} 51 | } 52 | return &syncWriter{lw: LevelWriterAdapter{w}} 53 | } 54 | 55 | // Write implements the io.Writer interface. 56 | func (s *syncWriter) Write(p []byte) (n int, err error) { 57 | s.mu.Lock() 58 | defer s.mu.Unlock() 59 | return s.lw.Write(p) 60 | } 61 | 62 | // WriteLevel implements the LevelWriter interface. 63 | func (s *syncWriter) WriteLevel(l Level, p []byte) (n int, err error) { 64 | s.mu.Lock() 65 | defer s.mu.Unlock() 66 | return s.lw.WriteLevel(l, p) 67 | } 68 | 69 | func (s *syncWriter) Close() error { 70 | s.mu.Lock() 71 | defer s.mu.Unlock() 72 | if closer, ok := s.lw.(io.Closer); ok { 73 | return closer.Close() 74 | } 75 | return nil 76 | } 77 | 78 | type multiLevelWriter struct { 79 | writers []LevelWriter 80 | } 81 | 82 | func (t multiLevelWriter) Write(p []byte) (n int, err error) { 83 | for _, w := range t.writers { 84 | if _n, _err := w.Write(p); err == nil { 85 | n = _n 86 | if _err != nil { 87 | err = _err 88 | } else if _n != len(p) { 89 | err = io.ErrShortWrite 90 | } 91 | } 92 | } 93 | return n, err 94 | } 95 | 96 | func (t multiLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) { 97 | for _, w := range t.writers { 98 | if _n, _err := w.WriteLevel(l, p); err == nil { 99 | n = _n 100 | if _err != nil { 101 | err = _err 102 | } else if _n != len(p) { 103 | err = io.ErrShortWrite 104 | } 105 | } 106 | } 107 | return n, err 108 | } 109 | 110 | // Calls close on all the underlying writers that are io.Closers. If any of the 111 | // Close methods return an error, the remainder of the closers are not closed 112 | // and the error is returned. 113 | func (t multiLevelWriter) Close() error { 114 | for _, w := range t.writers { 115 | if closer, ok := w.(io.Closer); ok { 116 | if err := closer.Close(); err != nil { 117 | return err 118 | } 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | // MultiLevelWriter creates a writer that duplicates its writes to all the 125 | // provided writers, similar to the Unix tee(1) command. If some writers 126 | // implement LevelWriter, their WriteLevel method will be used instead of Write. 127 | func MultiLevelWriter(writers ...io.Writer) LevelWriter { 128 | lwriters := make([]LevelWriter, 0, len(writers)) 129 | for _, w := range writers { 130 | if lw, ok := w.(LevelWriter); ok { 131 | lwriters = append(lwriters, lw) 132 | } else { 133 | lwriters = append(lwriters, LevelWriterAdapter{w}) 134 | } 135 | } 136 | return multiLevelWriter{lwriters} 137 | } 138 | 139 | // TestingLog is the logging interface of testing.TB. 140 | type TestingLog interface { 141 | Log(args ...interface{}) 142 | Logf(format string, args ...interface{}) 143 | Helper() 144 | } 145 | 146 | // TestWriter is a writer that writes to testing.TB. 147 | type TestWriter struct { 148 | T TestingLog 149 | 150 | // Frame skips caller frames to capture the original file and line numbers. 151 | Frame int 152 | } 153 | 154 | // NewTestWriter creates a writer that logs to the testing.TB. 155 | func NewTestWriter(t TestingLog) TestWriter { 156 | return TestWriter{T: t} 157 | } 158 | 159 | // Write to testing.TB. 160 | func (t TestWriter) Write(p []byte) (n int, err error) { 161 | t.T.Helper() 162 | 163 | n = len(p) 164 | 165 | // Strip trailing newline because t.Log always adds one. 166 | p = bytes.TrimRight(p, "\n") 167 | 168 | // Try to correct the log file and line number to the caller. 169 | if t.Frame > 0 { 170 | _, origFile, origLine, _ := runtime.Caller(1) 171 | _, frameFile, frameLine, ok := runtime.Caller(1 + t.Frame) 172 | if ok { 173 | erase := strings.Repeat("\b", len(path.Base(origFile))+len(strconv.Itoa(origLine))+3) 174 | t.T.Logf("%s%s:%d: %s", erase, path.Base(frameFile), frameLine, p) 175 | return n, err 176 | } 177 | } 178 | t.T.Log(string(p)) 179 | 180 | return n, err 181 | } 182 | 183 | // ConsoleTestWriter creates an option that correctly sets the file frame depth for testing.TB log. 184 | func ConsoleTestWriter(t TestingLog) func(w *ConsoleWriter) { 185 | return func(w *ConsoleWriter) { 186 | w.Out = TestWriter{T: t, Frame: 6} 187 | } 188 | } 189 | 190 | // FilteredLevelWriter writes only logs at Level or above to Writer. 191 | // 192 | // It should be used only in combination with MultiLevelWriter when you 193 | // want to write to multiple destinations at different levels. Otherwise 194 | // you should just set the level on the logger and filter events early. 195 | // When using MultiLevelWriter then you set the level on the logger to 196 | // the lowest of the levels you use for writers. 197 | type FilteredLevelWriter struct { 198 | Writer LevelWriter 199 | Level Level 200 | } 201 | 202 | // Write writes to the underlying Writer. 203 | func (w *FilteredLevelWriter) Write(p []byte) (int, error) { 204 | return w.Writer.Write(p) 205 | } 206 | 207 | // WriteLevel calls WriteLevel of the underlying Writer only if the level is equal 208 | // or above the Level. 209 | func (w *FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error) { 210 | if level >= w.Level { 211 | return w.Writer.WriteLevel(level, p) 212 | } 213 | return len(p), nil 214 | } 215 | 216 | // Call the underlying writer's Close method if it is an io.Closer. Otherwise 217 | // does nothing. 218 | func (w *FilteredLevelWriter) Close() error { 219 | if closer, ok := w.Writer.(io.Closer); ok { 220 | return closer.Close() 221 | } 222 | return nil 223 | } 224 | 225 | var triggerWriterPool = &sync.Pool{ 226 | New: func() interface{} { 227 | return bytes.NewBuffer(make([]byte, 0, 1024)) 228 | }, 229 | } 230 | 231 | // TriggerLevelWriter buffers log lines at the ConditionalLevel or below 232 | // until a trigger level (or higher) line is emitted. Log lines with level 233 | // higher than ConditionalLevel are always written out to the destination 234 | // writer. If trigger never happens, buffered log lines are never written out. 235 | // 236 | // It can be used to configure "log level per request". 237 | type TriggerLevelWriter struct { 238 | // Destination writer. If LevelWriter is provided (usually), its WriteLevel is used 239 | // instead of Write. 240 | io.Writer 241 | 242 | // ConditionalLevel is the level (and below) at which lines are buffered until 243 | // a trigger level (or higher) line is emitted. Usually this is set to DebugLevel. 244 | ConditionalLevel Level 245 | 246 | // TriggerLevel is the lowest level that triggers the sending of the conditional 247 | // level lines. Usually this is set to ErrorLevel. 248 | TriggerLevel Level 249 | 250 | buf *bytes.Buffer 251 | triggered bool 252 | mu sync.Mutex 253 | } 254 | 255 | func (w *TriggerLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) { 256 | w.mu.Lock() 257 | defer w.mu.Unlock() 258 | 259 | // At first trigger level or above log line, we flush the buffer and change the 260 | // trigger state to triggered. 261 | if !w.triggered && l >= w.TriggerLevel { 262 | err := w.trigger() 263 | if err != nil { 264 | return 0, err 265 | } 266 | } 267 | 268 | // Unless triggered, we buffer everything at and below ConditionalLevel. 269 | if !w.triggered && l <= w.ConditionalLevel { 270 | if w.buf == nil { 271 | w.buf = triggerWriterPool.Get().(*bytes.Buffer) 272 | } 273 | 274 | // We prefix each log line with a byte with the level. 275 | // Hopefully we will never have a level value which equals a newline 276 | // (which could interfere with reconstruction of log lines in the trigger method). 277 | w.buf.WriteByte(byte(l)) 278 | w.buf.Write(p) 279 | return len(p), nil 280 | } 281 | 282 | // Anything above ConditionalLevel is always passed through. 283 | // Once triggered, everything is passed through. 284 | if lw, ok := w.Writer.(LevelWriter); ok { 285 | return lw.WriteLevel(l, p) 286 | } 287 | return w.Write(p) 288 | } 289 | 290 | // trigger expects lock to be held. 291 | func (w *TriggerLevelWriter) trigger() error { 292 | if w.triggered { 293 | return nil 294 | } 295 | w.triggered = true 296 | 297 | if w.buf == nil { 298 | return nil 299 | } 300 | 301 | p := w.buf.Bytes() 302 | for len(p) > 0 { 303 | // We do not use bufio.Scanner here because we already have full buffer 304 | // in the memory and we do not want extra copying from the buffer to 305 | // scanner's token slice, nor we want to hit scanner's token size limit, 306 | // and we also want to preserve newlines. 307 | i := bytes.IndexByte(p, '\n') 308 | line := p[0 : i+1] 309 | p = p[i+1:] 310 | // We prefixed each log line with a byte with the level. 311 | level := Level(line[0]) 312 | line = line[1:] 313 | var err error 314 | if lw, ok := w.Writer.(LevelWriter); ok { 315 | _, err = lw.WriteLevel(level, line) 316 | } else { 317 | _, err = w.Write(line) 318 | } 319 | if err != nil { 320 | return err 321 | } 322 | } 323 | 324 | return nil 325 | } 326 | 327 | // Trigger forces flushing the buffer and change the trigger state to 328 | // triggered, if the writer has not already been triggered before. 329 | func (w *TriggerLevelWriter) Trigger() error { 330 | w.mu.Lock() 331 | defer w.mu.Unlock() 332 | 333 | return w.trigger() 334 | } 335 | 336 | // Close closes the writer and returns the buffer to the pool. 337 | func (w *TriggerLevelWriter) Close() error { 338 | w.mu.Lock() 339 | defer w.mu.Unlock() 340 | 341 | if w.buf == nil { 342 | return nil 343 | } 344 | 345 | // We return the buffer only if it has not grown above the limit. 346 | // This prevents accumulation of large buffers in the pool just 347 | // because occasionally a large buffer might be needed. 348 | if w.buf.Cap() <= TriggerLevelWriterBufferReuseLimit { 349 | w.buf.Reset() 350 | triggerWriterPool.Put(w.buf) 351 | } 352 | w.buf = nil 353 | 354 | return nil 355 | } 356 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | //go:build !binary_log && !windows 2 | // +build !binary_log,!windows 3 | 4 | package zerolog 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | func TestMultiSyslogWriter(t *testing.T) { 16 | sw := &syslogTestWriter{} 17 | log := New(MultiLevelWriter(SyslogLevelWriter(sw))) 18 | log.Debug().Msg("debug") 19 | log.Info().Msg("info") 20 | log.Warn().Msg("warn") 21 | log.Error().Msg("error") 22 | log.Log().Msg("nolevel") 23 | want := []syslogEvent{ 24 | {"Debug", `{"level":"debug","message":"debug"}` + "\n"}, 25 | {"Info", `{"level":"info","message":"info"}` + "\n"}, 26 | {"Warning", `{"level":"warn","message":"warn"}` + "\n"}, 27 | {"Err", `{"level":"error","message":"error"}` + "\n"}, 28 | {"Info", `{"message":"nolevel"}` + "\n"}, 29 | } 30 | if got := sw.events; !reflect.DeepEqual(got, want) { 31 | t.Errorf("Invalid syslog message routing: want %v, got %v", want, got) 32 | } 33 | } 34 | 35 | var writeCalls int 36 | 37 | type mockedWriter struct { 38 | wantErr bool 39 | } 40 | 41 | func (c mockedWriter) Write(p []byte) (int, error) { 42 | writeCalls++ 43 | 44 | if c.wantErr { 45 | return -1, errors.New("Expected error") 46 | } 47 | 48 | return len(p), nil 49 | } 50 | 51 | // Tests that a new writer is only used if it actually works. 52 | func TestResilientMultiWriter(t *testing.T) { 53 | tests := []struct { 54 | name string 55 | writers []io.Writer 56 | }{ 57 | { 58 | name: "All valid writers", 59 | writers: []io.Writer{ 60 | mockedWriter{ 61 | wantErr: false, 62 | }, 63 | mockedWriter{ 64 | wantErr: false, 65 | }, 66 | }, 67 | }, 68 | { 69 | name: "All invalid writers", 70 | writers: []io.Writer{ 71 | mockedWriter{ 72 | wantErr: true, 73 | }, 74 | mockedWriter{ 75 | wantErr: true, 76 | }, 77 | }, 78 | }, 79 | { 80 | name: "First invalid writer", 81 | writers: []io.Writer{ 82 | mockedWriter{ 83 | wantErr: true, 84 | }, 85 | mockedWriter{ 86 | wantErr: false, 87 | }, 88 | }, 89 | }, 90 | { 91 | name: "First valid writer", 92 | writers: []io.Writer{ 93 | mockedWriter{ 94 | wantErr: false, 95 | }, 96 | mockedWriter{ 97 | wantErr: true, 98 | }, 99 | }, 100 | }, 101 | } 102 | 103 | for _, tt := range tests { 104 | writers := tt.writers 105 | multiWriter := MultiLevelWriter(writers...) 106 | 107 | logger := New(multiWriter).With().Timestamp().Logger().Level(InfoLevel) 108 | logger.Info().Msg("Test msg") 109 | 110 | if len(writers) != writeCalls { 111 | t.Errorf("Expected %d writers to have been called but only %d were.", len(writers), writeCalls) 112 | } 113 | writeCalls = 0 114 | } 115 | } 116 | 117 | type testingLog struct { 118 | testing.TB 119 | buf bytes.Buffer 120 | } 121 | 122 | func (t *testingLog) Log(args ...interface{}) { 123 | if _, err := t.buf.WriteString(fmt.Sprint(args...)); err != nil { 124 | t.Error(err) 125 | } 126 | } 127 | 128 | func (t *testingLog) Logf(format string, args ...interface{}) { 129 | if _, err := t.buf.WriteString(fmt.Sprintf(format, args...)); err != nil { 130 | t.Error(err) 131 | } 132 | } 133 | 134 | func TestTestWriter(t *testing.T) { 135 | tests := []struct { 136 | name string 137 | write []byte 138 | want []byte 139 | }{{ 140 | name: "newline", 141 | write: []byte("newline\n"), 142 | want: []byte("newline"), 143 | }, { 144 | name: "oneline", 145 | write: []byte("oneline"), 146 | want: []byte("oneline"), 147 | }, { 148 | name: "twoline", 149 | write: []byte("twoline\n\n"), 150 | want: []byte("twoline"), 151 | }} 152 | 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | tb := &testingLog{TB: t} // Capture TB log buffer. 156 | w := TestWriter{T: tb} 157 | 158 | n, err := w.Write(tt.write) 159 | if err != nil { 160 | t.Error(err) 161 | } 162 | if n != len(tt.write) { 163 | t.Errorf("Expected %d write length but got %d", len(tt.write), n) 164 | } 165 | p := tb.buf.Bytes() 166 | if !bytes.Equal(tt.want, p) { 167 | t.Errorf("Expected %q, got %q.", tt.want, p) 168 | } 169 | 170 | log := New(NewConsoleWriter(ConsoleTestWriter(t))) 171 | log.Info().Str("name", tt.name).Msg("Success!") 172 | 173 | tb.buf.Reset() 174 | }) 175 | } 176 | 177 | } 178 | 179 | func TestFilteredLevelWriter(t *testing.T) { 180 | buf := bytes.Buffer{} 181 | writer := FilteredLevelWriter{ 182 | Writer: LevelWriterAdapter{&buf}, 183 | Level: InfoLevel, 184 | } 185 | _, err := writer.WriteLevel(DebugLevel, []byte("no")) 186 | if err != nil { 187 | t.Error(err) 188 | } 189 | _, err = writer.WriteLevel(InfoLevel, []byte("yes")) 190 | if err != nil { 191 | t.Error(err) 192 | } 193 | p := buf.Bytes() 194 | if want := "yes"; !bytes.Equal([]byte(want), p) { 195 | t.Errorf("Expected %q, got %q.", want, p) 196 | } 197 | } 198 | 199 | type testWrite struct { 200 | Level 201 | Line []byte 202 | } 203 | 204 | func TestTriggerLevelWriter(t *testing.T) { 205 | tests := []struct { 206 | write []testWrite 207 | want []byte 208 | all []byte 209 | }{{ 210 | []testWrite{ 211 | {DebugLevel, []byte("no\n")}, 212 | {InfoLevel, []byte("yes\n")}, 213 | }, 214 | []byte("yes\n"), 215 | []byte("yes\nno\n"), 216 | }, { 217 | []testWrite{ 218 | {DebugLevel, []byte("yes1\n")}, 219 | {InfoLevel, []byte("yes2\n")}, 220 | {ErrorLevel, []byte("yes3\n")}, 221 | {DebugLevel, []byte("yes4\n")}, 222 | }, 223 | []byte("yes2\nyes1\nyes3\nyes4\n"), 224 | []byte("yes2\nyes1\nyes3\nyes4\n"), 225 | }} 226 | 227 | for k, tt := range tests { 228 | t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { 229 | buf := bytes.Buffer{} 230 | writer := TriggerLevelWriter{Writer: LevelWriterAdapter{&buf}, ConditionalLevel: DebugLevel, TriggerLevel: ErrorLevel} 231 | t.Cleanup(func() { writer.Close() }) 232 | for _, w := range tt.write { 233 | _, err := writer.WriteLevel(w.Level, w.Line) 234 | if err != nil { 235 | t.Error(err) 236 | } 237 | } 238 | p := buf.Bytes() 239 | if want := tt.want; !bytes.Equal([]byte(want), p) { 240 | t.Errorf("Expected %q, got %q.", want, p) 241 | } 242 | err := writer.Trigger() 243 | if err != nil { 244 | t.Error(err) 245 | } 246 | p = buf.Bytes() 247 | if want := tt.all; !bytes.Equal([]byte(want), p) { 248 | t.Errorf("Expected %q, got %q.", want, p) 249 | } 250 | }) 251 | } 252 | } 253 | --------------------------------------------------------------------------------