From 9070d49a1a4154c819e9c5ae6c923f3cc916d959 Mon Sep 17 00:00:00 2001 From: stergiotis Date: Mon, 19 Jun 2023 01:30:44 +0200 Subject: [PATCH] Add event method RawCBOR analogous to RawJSON (#556) CBOR is encoded as data-url in JSON encoding and tagged in CBOR encoding. --- encoder_cbor.go | 3 +++ encoder_json.go | 12 ++++++++++ event.go | 12 ++++++++++ internal/cbor/cbor.go | 3 ++- internal/cbor/decode_stream.go | 40 ++++++++++++++++++++++++++++++++++ internal/cbor/string.go | 22 +++++++++++++++++++ log_test.go | 3 ++- 7 files changed, 93 insertions(+), 2 deletions(-) diff --git a/encoder_cbor.go b/encoder_cbor.go index 7b0dafef..36cb994b 100644 --- a/encoder_cbor.go +++ b/encoder_cbor.go @@ -24,6 +24,9 @@ func init() { func appendJSON(dst []byte, j []byte) []byte { return cbor.AppendEmbeddedJSON(dst, j) } +func appendCBOR(dst []byte, c []byte) []byte { + return cbor.AppendEmbeddedCBOR(dst, c) +} // decodeIfBinaryToString - converts a binary formatted log msg to a // JSON formatted String Log message. diff --git a/encoder_json.go b/encoder_json.go index 0e0450e2..6f96c68a 100644 --- a/encoder_json.go +++ b/encoder_json.go @@ -6,6 +6,7 @@ package zerolog // JSON encoded byte stream. import ( + "encoding/base64" "github.com/rs/zerolog/internal/json" ) @@ -25,6 +26,17 @@ func init() { func appendJSON(dst []byte, j []byte) []byte { return append(dst, j...) } +func appendCBOR(dst []byte, cbor []byte) []byte { + dst = append(dst, []byte("\"data:application/cbor;base64,")...) + l := len(dst) + enc := base64.StdEncoding + n := enc.EncodedLen(len(cbor)) + for i := 0; i < n; i++ { + dst = append(dst, '.') + } + enc.Encode(dst[l:], cbor) + return append(dst, '"') +} func decodeIfBinaryToString(in []byte) string { return string(in) diff --git a/event.go b/event.go index 2e736c83..5fb46536 100644 --- a/event.go +++ b/event.go @@ -318,6 +318,18 @@ func (e *Event) RawJSON(key string, b []byte) *Event { return e } +// RawCBOR adds already encoded CBOR to the log line under key. +// +// No sanity check is performed on b +// Note: The full featureset of CBOR is supported as data will not be mapped to json but stored as data-url +func (e *Event) RawCBOR(key string, b []byte) *Event { + if e == nil { + return e + } + e.buf = appendCBOR(enc.AppendKey(e.buf, key), b) + return e +} + // AnErr adds the field key with serialized err to the *Event context. // If err is nil, no field is added. func (e *Event) AnErr(key string, err error) *Event { diff --git a/internal/cbor/cbor.go b/internal/cbor/cbor.go index bc54e37a..1bf14438 100644 --- a/internal/cbor/cbor.go +++ b/internal/cbor/cbor.go @@ -26,7 +26,8 @@ const ( additionalTypeBreak byte = 31 // Tag Sub-types. - additionalTypeTimestamp byte = 01 + additionalTypeTimestamp byte = 01 + additionalTypeEmbeddedCBOR byte = 63 // Extended Tags - from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml additionalTypeTagNetworkAddr uint16 = 260 diff --git a/internal/cbor/decode_stream.go b/internal/cbor/decode_stream.go index fc16f98c..616bed65 100644 --- a/internal/cbor/decode_stream.go +++ b/internal/cbor/decode_stream.go @@ -5,6 +5,7 @@ package cbor import ( "bufio" "bytes" + "encoding/base64" "fmt" "io" "math" @@ -213,6 +214,31 @@ func decodeString(src *bufio.Reader, noQuotes bool) []byte { } return append(result, '"') } +func decodeStringToDataUrl(src *bufio.Reader, mimeType string) []byte { + pb := readByte(src) + major := pb & maskOutAdditionalType + minor := pb & maskOutMajorType + if major != majorTypeByteString { + panic(fmt.Errorf("Major type is: %d in decodeString", major)) + } + length := decodeIntAdditionalType(src, minor) + l := int(length) + enc := base64.StdEncoding + lEnc := enc.EncodedLen(l) + result := make([]byte, len("\"data:;base64,\"")+len(mimeType)+lEnc) + dest := result + u := copy(dest, "\"data:") + dest = dest[u:] + u = copy(dest, mimeType) + dest = dest[u:] + u = copy(dest, ";base64,") + dest = dest[u:] + pbs := readNBytes(src, l) + enc.Encode(dest, pbs) + dest = dest[lEnc:] + dest[0] = '"' + return result +} func decodeUTF8String(src *bufio.Reader) []byte { pb := readByte(src) @@ -349,6 +375,20 @@ func decodeTagData(src *bufio.Reader) []byte { switch minor { case additionalTypeTimestamp: return decodeTimeStamp(src) + case additionalTypeIntUint8: + val := decodeIntAdditionalType(src, minor) + switch byte(val) { + case additionalTypeEmbeddedCBOR: + pb := readByte(src) + dataMajor := pb & maskOutAdditionalType + if dataMajor != majorTypeByteString { + panic(fmt.Errorf("Unsupported embedded Type: %d in decodeEmbeddedCBOR", dataMajor)) + } + src.UnreadByte() + return decodeStringToDataUrl(src, "application/cbor") + default: + panic(fmt.Errorf("Unsupported Additional Tag Type: %d in decodeTagData", val)) + } // Tag value is larger than 256 (so uint16). case additionalTypeIntUint16: diff --git a/internal/cbor/string.go b/internal/cbor/string.go index a33890a5..9fc9a4f8 100644 --- a/internal/cbor/string.go +++ b/internal/cbor/string.go @@ -93,3 +93,25 @@ func AppendEmbeddedJSON(dst, s []byte) []byte { } return append(dst, s...) } + +// AppendEmbeddedCBOR adds a tag and embeds input CBOR as such. +func AppendEmbeddedCBOR(dst, s []byte) []byte { + major := majorTypeTags + minor := additionalTypeEmbeddedCBOR + + // Append the TAG to indicate this is Embedded JSON. + dst = append(dst, major|additionalTypeIntUint8) + dst = append(dst, minor) + + // Append the CBOR Object as Byte String. + major = majorTypeByteString + + l := len(s) + if l <= additionalMax { + lb := byte(l) + dst = append(dst, major|lb) + } else { + dst = appendCborTypePrefix(dst, major, uint64(l)) + } + return append(dst, s...) +} diff --git a/log_test.go b/log_test.go index aa84750c..0e64c494 100644 --- a/log_test.go +++ b/log_test.go @@ -320,6 +320,7 @@ func TestFields(t *testing.T) { Bytes("bytes", []byte("bar")). Hex("hex", []byte{0x12, 0xef}). RawJSON("json", []byte(`{"some":"json"}`)). + RawCBOR("cbor", []byte{0x83, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05}). Func(func(e *Event) { e.Str("func", "func_output") }). AnErr("some_err", nil). Err(errors.New("some error")). @@ -344,7 +345,7 @@ func TestFields(t *testing.T) { Time("time", time.Time{}). TimeDiff("diff", now, now.Add(-10*time.Second)). Msg("") - if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"func":"func_output","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want { + if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"cbor":"data:application/cbor;base64,gwGCAgOCBAU=","func":"func_output","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } }