diff --git a/context.go b/context.go index ce23514..93e0808 100644 --- a/context.go +++ b/context.go @@ -26,6 +26,12 @@ func (c Context) Str(key, val string) Context { return c } +// Bytes adds the field key with val as a []byte to the logger context. +func (c Context) Bytes(key string, val []byte) Context { + c.l.context = appendBytes(c.l.context, key, val) + return c +} + // AnErr adds the field key with err as a string to the logger context. func (c Context) AnErr(key string, err error) Context { c.l.context = appendErrorKey(c.l.context, key, err) diff --git a/event.go b/event.go index 1175f2e..41bc562 100644 --- a/event.go +++ b/event.go @@ -121,6 +121,15 @@ func (e *Event) Str(key, val string) *Event { return e } +// Bytes adds the field key with val as a []byte to the *Event context. +func (e *Event) Bytes(key string, val []byte) *Event { + if !e.enabled { + return e + } + e.buf = appendBytes(e.buf, key, val) + return e +} + // AnErr adds the field key with err as a string to the *Event context. // If err is nil, no field is added. func (e *Event) AnErr(key string, err error) *Event { diff --git a/field.go b/field.go index 0c0e5b0..02710e4 100644 --- a/field.go +++ b/field.go @@ -19,6 +19,10 @@ func appendString(dst []byte, key, val string) []byte { return appendJSONString(appendKey(dst, key), val) } +func appendBytes(dst []byte, key string, val []byte) []byte { + return appendJSONBytes(appendKey(dst, key), val) +} + func appendErrorKey(dst []byte, key string, err error) []byte { if err == nil { return dst diff --git a/json.go b/json.go index b439d18..d55a0fd 100644 --- a/json.go +++ b/json.go @@ -94,3 +94,72 @@ func appendJSONStringComplex(dst []byte, s string, i int) []byte { } return dst } + +// appendJSONBytes is a mirror of appendJSONString with []byte arg +func appendJSONBytes(dst, s []byte) []byte { + dst = append(dst, '"') + for i := 0; i < len(s); i++ { + if s[i] < 0x20 || s[i] > 0x7e || s[i] == '\\' || s[i] == '"' { + dst = appendJSONBytesComplex(dst, s, i) + return append(dst, '"') + } + } + dst = append(dst, s...) + return append(dst, '"') +} + +// appendJSONBytesComplex is a mirror of the appendJSONStringComplex +// with []byte arg +func appendJSONBytesComplex(dst, s []byte, i int) []byte { + start := 0 + for i < len(s) { + b := s[i] + if b >= utf8.RuneSelf { + r, size := utf8.DecodeRune(s[i:]) + if r == utf8.RuneError && size == 1 { + if start < i { + dst = append(dst, s[start:i]...) + } + dst = append(dst, `\ufffd`...) + i += size + start = i + continue + } + i += size + continue + } + if b >= 0x20 && b <= 0x7e && b != '\\' && b != '"' { + i++ + continue + } + // We encountered a character that needs to be encoded. + // Let's append the previous simple characters to the byte slice + // and switch our operation to read and encode the remainder + // characters byte-by-byte. + if start < i { + dst = append(dst, s[start:i]...) + } + switch b { + case '"', '\\': + dst = append(dst, '\\', b) + case '\b': + dst = append(dst, '\\', 'b') + case '\f': + dst = append(dst, '\\', 'f') + case '\n': + dst = append(dst, '\\', 'n') + case '\r': + dst = append(dst, '\\', 'r') + case '\t': + dst = append(dst, '\\', 't') + default: + dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF]) + } + i++ + start = i + } + if start < len(s) { + dst = append(dst, s[start:]...) + } + return dst +} diff --git a/json_test.go b/json_test.go index 2c45ca5..69a344a 100644 --- a/json_test.go +++ b/json_test.go @@ -2,57 +2,58 @@ package zerolog import ( "testing" + "unicode" ) -func TestAppendJSONString(t *testing.T) { - encodeStringTests := []struct { - in string - out string - }{ - {"", `""`}, - {"\\", `"\\"`}, - {"\x00", `"\u0000"`}, - {"\x01", `"\u0001"`}, - {"\x02", `"\u0002"`}, - {"\x03", `"\u0003"`}, - {"\x04", `"\u0004"`}, - {"\x05", `"\u0005"`}, - {"\x06", `"\u0006"`}, - {"\x07", `"\u0007"`}, - {"\x08", `"\b"`}, - {"\x09", `"\t"`}, - {"\x0a", `"\n"`}, - {"\x0b", `"\u000b"`}, - {"\x0c", `"\f"`}, - {"\x0d", `"\r"`}, - {"\x0e", `"\u000e"`}, - {"\x0f", `"\u000f"`}, - {"\x10", `"\u0010"`}, - {"\x11", `"\u0011"`}, - {"\x12", `"\u0012"`}, - {"\x13", `"\u0013"`}, - {"\x14", `"\u0014"`}, - {"\x15", `"\u0015"`}, - {"\x16", `"\u0016"`}, - {"\x17", `"\u0017"`}, - {"\x18", `"\u0018"`}, - {"\x19", `"\u0019"`}, - {"\x1a", `"\u001a"`}, - {"\x1b", `"\u001b"`}, - {"\x1c", `"\u001c"`}, - {"\x1d", `"\u001d"`}, - {"\x1e", `"\u001e"`}, - {"\x1f", `"\u001f"`}, - {"✭", `"✭"`}, - {"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence - {"ascii", `"ascii"`}, - {"\"a", `"\"a"`}, - {"\x1fa", `"\u001fa"`}, - {"foo\"bar\"baz", `"foo\"bar\"baz"`}, - {"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`}, - {"emoji \u2764\ufe0f!", `"emoji ❤️!"`}, - } +var encodeStringTests = []struct { + in string + out string +}{ + {"", `""`}, + {"\\", `"\\"`}, + {"\x00", `"\u0000"`}, + {"\x01", `"\u0001"`}, + {"\x02", `"\u0002"`}, + {"\x03", `"\u0003"`}, + {"\x04", `"\u0004"`}, + {"\x05", `"\u0005"`}, + {"\x06", `"\u0006"`}, + {"\x07", `"\u0007"`}, + {"\x08", `"\b"`}, + {"\x09", `"\t"`}, + {"\x0a", `"\n"`}, + {"\x0b", `"\u000b"`}, + {"\x0c", `"\f"`}, + {"\x0d", `"\r"`}, + {"\x0e", `"\u000e"`}, + {"\x0f", `"\u000f"`}, + {"\x10", `"\u0010"`}, + {"\x11", `"\u0011"`}, + {"\x12", `"\u0012"`}, + {"\x13", `"\u0013"`}, + {"\x14", `"\u0014"`}, + {"\x15", `"\u0015"`}, + {"\x16", `"\u0016"`}, + {"\x17", `"\u0017"`}, + {"\x18", `"\u0018"`}, + {"\x19", `"\u0019"`}, + {"\x1a", `"\u001a"`}, + {"\x1b", `"\u001b"`}, + {"\x1c", `"\u001c"`}, + {"\x1d", `"\u001d"`}, + {"\x1e", `"\u001e"`}, + {"\x1f", `"\u001f"`}, + {"✭", `"✭"`}, + {"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence + {"ascii", `"ascii"`}, + {"\"a", `"\"a"`}, + {"\x1fa", `"\u001fa"`}, + {"foo\"bar\"baz", `"foo\"bar\"baz"`}, + {"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`}, + {"emoji \u2764\ufe0f!", `"emoji ❤️!"`}, +} +func TestAppendJSONString(t *testing.T) { for _, tt := range encodeStringTests { b := appendJSONString([]byte{}, tt.in) if got, want := string(b), tt.out; got != want { @@ -61,6 +62,52 @@ func TestAppendJSONString(t *testing.T) { } } +func TestAppendJSONBytes(t *testing.T) { + for _, tt := range encodeStringTests { + b := appendJSONBytes([]byte{}, []byte(tt.in)) + if got, want := string(b), tt.out; got != want { + t.Errorf("appendJSONBytes(%q) = %#q, want %#q", tt.in, got, want) + } + } +} + +func TestStringBytes(t *testing.T) { + t.Parallel() + // Test that encodeState.stringBytes and encodeState.string use the same encoding. + var r []rune + for i := '\u0000'; i <= unicode.MaxRune; i++ { + r = append(r, i) + } + s := string(r) + "\xff\xff\xffhello" // some invalid UTF-8 too + + enc := string(appendJSONString([]byte{}, s)) + encBytes := string(appendJSONBytes([]byte{}, []byte(s))) + + if enc != encBytes { + i := 0 + for i < len(enc) && i < len(encBytes) && enc[i] == encBytes[i] { + i++ + } + enc = enc[i:] + encBytes = encBytes[i:] + i = 0 + for i < len(enc) && i < len(encBytes) && enc[len(enc)-i-1] == encBytes[len(encBytes)-i-1] { + i++ + } + enc = enc[:len(enc)-i] + encBytes = encBytes[:len(encBytes)-i] + + if len(enc) > 20 { + enc = enc[:20] + "..." + } + if len(encBytes) > 20 { + encBytes = encBytes[:20] + "..." + } + + t.Errorf("encodings differ at %#q vs %#q", enc, encBytes) + } +} + func BenchmarkAppendJSONString(b *testing.B) { tests := map[string]string{ "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, @@ -80,3 +127,24 @@ func BenchmarkAppendJSONString(b *testing.B) { }) } } + +func BenchmarkAppendJSONBytes(b *testing.B) { + tests := map[string]string{ + "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, + "EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, + "EncodingMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa"aaaaaaaaaaaaaaaaaaaaaaaa`, + "EncodingLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"`, + "MultiBytesFirst": `❤️aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, + "MultiBytesMiddle": `aaaaaaaaaaaaaaaaaaaaaaaaa❤️aaaaaaaaaaaaaaaaaaaaaaaa`, + "MultiBytesLast": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa❤️`, + } + for name, str := range tests { + byt := []byte(str) + b.Run(name, func(b *testing.B) { + buf := make([]byte, 0, 100) + for i := 0; i < b.N; i++ { + _ = appendJSONBytes(buf, byt) + } + }) + } +}