This commit is contained in:
a 2023-07-17 11:43:17 -05:00
commit 3a569c99c0
Signed by: a
GPG Key ID: 374BC539FE795AF0
15 changed files with 154 additions and 16 deletions

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout code - name: Checkout code

View File

@ -30,7 +30,7 @@ Find out [who uses zlog](https://tuxpa.in/a/zlog/wiki/Who-uses-zlog) and add you
* [Sampling](#log-sampling) * [Sampling](#log-sampling)
* [Hooks](#hooks) * [Hooks](#hooks)
* [Contextual fields](#contextual-logging) * [Contextual fields](#contextual-logging)
* `context.Context` integration * [`context.Context` integration](#contextcontext-integration)
* [Integration with `net/http`](#integration-with-nethttp) * [Integration with `net/http`](#integration-with-nethttp)
* [JSON and CBOR encoding formats](#binary-encoding) * [JSON and CBOR encoding formats](#binary-encoding)
* [Pretty logging for development](#pretty-logging) * [Pretty logging for development](#pretty-logging)
@ -505,7 +505,7 @@ log.Ctx(ctx).Info().Msg("hello world")
### Set as standard logger output ### Set as standard logger output
```go ```go
log := zlog.New(os.Stdout).With(). stdlog := zlog.New(os.Stdout).With().
Str("foo", "bar"). Str("foo", "bar").
Logger() Logger()
@ -517,6 +517,30 @@ stdlog.Print("hello world")
// Output: {"foo":"bar","message":"hello world"} // Output: {"foo":"bar","message":"hello world"}
``` ```
### context.Context integration
The `Logger` instance could be attached to `context.Context` values with `logger.WithContext(ctx)`
and extracted from it using `zerolog.Ctx(ctx)`.
Example to add logger to context:
```go
// this code attach logger instance to context fields
ctx := context.Background()
logger := zerolog.New(os.Stdout)
ctx = logger.WithContext(ctx)
someFunc(ctx)
```
Extracting logger from context:
```go
func someFunc(ctx context.Context) {
// get logger from context. if it's nill, then `zerolog.DefaultContextLogger` is returned,
// if `DefaultContextLogger` is nil, then disabled logger returned.
logger := zerolog.Ctx(ctx)
logger.Info().Msg("Hello")
}
```
### Integration with `net/http` ### Integration with `net/http`
The `tuxpa.in/a/zlog/hlog` package provides some helpers to integrate zlog with `http.Handler`. The `tuxpa.in/a/zlog/hlog` package provides some helpers to integrate zlog with `http.Handler`.

View File

@ -329,8 +329,9 @@ func (ts timestampHook) Run(e *Event, level Level, msg string) {
var th = timestampHook{} var th = timestampHook{}
// Timestamp adds the current local time as UNIX timestamp to the logger context with the "time" key. // Timestamp adds the current local time to the logger context with the "time" key, formatted using zlog.TimeFieldFormat.
// To customize the key name, change zlog.TimestampFieldName. // To customize the key name, change zlog.TimestampFieldName.
// To customize the time format, change zlog.TimeFieldFormat.
// //
// NOTE: It won't dedupe the "time" key if the *Context has one already. // NOTE: It won't dedupe the "time" key if the *Context has one already.
func (c Context) Timestamp() Context { func (c Context) Timestamp() Context {

View File

@ -24,6 +24,9 @@ func init() {
func appendJSON(dst []byte, j []byte) []byte { func appendJSON(dst []byte, j []byte) []byte {
return cbor.AppendEmbeddedJSON(dst, j) 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 // decodeIfBinaryToString - converts a binary formatted log msg to a
// JSON formatted String Log message. // JSON formatted String Log message.

View File

@ -1,3 +1,4 @@
//go:build !binary_log
// +build !binary_log // +build !binary_log
package zlog package zlog
@ -6,6 +7,7 @@ package zlog
// JSON encoded byte stream. // JSON encoded byte stream.
import ( import (
"encoding/base64"
"tuxpa.in/a/zlog/internal/json" "tuxpa.in/a/zlog/internal/json"
) )
@ -25,6 +27,17 @@ func init() {
func appendJSON(dst []byte, j []byte) []byte { func appendJSON(dst []byte, j []byte) []byte {
return append(dst, j...) 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 { func decodeIfBinaryToString(in []byte) string {
return string(in) return string(in)

View File

@ -318,6 +318,18 @@ func (e *Event) RawJSON(key string, b []byte) *Event {
return e 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. // AnErr adds the field key with serialized err 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 {

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/go-systemd/v22 v22.5.0
github.com/mattn/go-colorable v0.1.12 github.com/mattn/go-colorable v0.1.12
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rs/xid v1.4.0 github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.28.0 github.com/rs/zerolog v1.28.0
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
) )

3
go.sum
View File

@ -8,8 +8,9 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -26,7 +26,8 @@ const (
additionalTypeBreak byte = 31 additionalTypeBreak byte = 31
// Tag Sub-types. // 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 // Extended Tags - from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml
additionalTypeTagNetworkAddr uint16 = 260 additionalTypeTagNetworkAddr uint16 = 260

View File

@ -5,6 +5,7 @@ package cbor
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"math" "math"
@ -213,6 +214,31 @@ func decodeString(src *bufio.Reader, noQuotes bool) []byte {
} }
return append(result, '"') 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 { func decodeUTF8String(src *bufio.Reader) []byte {
pb := readByte(src) pb := readByte(src)
@ -349,6 +375,20 @@ func decodeTagData(src *bufio.Reader) []byte {
switch minor { switch minor {
case additionalTypeTimestamp: case additionalTypeTimestamp:
return decodeTimeStamp(src) 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). // Tag value is larger than 256 (so uint16).
case additionalTypeIntUint16: case additionalTypeIntUint16:

View File

@ -93,3 +93,25 @@ func AppendEmbeddedJSON(dst, s []byte) []byte {
} }
return append(dst, s...) 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...)
}

4
log.go
View File

@ -308,7 +308,9 @@ func (l Logger) Sample(s Sampler) Logger {
// Hook returns a logger with the h Hook. // Hook returns a logger with the h Hook.
func (l Logger) Hook(h Hook) Logger { func (l Logger) Hook(h Hook) Logger {
l.hooks = append(l.hooks, h) newHooks := make([]Hook, len(l.hooks), len(l.hooks)+1)
copy(newHooks, l.hooks)
l.hooks = append(newHooks, h)
return l return l
} }

View File

@ -320,6 +320,7 @@ func TestFields(t *testing.T) {
Bytes("bytes", []byte("bar")). Bytes("bytes", []byte("bar")).
Hex("hex", []byte{0x12, 0xef}). Hex("hex", []byte{0x12, 0xef}).
RawJSON("json", []byte(`{"some":"json"}`)). 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") }). Func(func(e *Event) { e.Str("func", "func_output") }).
AnErr("some_err", nil). AnErr("some_err", nil).
Err(errors.New("some error")). Err(errors.New("some error")).
@ -344,7 +345,7 @@ func TestFields(t *testing.T) {
Time("time", time.Time{}). Time("time", time.Time{}).
TimeDiff("diff", now, now.Add(-10*time.Second)). TimeDiff("diff", now, now.Add(-10*time.Second)).
Msg("") 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) t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
} }
} }

View File

@ -42,15 +42,32 @@ func frameField(f errors.Frame, s *state, c rune) string {
// MarshalStack implements pkg/errors stack trace marshaling. // MarshalStack implements pkg/errors stack trace marshaling.
// //
// zlog.ErrorStackMarshaler = MarshalStack // zlog.ErrorStackMarshaler = MarshalStack
func MarshalStack(err error) interface{} { func MarshalStack(err error) interface{} {
type stackTracer interface { type stackTracer interface {
StackTrace() errors.StackTrace StackTrace() errors.StackTrace
} }
sterr, ok := err.(stackTracer) var sterr stackTracer
if !ok { var ok bool
for err != nil {
sterr, ok = err.(stackTracer)
if ok {
break
}
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
err = u.Unwrap()
}
if sterr == nil {
return nil return nil
} }
st := sterr.StackTrace() st := sterr.StackTrace()
s := &state{} s := &state{}
out := make([]map[string]string, 0, len(st)) out := make([]map[string]string, 0, len(st))

View File

@ -4,6 +4,7 @@ package pkgerrors
import ( import (
"bytes" "bytes"
"fmt"
"regexp" "regexp"
"testing" "testing"
@ -17,11 +18,11 @@ func TestLogStack(t *testing.T) {
out := &bytes.Buffer{} out := &bytes.Buffer{}
log := zlog.New(out) log := zlog.New(out)
err := errors.Wrap(errors.New("error message"), "from error") err := fmt.Errorf("from error: %w", errors.New("error message"))
log.Log().Stack().Err(err).Msg("") log.Log().Stack().Err(err).Msg("")
got := out.String() got := out.String()
want := `\{"stack":\[\{"func":"TestLogStack","line":"20","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n` want := `\{"stack":\[\{"func":"TestLogStack","line":"21","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n`
if ok, _ := regexp.MatchString(want, got); !ok { if ok, _ := regexp.MatchString(want, got); !ok {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
} }
@ -33,11 +34,11 @@ func TestLogStackFromContext(t *testing.T) {
out := &bytes.Buffer{} out := &bytes.Buffer{}
log := zlog.New(out).With().Stack().Logger() // calling Stack() on log context instead of event log := zlog.New(out).With().Stack().Logger() // calling Stack() on log context instead of event
err := errors.Wrap(errors.New("error message"), "from error") err := fmt.Errorf("from error: %w", errors.New("error message"))
log.Log().Err(err).Msg("") // not explicitly calling Stack() log.Log().Err(err).Msg("") // not explicitly calling Stack()
got := out.String() got := out.String()
want := `\{"stack":\[\{"func":"TestLogStackFromContext","line":"36","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n` want := `\{"stack":\[\{"func":"TestLogStackFromContext","line":"37","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n`
if ok, _ := regexp.MatchString(want, got); !ok { if ok, _ := regexp.MatchString(want, got); !ok {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
} }