Add []byte fields support
Add efficient []byte field support with no string conversion.
This commit is contained in:
parent
447d0fc7f5
commit
72d41dedeb
@ -26,6 +26,12 @@ func (c Context) Str(key, val string) Context {
|
|||||||
return c
|
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.
|
// AnErr adds the field key with err as a string to the logger context.
|
||||||
func (c Context) AnErr(key string, err error) Context {
|
func (c Context) AnErr(key string, err error) Context {
|
||||||
c.l.context = appendErrorKey(c.l.context, key, err)
|
c.l.context = appendErrorKey(c.l.context, key, err)
|
||||||
|
9
event.go
9
event.go
@ -121,6 +121,15 @@ func (e *Event) Str(key, val string) *Event {
|
|||||||
return e
|
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.
|
// AnErr adds the field key with err as a string to the *Event context.
|
||||||
// If err is nil, no field is added.
|
// If err is nil, no field is added.
|
||||||
func (e *Event) AnErr(key string, err error) *Event {
|
func (e *Event) AnErr(key string, err error) *Event {
|
||||||
|
4
field.go
4
field.go
@ -19,6 +19,10 @@ func appendString(dst []byte, key, val string) []byte {
|
|||||||
return appendJSONString(appendKey(dst, key), val)
|
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 {
|
func appendErrorKey(dst []byte, key string, err error) []byte {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return dst
|
return dst
|
||||||
|
69
json.go
69
json.go
@ -94,3 +94,72 @@ func appendJSONStringComplex(dst []byte, s string, i int) []byte {
|
|||||||
}
|
}
|
||||||
return dst
|
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
|
||||||
|
}
|
||||||
|
164
json_test.go
164
json_test.go
@ -2,57 +2,58 @@ package zerolog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppendJSONString(t *testing.T) {
|
var encodeStringTests = []struct {
|
||||||
encodeStringTests := []struct {
|
in string
|
||||||
in string
|
out string
|
||||||
out string
|
}{
|
||||||
}{
|
{"", `""`},
|
||||||
{"", `""`},
|
{"\\", `"\\"`},
|
||||||
{"\\", `"\\"`},
|
{"\x00", `"\u0000"`},
|
||||||
{"\x00", `"\u0000"`},
|
{"\x01", `"\u0001"`},
|
||||||
{"\x01", `"\u0001"`},
|
{"\x02", `"\u0002"`},
|
||||||
{"\x02", `"\u0002"`},
|
{"\x03", `"\u0003"`},
|
||||||
{"\x03", `"\u0003"`},
|
{"\x04", `"\u0004"`},
|
||||||
{"\x04", `"\u0004"`},
|
{"\x05", `"\u0005"`},
|
||||||
{"\x05", `"\u0005"`},
|
{"\x06", `"\u0006"`},
|
||||||
{"\x06", `"\u0006"`},
|
{"\x07", `"\u0007"`},
|
||||||
{"\x07", `"\u0007"`},
|
{"\x08", `"\b"`},
|
||||||
{"\x08", `"\b"`},
|
{"\x09", `"\t"`},
|
||||||
{"\x09", `"\t"`},
|
{"\x0a", `"\n"`},
|
||||||
{"\x0a", `"\n"`},
|
{"\x0b", `"\u000b"`},
|
||||||
{"\x0b", `"\u000b"`},
|
{"\x0c", `"\f"`},
|
||||||
{"\x0c", `"\f"`},
|
{"\x0d", `"\r"`},
|
||||||
{"\x0d", `"\r"`},
|
{"\x0e", `"\u000e"`},
|
||||||
{"\x0e", `"\u000e"`},
|
{"\x0f", `"\u000f"`},
|
||||||
{"\x0f", `"\u000f"`},
|
{"\x10", `"\u0010"`},
|
||||||
{"\x10", `"\u0010"`},
|
{"\x11", `"\u0011"`},
|
||||||
{"\x11", `"\u0011"`},
|
{"\x12", `"\u0012"`},
|
||||||
{"\x12", `"\u0012"`},
|
{"\x13", `"\u0013"`},
|
||||||
{"\x13", `"\u0013"`},
|
{"\x14", `"\u0014"`},
|
||||||
{"\x14", `"\u0014"`},
|
{"\x15", `"\u0015"`},
|
||||||
{"\x15", `"\u0015"`},
|
{"\x16", `"\u0016"`},
|
||||||
{"\x16", `"\u0016"`},
|
{"\x17", `"\u0017"`},
|
||||||
{"\x17", `"\u0017"`},
|
{"\x18", `"\u0018"`},
|
||||||
{"\x18", `"\u0018"`},
|
{"\x19", `"\u0019"`},
|
||||||
{"\x19", `"\u0019"`},
|
{"\x1a", `"\u001a"`},
|
||||||
{"\x1a", `"\u001a"`},
|
{"\x1b", `"\u001b"`},
|
||||||
{"\x1b", `"\u001b"`},
|
{"\x1c", `"\u001c"`},
|
||||||
{"\x1c", `"\u001c"`},
|
{"\x1d", `"\u001d"`},
|
||||||
{"\x1d", `"\u001d"`},
|
{"\x1e", `"\u001e"`},
|
||||||
{"\x1e", `"\u001e"`},
|
{"\x1f", `"\u001f"`},
|
||||||
{"\x1f", `"\u001f"`},
|
{"✭", `"✭"`},
|
||||||
{"✭", `"✭"`},
|
{"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence
|
||||||
{"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence
|
{"ascii", `"ascii"`},
|
||||||
{"ascii", `"ascii"`},
|
{"\"a", `"\"a"`},
|
||||||
{"\"a", `"\"a"`},
|
{"\x1fa", `"\u001fa"`},
|
||||||
{"\x1fa", `"\u001fa"`},
|
{"foo\"bar\"baz", `"foo\"bar\"baz"`},
|
||||||
{"foo\"bar\"baz", `"foo\"bar\"baz"`},
|
{"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`},
|
||||||
{"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`},
|
{"emoji \u2764\ufe0f!", `"emoji ❤️!"`},
|
||||||
{"emoji \u2764\ufe0f!", `"emoji ❤️!"`},
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
func TestAppendJSONString(t *testing.T) {
|
||||||
for _, tt := range encodeStringTests {
|
for _, tt := range encodeStringTests {
|
||||||
b := appendJSONString([]byte{}, tt.in)
|
b := appendJSONString([]byte{}, tt.in)
|
||||||
if got, want := string(b), tt.out; got != want {
|
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) {
|
func BenchmarkAppendJSONString(b *testing.B) {
|
||||||
tests := map[string]string{
|
tests := map[string]string{
|
||||||
"NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
|
"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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user