diff --git a/encoder_json.go b/encoder_json.go index 6f96c68a..3257c7a9 100644 --- a/encoder_json.go +++ b/encoder_json.go @@ -1,3 +1,4 @@ +//go:build !binary_log // +build !binary_log package zerolog @@ -21,6 +22,12 @@ func init() { json.JSONMarshalFunc = func(v interface{}) ([]byte, error) { return InterfaceMarshalFunc(v) } + if JSONBytesMarshalFunc == nil { + JSONBytesMarshalFunc = json.AppendBytesDefault + } + json.AppendBytesFunc = func(dst, src []byte) []byte { + return JSONBytesMarshalFunc(dst, src) + } } func appendJSON(dst []byte, j []byte) []byte { diff --git a/event.go b/event.go index 5c949f8a..69173567 100644 --- a/event.go +++ b/event.go @@ -290,7 +290,7 @@ func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event { // Bytes adds the field key with val as a string to the *Event context. // // Runes outside of normal ASCII ranges will be hex-encoded in the resulting -// JSON. +// JSON. The marshaling can be customized by setting JSONBytesMarshalFunc. func (e *Event) Bytes(key string, val []byte) *Event { if e == nil { return e diff --git a/globals.go b/globals.go index bf9e3da9..7c28fa45 100644 --- a/globals.go +++ b/globals.go @@ -1,6 +1,7 @@ package zerolog import ( + "encoding/base64" "encoding/json" "strconv" "sync/atomic" @@ -80,6 +81,12 @@ var ( return err } + // JSONBytesMarshalFunc allows customization of how Bytes() produces JSON output. + // The default implementation leaves ASCII characters as-is and encodes Unicode runes as hex escapes. + // + // The function should append the encoded bytes to dst and return the extended buffer. + JSONBytesMarshalFunc func(dst, src []byte) []byte + // InterfaceMarshalFunc allows customization of interface marshaling. // Default: "encoding/json.Marshal" InterfaceMarshalFunc = json.Marshal @@ -164,3 +171,17 @@ func DisableSampling(v bool) { func samplingDisabled() bool { return atomic.LoadInt32(disableSampling) == 1 } + +// JSONBytesMarshalBase64 returns a function compatible with JSONBytesMarshalFunc that encodes bytes using the given base64 encoder. +func JSONBytesMarshalBase64(enc *base64.Encoding) func(dst, src []byte) []byte { + return func(dst, src []byte) []byte { + start := len(dst) + targetLen := start + enc.EncodedLen(len(src)) + for cap(dst) < targetLen { + dst = append(dst[:cap(dst)], 0) + } + dst = dst[:targetLen] + enc.Encode(dst[start:], src) + return dst + } +} diff --git a/internal/json/base.go b/internal/json/base.go index 09ec59f4..34190964 100644 --- a/internal/json/base.go +++ b/internal/json/base.go @@ -8,6 +8,8 @@ package json // you might get a nil pointer dereference panic at runtime. var JSONMarshalFunc func(v interface{}) ([]byte, error) +var AppendBytesFunc func(dst, src []byte) []byte = AppendBytesDefault + type Encoder struct{} // AppendKey appends a new key to the output JSON. diff --git a/internal/json/bytes.go b/internal/json/bytes.go index de64120d..07da516b 100644 --- a/internal/json/bytes.go +++ b/internal/json/bytes.go @@ -5,14 +5,17 @@ import "unicode/utf8" // AppendBytes is a mirror of appendString with []byte arg func (Encoder) AppendBytes(dst, s []byte) []byte { dst = append(dst, '"') + dst = AppendBytesFunc(dst, s) + return append(dst, '"') +} + +func AppendBytesDefault(dst, s []byte) []byte { for i := 0; i < len(s); i++ { if !noEscapeTable[s[i]] { - dst = appendBytesComplex(dst, s, i) - return append(dst, '"') + return appendBytesComplex(dst, s, i) } } - dst = append(dst, s...) - return append(dst, '"') + return append(dst, s...) } // AppendHex encodes the input bytes to a hex string and appends diff --git a/log_json_test.go b/log_json_test.go new file mode 100644 index 00000000..f62f0f30 --- /dev/null +++ b/log_json_test.go @@ -0,0 +1,46 @@ +//go:build !binary_log +// +build !binary_log + +package zerolog + +import ( + "bytes" + "encoding/base64" + "os" + "testing" +) + +func TestJSONBytesMarshalFunc(t *testing.T) { + out := &bytes.Buffer{} + log := New(out) + log.Log().Bytes("bytes", []byte{'a', 'b', 'c', 1, 2, 3, 0xff}).Msg("msg") + if got, want := decodeIfBinaryToString(out.Bytes()), `{"bytes":"abc\u0001\u0002\u0003\ufffd","message":"msg"}`+"\n"; got != want { + t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) + } + out.Reset() + origBytesMarshalFunc := JSONBytesMarshalFunc + defer func() { + JSONBytesMarshalFunc = origBytesMarshalFunc + }() + + JSONBytesMarshalFunc = JSONBytesMarshalBase64(base64.StdEncoding) + log.Log().Bytes("bytes", []byte{'a', 'b', 'c', 1, 2, 3, 0xff}).Msg("msg") + if got, want := decodeIfBinaryToString(out.Bytes()), `{"bytes":"YWJjAQID/w==","message":"msg"}`+"\n"; got != want { + t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) + } + out.Reset() + + JSONBytesMarshalFunc = JSONBytesMarshalBase64(base64.RawURLEncoding) + log.Log().Bytes("bytes", []byte{'a', 'b', 'c', 1, 2, 3, 0xff}).Msg("msg") + if got, want := decodeIfBinaryToString(out.Bytes()), `{"bytes":"YWJjAQID_w","message":"msg"}`+"\n"; got != want { + t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) + } + out.Reset() +} + +func ExampleJSONBytesMarshalBase64() { + log := New(os.Stdout) + JSONBytesMarshalFunc = JSONBytesMarshalBase64(base64.StdEncoding) + log.Info().Bytes("bytes", []byte{'a', 'b', 'c', 1, 2, 3, 0xff}).Msg("msg") + // Output: {"level":"info","bytes":"YWJjAQID/w==","message":"msg"} +}