From 1c575db92819e737bf5f8751e6cc90d2ccd273e1 Mon Sep 17 00:00:00 2001 From: Max Wolter Date: Thu, 15 Mar 2018 18:29:26 +0100 Subject: [PATCH] Add support for hex-encoded of byte slice (#42) --- array.go | 6 ++++ array_test.go | 4 ++- context.go | 6 ++++ event.go | 9 ++++++ internal/json/base.go | 5 ++++ internal/json/string.go | 15 ++++++++++ internal/json/string_test.go | 28 +++++++++++++++--- internal/json/time.go | 8 ++++++ internal/json/types.go | 56 ++++++++++++++++++++++++++++++++++++ log_test.go | 10 +++++-- 10 files changed, 139 insertions(+), 8 deletions(-) diff --git a/array.go b/array.go index cfce02c..4597187 100644 --- a/array.go +++ b/array.go @@ -64,6 +64,12 @@ func (a *Array) Bytes(val []byte) *Array { return a } +// Hex append the val as a hex string to the array. +func (a *Array) Hex(val []byte) *Array { + a.buf = json.AppendHex(append(a.buf, ','), val) + return a +} + // Err append the err as a string to the array. func (a *Array) Err(err error) *Array { a.buf = json.AppendError(append(a.buf, ','), err) diff --git a/array_test.go b/array_test.go index 02fe9ae..65ca343 100644 --- a/array_test.go +++ b/array_test.go @@ -21,9 +21,11 @@ func TestArray(t *testing.T) { Float32(11). Float64(12). Str("a"). + Bytes([]byte("b")). + Hex([]byte{0x1f}). Time(time.Time{}). Dur(0) - want := `[true,1,2,3,4,5,6,7,8,9,10,11,12,"a","0001-01-01T00:00:00Z",0]` + want := `[true,1,2,3,4,5,6,7,8,9,10,11,12,"a","b","1f","0001-01-01T00:00:00Z",0]` if got := string(a.write([]byte{})); got != want { t.Errorf("Array.write()\ngot: %s\nwant: %s", got, want) } diff --git a/context.go b/context.go index 26f8a2a..ce27d7e 100644 --- a/context.go +++ b/context.go @@ -79,6 +79,12 @@ func (c Context) Bytes(key string, val []byte) Context { return c } +// Hex adds the field key with val as a hex string to the logger context. +func (c Context) Hex(key string, val []byte) Context { + c.l.context = json.AppendHex(json.AppendKey(c.l.context, key), val) + return c +} + // RawJSON adds already encoded JSON to context. // // No sanity check is performed on b; it must not contain carriage returns and diff --git a/event.go b/event.go index 13358e1..e451f5d 100644 --- a/event.go +++ b/event.go @@ -219,6 +219,15 @@ func (e *Event) Bytes(key string, val []byte) *Event { return e } +// Hex adds the field key with val as a hex string to the *Event context. +func (e *Event) Hex(key string, val []byte) *Event { + if e == nil { + return e + } + e.buf = json.AppendHex(json.AppendKey(e.buf, key), val) + return e +} + // RawJSON adds already encoded JSON to the log line under key. // // No sanity check is performed on b; it must not contain carriage returns and diff --git a/internal/json/base.go b/internal/json/base.go index 7baeec5..2e03e2e 100644 --- a/internal/json/base.go +++ b/internal/json/base.go @@ -1,5 +1,6 @@ package json +// AppendKey appends a new key to the output JSON. func AppendKey(dst []byte, key string) []byte { if len(dst) > 1 { dst = append(dst, ',') @@ -8,6 +9,8 @@ func AppendKey(dst []byte, key string) []byte { return append(dst, ':') } +// AppendError encodes the error string to json and appends +// the encoded string to the input byte slice. func AppendError(dst []byte, err error) []byte { if err == nil { return append(dst, `null`...) @@ -15,6 +18,8 @@ func AppendError(dst []byte, err error) []byte { return AppendString(dst, err.Error()) } +// AppendErrors encodes the error strings to json and +// appends the encoded string list to the input byte slice. func AppendErrors(dst []byte, errs []error) []byte { if len(errs) == 0 { return append(dst, '[', ']') diff --git a/internal/json/string.go b/internal/json/string.go index 8f8f4df..7f85ad6 100644 --- a/internal/json/string.go +++ b/internal/json/string.go @@ -4,6 +4,8 @@ import "unicode/utf8" const hex = "0123456789abcdef" +// AppendStrings encodes the input strings to json and +// appends the encoded string list to the input byte slice. func AppendStrings(dst []byte, vals []string) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -123,6 +125,19 @@ func AppendBytes(dst, s []byte) []byte { return append(dst, '"') } +// AppendHex encodes the input bytes to a hex string and appends +// the encoded string to the input byte slice. +// +// The operation loops though each byte and encodes it as hex using +// the hex lookup table. +func AppendHex(dst, s []byte) []byte { + dst = append(dst, '"') + for _, v := range s { + dst = append(dst, hex[v>>4], hex[v&0x0f]) + } + return append(dst, '"') +} + // appendBytesComplex is a mirror of the appendStringComplex // with []byte arg func appendBytesComplex(dst, s []byte, i int) []byte { diff --git a/internal/json/string_test.go b/internal/json/string_test.go index b0c2c32..0d5fc6c 100644 --- a/internal/json/string_test.go +++ b/internal/json/string_test.go @@ -53,7 +53,18 @@ var encodeStringTests = []struct { {"emoji \u2764\ufe0f!", `"emoji ❤️!"`}, } -func TestappendString(t *testing.T) { +var encodeHexTests = []struct { + in byte + out string +}{ + {0x00, `"00"`}, + {0x0f, `"0f"`}, + {0x10, `"10"`}, + {0xf0, `"f0"`}, + {0xff, `"ff"`}, +} + +func TestAppendString(t *testing.T) { for _, tt := range encodeStringTests { b := AppendString([]byte{}, tt.in) if got, want := string(b), tt.out; got != want { @@ -62,7 +73,7 @@ func TestappendString(t *testing.T) { } } -func TestappendBytes(t *testing.T) { +func TestAppendBytes(t *testing.T) { for _, tt := range encodeStringTests { b := AppendBytes([]byte{}, []byte(tt.in)) if got, want := string(b), tt.out; got != want { @@ -71,6 +82,15 @@ func TestappendBytes(t *testing.T) { } } +func TestAppendHex(t *testing.T) { + for _, tt := range encodeHexTests { + b := AppendHex([]byte{}, []byte{tt.in}) + if got, want := string(b), tt.out; got != want { + t.Errorf("appendHex(%x) = %s, want %s", tt.in, got, want) + } + } +} + func TestStringBytes(t *testing.T) { t.Parallel() // Test that encodeState.stringBytes and encodeState.string use the same encoding. @@ -108,7 +128,7 @@ func TestStringBytes(t *testing.T) { } } -func BenchmarkappendString(b *testing.B) { +func BenchmarkAppendString(b *testing.B) { tests := map[string]string{ "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, "EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, @@ -128,7 +148,7 @@ func BenchmarkappendString(b *testing.B) { } } -func BenchmarkappendBytes(b *testing.B) { +func BenchmarkAppendBytes(b *testing.B) { tests := map[string]string{ "NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, "EncodingFirst": `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, diff --git a/internal/json/time.go b/internal/json/time.go index 612438d..866cc4d 100644 --- a/internal/json/time.go +++ b/internal/json/time.go @@ -5,6 +5,8 @@ import ( "time" ) +// AppendTime formats the input time with the given format +// and appends the encoded string to the input byte slice. func AppendTime(dst []byte, t time.Time, format string) []byte { if format == "" { return AppendInt64(dst, t.Unix()) @@ -12,6 +14,8 @@ func AppendTime(dst []byte, t time.Time, format string) []byte { return append(t.AppendFormat(append(dst, '"'), format), '"') } +// AppendTimes converts the input times with the given format +// and appends the encoded string list to the input byte slice. func AppendTimes(dst []byte, vals []time.Time, format string) []byte { if format == "" { return appendUnixTimes(dst, vals) @@ -45,6 +49,8 @@ func appendUnixTimes(dst []byte, vals []time.Time) []byte { return dst } +// AppendDuration formats the input duration with the given unit & format +// and appends the encoded string to the input byte slice. func AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte { if useInt { return strconv.AppendInt(dst, int64(d/unit), 10) @@ -52,6 +58,8 @@ func AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool return AppendFloat64(dst, float64(d)/float64(unit)) } +// AppendDurations formats the input durations with the given unit & format +// and appends the encoded string list to the input byte slice. func AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte { if len(vals) == 0 { return append(dst, '[', ']') diff --git a/internal/json/types.go b/internal/json/types.go index bbc8e42..2f3ca24 100644 --- a/internal/json/types.go +++ b/internal/json/types.go @@ -7,10 +7,14 @@ import ( "strconv" ) +// AppendBool converts the input bool to a string and +// appends the encoded string to the input byte slice. func AppendBool(dst []byte, val bool) []byte { return strconv.AppendBool(dst, val) } +// AppendBools encodes the input bools to json and +// appends the encoded string list to the input byte slice. func AppendBools(dst []byte, vals []bool) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -26,10 +30,14 @@ func AppendBools(dst []byte, vals []bool) []byte { return dst } +// AppendInt converts the input int to a string and +// appends the encoded string to the input byte slice. func AppendInt(dst []byte, val int) []byte { return strconv.AppendInt(dst, int64(val), 10) } +// AppendInts encodes the input ints to json and +// appends the encoded string list to the input byte slice. func AppendInts(dst []byte, vals []int) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -45,10 +53,14 @@ func AppendInts(dst []byte, vals []int) []byte { return dst } +// AppendInt8 converts the input []int8 to a string and +// appends the encoded string to the input byte slice. func AppendInt8(dst []byte, val int8) []byte { return strconv.AppendInt(dst, int64(val), 10) } +// AppendInts8 encodes the input int8s to json and +// appends the encoded string list to the input byte slice. func AppendInts8(dst []byte, vals []int8) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -64,10 +76,14 @@ func AppendInts8(dst []byte, vals []int8) []byte { return dst } +// AppendInt16 converts the input int16 to a string and +// appends the encoded string to the input byte slice. func AppendInt16(dst []byte, val int16) []byte { return strconv.AppendInt(dst, int64(val), 10) } +// AppendInts16 encodes the input int16s to json and +// appends the encoded string list to the input byte slice. func AppendInts16(dst []byte, vals []int16) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -83,10 +99,14 @@ func AppendInts16(dst []byte, vals []int16) []byte { return dst } +// AppendInt32 converts the input int32 to a string and +// appends the encoded string to the input byte slice. func AppendInt32(dst []byte, val int32) []byte { return strconv.AppendInt(dst, int64(val), 10) } +// AppendInts32 encodes the input int32s to json and +// appends the encoded string list to the input byte slice. func AppendInts32(dst []byte, vals []int32) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -102,10 +122,14 @@ func AppendInts32(dst []byte, vals []int32) []byte { return dst } +// AppendInt64 converts the input int64 to a string and +// appends the encoded string to the input byte slice. func AppendInt64(dst []byte, val int64) []byte { return strconv.AppendInt(dst, val, 10) } +// AppendInts64 encodes the input int64s to json and +// appends the encoded string list to the input byte slice. func AppendInts64(dst []byte, vals []int64) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -121,10 +145,14 @@ func AppendInts64(dst []byte, vals []int64) []byte { return dst } +// AppendUint converts the input uint to a string and +// appends the encoded string to the input byte slice. func AppendUint(dst []byte, val uint) []byte { return strconv.AppendUint(dst, uint64(val), 10) } +// AppendUints encodes the input uints to json and +// appends the encoded string list to the input byte slice. func AppendUints(dst []byte, vals []uint) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -140,10 +168,14 @@ func AppendUints(dst []byte, vals []uint) []byte { return dst } +// AppendUint8 converts the input uint8 to a string and +// appends the encoded string to the input byte slice. func AppendUint8(dst []byte, val uint8) []byte { return strconv.AppendUint(dst, uint64(val), 10) } +// AppendUints8 encodes the input uint8s to json and +// appends the encoded string list to the input byte slice. func AppendUints8(dst []byte, vals []uint8) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -159,10 +191,14 @@ func AppendUints8(dst []byte, vals []uint8) []byte { return dst } +// AppendUint16 converts the input uint16 to a string and +// appends the encoded string to the input byte slice. func AppendUint16(dst []byte, val uint16) []byte { return strconv.AppendUint(dst, uint64(val), 10) } +// AppendUints16 encodes the input uint16s to json and +// appends the encoded string list to the input byte slice. func AppendUints16(dst []byte, vals []uint16) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -178,10 +214,14 @@ func AppendUints16(dst []byte, vals []uint16) []byte { return dst } +// AppendUint32 converts the input uint32 to a string and +// appends the encoded string to the input byte slice. func AppendUint32(dst []byte, val uint32) []byte { return strconv.AppendUint(dst, uint64(val), 10) } +// AppendUints32 encodes the input uint32s to json and +// appends the encoded string list to the input byte slice. func AppendUints32(dst []byte, vals []uint32) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -197,10 +237,14 @@ func AppendUints32(dst []byte, vals []uint32) []byte { return dst } +// AppendUint64 converts the input uint64 to a string and +// appends the encoded string to the input byte slice. func AppendUint64(dst []byte, val uint64) []byte { return strconv.AppendUint(dst, uint64(val), 10) } +// AppendUints64 encodes the input uint64s to json and +// appends the encoded string list to the input byte slice. func AppendUints64(dst []byte, vals []uint64) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -216,6 +260,8 @@ func AppendUints64(dst []byte, vals []uint64) []byte { return dst } +// AppendFloat converts the input float to a string and +// appends the encoded string to the input byte slice. func AppendFloat(dst []byte, val float64, bitSize int) []byte { // JSON does not permit NaN or Infinity. A typical JSON encoder would fail // with an error, but a logging library wants the data to get thru so we @@ -231,10 +277,14 @@ func AppendFloat(dst []byte, val float64, bitSize int) []byte { return strconv.AppendFloat(dst, val, 'f', -1, bitSize) } +// AppendFloat32 converts the input float32 to a string and +// appends the encoded string to the input byte slice. func AppendFloat32(dst []byte, val float32) []byte { return AppendFloat(dst, float64(val), 32) } +// AppendFloats32 encodes the input float32s to json and +// appends the encoded string list to the input byte slice. func AppendFloats32(dst []byte, vals []float32) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -250,10 +300,14 @@ func AppendFloats32(dst []byte, vals []float32) []byte { return dst } +// AppendFloat64 converts the input float64 to a string and +// appends the encoded string to the input byte slice. func AppendFloat64(dst []byte, val float64) []byte { return AppendFloat(dst, val, 64) } +// AppendFloats64 encodes the input float64s to json and +// appends the encoded string list to the input byte slice. func AppendFloats64(dst []byte, vals []float64) []byte { if len(vals) == 0 { return append(dst, '[', ']') @@ -269,6 +323,8 @@ func AppendFloats64(dst []byte, vals []float64) []byte { return dst } +// AppendInterface marshals the input interface to a string and +// appends the encoded string to the input byte slice. func AppendInterface(dst []byte, i interface{}) []byte { marshaled, err := json.Marshal(i) if err != nil { diff --git a/log_test.go b/log_test.go index 38ce7f1..0f5e4cc 100644 --- a/log_test.go +++ b/log_test.go @@ -77,7 +77,9 @@ func TestInfo(t *testing.T) { func TestWith(t *testing.T) { out := &bytes.Buffer{} ctx := New(out).With(). - Str("foo", "bar"). + Str("string", "foo"). + Bytes("bytes", []byte("bar")). + Hex("hex", []byte{0x12, 0xef}). RawJSON("json", []byte(`{"some":"json"}`)). AnErr("some_err", nil). Err(errors.New("some error")). @@ -99,7 +101,7 @@ func TestWith(t *testing.T) { caller := fmt.Sprintf("%s:%d", file, line+3) log := ctx.Caller().Logger() log.Log().Msg("") - if got, want := out.String(), `{"foo":"bar","json":{"some":"json"},"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,"float32":11,"float64":12,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want { + if got, want := out.String(), `{"string":"foo","bytes":"bar","hex":"12ef","json":{"some":"json"},"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,"float32":11,"float64":12,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -143,6 +145,7 @@ func TestFields(t *testing.T) { Caller(). Str("string", "foo"). Bytes("bytes", []byte("bar")). + Hex("hex", []byte{0x12, 0xef}). RawJSON("json", []byte(`{"some":"json"}`)). AnErr("some_err", nil). Err(errors.New("some error")). @@ -163,7 +166,7 @@ func TestFields(t *testing.T) { Time("time", time.Time{}). TimeDiff("diff", now, now.Add(-10*time.Second)). Msg("") - if got, want := out.String(), `{"caller":"`+caller+`","string":"foo","bytes":"bar","json":{"some":"json"},"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,"float32":11,"float64":12,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want { + if got, want := out.String(), `{"caller":"`+caller+`","string":"foo","bytes":"bar","hex":"12ef","json":{"some":"json"},"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,"float32":11,"float64":12,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -256,6 +259,7 @@ func TestFieldsDisabled(t *testing.T) { log.Debug(). Str("string", "foo"). Bytes("bytes", []byte("bar")). + Hex("hex", []byte{0x12, 0xef}). AnErr("some_err", nil). Err(errors.New("some error")). Bool("bool", true).