Repository: rs/zerolog
Pull request #556 (2023-06-14 20:43:07)

Add event method RawCBOR analogous to RawJSON
CBOR is encoded as data-url in JSON encoding and tagged in CBOR encoding.


Patch:
diff --git a/encoder_cbor.go b/encoder_cbor.go
index 7b0dafe..36cb994 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 0e0450e..6f96c68 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 2e736c8..5fb4653 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 bc54e37..1bf1443 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 fc16f98..616bed6 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 a33890a..9fc9a4f 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...)
+}


Test patch:
diff --git a/log_test.go b/log_test.go
index aa84750..0e64c49 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)
 	}
 }