diff --git a/README.md b/README.md index 95c28ff..50550b8 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,12 @@ go get cdr.dev/slog Many more examples available at [godoc](https://godoc.org/cdr.dev/slog#pkg-examples). ```go -log := sloghuman.Make(os.Stdout) +ctx := sloghuman.Make(ctx, os.Stdout) -log.Info(context.Background(), "my message here", +slog.Info(ctx, "my message here", slog.F("field_name", "something or the other"), slog.F("some_map", slog.M( - slog.F("nested_fields", "wowow"), + slog.F("nested_fields", time.Date(2000, time.February, 5, 4, 4, 4, 0, time.UTC)), )), slog.Error( xerrors.Errorf("wrap1: %w", @@ -52,14 +52,14 @@ log.Info(context.Background(), "my message here", ) ``` -![Example output screenshot](https://i.imgur.com/7MJM0VE.png) +![Example output screenshot](https://i.imgur.com/KGRmQFo.png) ## Why? -The logging library of choice at [Coder](https://github.com/cdr) has been Uber's [zap](https://github.com/uber-go/zap) -for several years now. +At [Coder](https://github.com/cdr) we’ve used Uber’s [zap](https://github.com/uber-go/zap) for several years. +It is a fantastic library for performance. Thanks Uber! -It's a fantastic library for performance but the API and developer experience is not great. +However we felt the API and developer experience could be improved. Here is a list of reasons how we improved on zap with slog. @@ -87,6 +87,8 @@ Here is a list of reasons how we improved on zap with slog. 1. Full [context.Context](https://blog.golang.org/context) support - `slog` lets you set fields in a `context.Context` such that any log with the context prints those fields. + - `slog` stores the actual logger in the `context.Context`, following the example of + [the Go trace library](https://golang.org/pkg/runtime/trace/). Our logger doesn't bloat type and function signatures. - We wanted to be able to pull up all relevant logs for a given trace, user or request. With zap, we were plugging these fields in for every relevant log or passing around a logger with the fields set. This became very verbose. @@ -97,9 +99,7 @@ Here is a list of reasons how we improved on zap with slog. - zap is hard and confusing to extend. There are too many structures and configuration options. 1. Structured logging of Go structures with `json.Marshal` - - All values will be logged with `json.Marshal` unless they implement `fmt.Stringer` or `error`. - - You can force JSON by using [`slog.ForceJSON`](https://godoc.org/cdr.dev/slog#ForceJSON). - - One may implement [`slog.Value`](https://godoc.org/cdr.dev/slog#Value) to override the representation completely. + - Entire encoding process is documented on [godoc](https://godoc.org/cdr.dev/slog#Map.MarshalJSON). - With zap, We found ourselves often implementing zap's [ObjectMarshaler](https://godoc.org/go.uber.org/zap/zapcore#ObjectMarshaler) to log Go structures. This was verbose and most of the time we ended up only implementing `fmt.Stringer` and using `zap.Stringer` instead. diff --git a/context.go b/context.go new file mode 100644 index 0000000..d44a049 --- /dev/null +++ b/context.go @@ -0,0 +1,33 @@ +package slog + +import "context" + +type loggerCtxKey = struct{} + +type sinkContext struct { + context.Context + Sink +} + +// SinkContext is a context that implements Sink. +// It may be returned by log creators to allow for composition. +type SinkContext interface { + Sink + context.Context +} + +func contextWithLogger(ctx context.Context, l logger) SinkContext { + ctx = context.WithValue(ctx, loggerCtxKey{}, l) + return &sinkContext{ + Context: ctx, + Sink: l, + } +} + +func loggerFromContext(ctx context.Context) (logger, bool) { + v := ctx.Value(loggerCtxKey{}) + if v == nil { + return logger{}, false + } + return v.(logger), true +} diff --git a/example_forcejson_test.go b/example_forcejson_test.go deleted file mode 100644 index 89e2f06..0000000 --- a/example_forcejson_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package slog_test - -import ( - "context" - "fmt" - "os" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" -) - -type stringer struct { - X int `json:"x"` -} - -func (s *stringer) String() string { - return fmt.Sprintf("string method: %v", s.X) -} - -func (s *stringer) SlogValue() interface{} { - return slog.ForceJSON(s) -} - -func ExampleForceJSON() { - l := sloghuman.Make(os.Stdout) - - l.Info(context.Background(), "hello", slog.F("stringer", &stringer{X: 3})) - - // 2019-12-06 23:33:40.263 [INFO] hello {"stringer": {"x": 3}} -} diff --git a/example_helper_test.go b/example_helper_test.go index e06d372..853341d 100644 --- a/example_helper_test.go +++ b/example_helper_test.go @@ -12,12 +12,12 @@ import ( func httpLogHelper(ctx context.Context, status int) { slog.Helper() - l.Info(ctx, "sending HTTP response", + slog.Info(ctx, "sending HTTP response", slog.F("status", status), ) } -var l = sloghuman.Make(os.Stdout) +var l = sloghuman.Make(context.Background(), os.Stdout) func ExampleHelper() { ctx := context.Background() diff --git a/example_marshaller_test.go b/example_marshaller_test.go new file mode 100644 index 0000000..514ab62 --- /dev/null +++ b/example_marshaller_test.go @@ -0,0 +1,34 @@ +package slog_test + +import ( + "context" + "os" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" +) + +type myStruct struct { + foo int + bar int +} + +func (s myStruct) MarshalJSON() ([]byte, error) { + return slog.M( + slog.F("foo", s.foo), + slog.F("bar", s.foo), + ).MarshalJSON() +} + +func Example_marshaller() { + ctx := sloghuman.Make(context.Background(), os.Stdout) + + slog.Info(ctx, "wow", + slog.F("myStruct", myStruct{ + foo: 1, + bar: 2, + }), + ) + + // 2019-12-16 17:31:37.120 [INFO] wow {"myStruct": {"foo": 1, "bar": 1}} +} diff --git a/example_test.go b/example_test.go index e32b687..3627532 100644 --- a/example_test.go +++ b/example_test.go @@ -6,6 +6,7 @@ import ( "net" "os" "testing" + "time" "go.opencensus.io/trace" "golang.org/x/xerrors" @@ -17,14 +18,14 @@ import ( ) func Example() { - log := sloghuman.Make(os.Stdout) + ctx := sloghuman.Make(context.Background(), os.Stdout) - log.Info(context.Background(), "my message here", + slog.Info(ctx, "my message here", slog.F("field_name", "something or the other"), slog.F("some_map", slog.M( - slog.F("nested_fields", "wowow"), + slog.F("nested_fields", time.Date(2000, time.February, 5, 4, 4, 4, 0, time.UTC)), )), - slog.Error( + slog.Err( xerrors.Errorf("wrap1: %w", xerrors.Errorf("wrap2: %w", io.EOF, @@ -33,7 +34,7 @@ func Example() { ), ) - // 2019-12-09 05:04:53.398 [INFO] my message here {"field_name": "something or the other", "some_map": {"nested_fields": "wowow"}} ... + // 2019-12-09 05:04:53.398 [INFO] my message here {"field_name": "something or the other", "some_map": {"nested_fields": "2000-02-05T04:04:04Z"}} ... // "error": wrap1: // main.main // /Users/nhooyr/src/cdr/scratch/example.go:22 @@ -43,6 +44,26 @@ func Example() { // - EOF } +func Example_struct() { + ctx := sloghuman.Make(context.Background(), os.Stdout) + + type hello struct { + Meow int `json:"meow"` + Bar string `json:"bar"` + M time.Time `json:"m"` + } + + slog.Info(ctx, "check out my structure", + slog.F("hello", hello{ + Meow: 1, + Bar: "barbar", + M: time.Date(2000, time.February, 5, 4, 4, 4, 0, time.UTC), + }), + ) + + // 2019-12-16 17:31:51.769 [INFO] check out my structure {"hello": {"meow": 1, "bar": "barbar", "m": "2000-02-05T04:04:04Z"}} +} + func Example_testing() { // Provided by the testing package in tests. var t testing.TB @@ -55,27 +76,27 @@ func Example_testing() { } func Example_tracing() { - log := sloghuman.Make(os.Stdout) + var ctx context.Context + ctx = sloghuman.Make(context.Background(), os.Stdout) - ctx, _ := trace.StartSpan(context.Background(), "spanName") + ctx, _ = trace.StartSpan(ctx, "spanName") - log.Info(ctx, "my msg", slog.F("hello", "hi")) + slog.Info(ctx, "my msg", slog.F("hello", "hi")) // 2019-12-09 21:59:48.110 [INFO] my msg {"trace": "f143d018d00de835688453d8dc55c9fd", "span": "f214167bf550afc3", "hello": "hi"} } func Example_multiple() { - ctx := context.Background() - l := sloghuman.Make(os.Stdout) + ctx := sloghuman.Make(context.Background(), os.Stdout) f, err := os.OpenFile("stackdriver", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) if err != nil { - l.Fatal(ctx, "failed to open stackdriver log file", slog.Error(err)) + slog.Fatal(ctx, "failed to open stackdriver log file", slog.Err(err)) } - l = slog.Make(l, slogstackdriver.Make(f)) + ctx = slog.Make(l, slogstackdriver.Make(ctx, f)) - l.Info(ctx, "log to stdout and stackdriver") + slog.Info(ctx, "log to stdout and stackdriver") // 2019-12-07 20:59:55.790 [INFO] log to stdout and stackdriver } @@ -83,41 +104,41 @@ func Example_multiple() { func ExampleWith() { ctx := slog.With(context.Background(), slog.F("field", 1)) - l := sloghuman.Make(os.Stdout) - l.Info(ctx, "msg") + ctx = sloghuman.Make(ctx, os.Stdout) + slog.Info(ctx, "msg") // 2019-12-07 20:54:23.986 [INFO] msg {"field": 1} } func ExampleStdlib() { ctx := slog.With(context.Background(), slog.F("field", 1)) - l := slog.Stdlib(ctx, sloghuman.Make(os.Stdout)) + l := slog.Stdlib(sloghuman.Make(ctx, os.Stdout)) l.Print("msg") // 2019-12-07 20:54:23.986 [INFO] (stdlib) msg {"field": 1} } -func ExampleLogger_Named() { +func ExampleNamed() { ctx := context.Background() - l := sloghuman.Make(os.Stdout) - l = l.Named("http") - l.Info(ctx, "received request", slog.F("remote address", net.IPv4(127, 0, 0, 1))) + ctx = sloghuman.Make(ctx, os.Stdout) + ctx = slog.Named(ctx, "http") + slog.Info(ctx, "received request", slog.F("remote address", net.IPv4(127, 0, 0, 1))) // 2019-12-07 21:20:56.974 [INFO] (http) received request {"remote address": "127.0.0.1"} } -func ExampleLogger_Leveled() { +func ExampleLeveled() { ctx := context.Background() - l := sloghuman.Make(os.Stdout) - l.Debug(ctx, "testing1") - l.Info(ctx, "received request") + ctx = sloghuman.Make(ctx, os.Stdout) + slog.Debug(ctx, "testing1") + slog.Info(ctx, "received request") - l = l.Leveled(slog.LevelDebug) + ctx = slog.Leveled(ctx, slog.LevelDebug) - l.Debug(ctx, "testing2") + slog.Debug(ctx, "testing2") // 2019-12-07 21:26:20.945 [INFO] received request // 2019-12-07 21:26:20.945 [DEBUG] testing2 diff --git a/example_value_test.go b/example_value_test.go deleted file mode 100644 index e889900..0000000 --- a/example_value_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package slog_test - -import ( - "context" - "os" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" -) - -type vals struct { - first int - second int -} - -func (s *vals) SlogValue() interface{} { - return slog.M( - slog.F("total", s.first+s.second), - slog.F("first", s.first), - slog.F("second", s.second), - ) -} - -func ExampleValue() { - l := sloghuman.Make(os.Stdout) - l.Info(context.Background(), "hello", slog.F("val", &vals{ - first: 3, - second: 6, - })) - - // 2019-12-07 21:06:14.636 [INFO] hello {"val": {"total": 9, "first": 3, "second": 6}} -} diff --git a/export_test.go b/export_test.go index 1266811..a682e20 100644 --- a/export_test.go +++ b/export_test.go @@ -1,5 +1,12 @@ package slog -func (l *Logger) SetExit(fn func(int)) { +import "context" + +func SetExit(ctx context.Context, fn func(int)) context.Context { + l, ok := loggerFromContext(ctx) + if !ok { + return ctx + } l.exit = fn + return contextWithLogger(ctx, l) } diff --git a/go.mod b/go.mod index 0b417c5..bfca1fb 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,21 @@ -module cdr.dev/slog +module cdr.dev/slog/v2 go 1.13 require ( - cloud.google.com/go v0.43.0 - github.com/alecthomas/chroma v0.6.6 + cloud.google.com/go v0.49.0 + github.com/alecthomas/chroma v0.7.0 + github.com/dlclark/regexp2 v1.2.0 // indirect github.com/fatih/color v1.7.0 - github.com/google/go-cmp v0.3.2-0.20190829225427-b1c9c4891a65 - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.9 // indirect - go.opencensus.io v0.22.1 - golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 - golang.org/x/sys v0.0.0-20190904154756-749cb33beabd // indirect - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 - google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 + github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect + github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.11 // indirect + go.opencensus.io v0.22.2 + golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 + google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 + google.golang.org/grpc v1.25.1 // indirect ) diff --git a/go.sum b/go.sum index f21f294..6bd2924 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,17 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.43.0 h1:banaiRPAM8kUVYneOSkhgcDsLzEvL25FinuiSZaH/2w= -cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= +cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= @@ -10,8 +19,8 @@ github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtix github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.6.6 h1:AwWMP1sWgMNgEiptNtV/T5GWOLtZFDdrc2ZfWx1ogmg= -github.com/alecthomas/chroma v0.6.6/go.mod h1:zVlgtbRS7BJDrDY9SB238RmpoCBCYFlLmcfZ3durxTk= +github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= +github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= @@ -19,6 +28,7 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= @@ -29,12 +39,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -48,12 +65,13 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.2-0.20190829225427-b1c9c4891a65 h1:B3yqxlLHBEoav+FDQM8ph7IIRA6AhQ70w119k3hoT2Y= -github.com/google/go-cmp v0.3.2-0.20190829225427-b1c9c4891a65/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q= +github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -66,14 +84,20 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -82,25 +106,36 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= -go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -108,7 +143,12 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISg golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -120,6 +160,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -142,9 +184,9 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -160,14 +202,25 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0 h1:Dh6fw+p6FyRl5x/FvNswO1ji0lIGzm3KP8Y9VkS9PTE= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -179,14 +232,29 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb h1:i1Ppqkc3WQXikh8bXiwHqAN5Rv3/qDCcRk0/Otx73BY= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 h1:Ygq9/SRJX9+dU0WCIICM8RkWvDw03lvB77hrhJnpxfU= -google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/assert/assert.go b/internal/assert/assert.go index 4ac73a2..71ae94f 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -3,29 +3,31 @@ package assert import ( "reflect" - "strings" "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" ) -// Equal asserts exp == act. -func Equal(t testing.TB, exp, act interface{}, name string) { - t.Helper() - diff := CmpDiff(exp, act) - if diff != "" { - t.Fatalf("unexpected %v: %v", name, diff) - } +// Diff returns a diff between exp and act. +func Diff(exp, act interface{}, opts ...cmp.Option) string { + opts = append(opts, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { + return true + })) + return cmp.Diff(exp, act, opts...) } -// NotEqual asserts exp != act. -func NotEqual(t testing.TB, exp, act interface{}, name string) { +// Equal asserts exp == act. +func Equal(t testing.TB, name string, exp, act interface{}) { t.Helper() - if CmpDiff(exp, act) == "" { - t.Fatalf("expected different %v: %+v", name, act) + if diff := Diff(exp, act); diff != "" { + t.Fatalf(`unexpected %v: diff: +%v`, name, diff) } } // Success asserts err == nil. -func Success(t testing.TB, err error, name string) { +func Success(t testing.TB, name string, err error) { t.Helper() if err != nil { t.Fatalf("unexpected error for %v: %+v", name, err) @@ -33,46 +35,30 @@ func Success(t testing.TB, err error, name string) { } // Error asserts exp != nil. -func Error(t testing.TB, err error, name string) { +func Error(t testing.TB, name string, err error) { t.Helper() if err == nil { t.Fatalf("expected error from %v", name) } } -// ErrorContains asserts the error string from err contains sub. -func ErrorContains(t testing.TB, err error, sub, name string) { - t.Helper() - Error(t, err, name) - errs := err.Error() - if !strings.Contains(errs, sub) { - t.Fatalf("error string %q from %v does not contain %q", errs, name, sub) - } -} - // True asserts true == act. -func True(t testing.TB, act bool, name string) { +func True(t testing.TB, name string, act bool) { t.Helper() - Equal(t, true, act, name) + Equal(t, name, true, act) } // False asserts false == act. -func False(t testing.TB, act bool, name string) { +func False(t testing.TB, name string, act bool) { t.Helper() - Equal(t, false, act, name) + Equal(t, name, false, act) } // Len asserts n == len(a). -func Len(t testing.TB, n int, a interface{}, name string) { +func Len(t testing.TB, name string, n int, a interface{}) { t.Helper() act := reflect.ValueOf(a).Len() if n != act { t.Fatalf("expected len(%v) == %v but got %v", name, n, act) } } - -// Nil asserts v == nil. -func Nil(t testing.TB, v interface{}, name string) { - t.Helper() - Equal(t, nil, v, name) -} diff --git a/internal/assert/cmp.go b/internal/assert/cmp.go deleted file mode 100644 index c3768be..0000000 --- a/internal/assert/cmp.go +++ /dev/null @@ -1,54 +0,0 @@ -package assert - -import ( - "reflect" - - "github.com/google/go-cmp/cmp" -) - -// CmpDiff compares exp to act and returns a human readable string representing their diff. -// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 -func CmpDiff(exp, act interface{}) string { - return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) -} - -func deepAllowUnexported(vs ...interface{}) cmp.Option { - m := make(map[reflect.Type]struct{}) - for _, v := range vs { - structTypes(reflect.ValueOf(v), m) - } - var typs []interface{} - for t := range m { - typs = append(typs, reflect.New(t).Elem().Interface()) - } - return cmp.AllowUnexported(typs...) -} - -func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { - if !v.IsValid() { - return - } - switch v.Kind() { - case reflect.Ptr: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Interface: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - structTypes(v.Index(i), m) - } - case reflect.Map: - for _, k := range v.MapKeys() { - structTypes(v.MapIndex(k), m) - } - case reflect.Struct: - m[v.Type()] = struct{}{} - for i := 0; i < v.NumField(); i++ { - structTypes(v.Field(i), m) - } - } -} diff --git a/internal/entryhuman/entry.go b/internal/entryhuman/entry.go index fcda4e9..d17f80a 100644 --- a/internal/entryhuman/entry.go +++ b/internal/entryhuman/entry.go @@ -119,11 +119,20 @@ func Fmt(w io.Writer, ent slog.SinkEntry) string { } if multilineVal != "" { - multilineVal = strings.TrimSpace(multilineVal) - multilineKey = c(w, color.FgBlue).Sprintf(`"%v"`, multilineKey) if msg != "..." { ents += " ..." } + + // Proper indentation. + lines := strings.Split(multilineVal, "\n") + for i, line := range lines[1:] { + if line != "" { + lines[i+1] = strings.Repeat(" ", len(multilineKey)+4) + line + } + } + multilineVal = strings.Join(lines, "\n") + + multilineKey = c(w, color.FgBlue).Sprintf(`"%v"`, multilineKey) ents += fmt.Sprintf("\n%v: %v", multilineKey, multilineVal) } diff --git a/internal/entryhuman/entry_test.go b/internal/entryhuman/entry_test.go index 0246ddb..8530bd7 100644 --- a/internal/entryhuman/entry_test.go +++ b/internal/entryhuman/entry_test.go @@ -19,7 +19,7 @@ func TestEntry(t *testing.T) { test := func(t *testing.T, in slog.SinkEntry, exp string) { act := entryhuman.Fmt(ioutil.Discard, in) - assert.Equal(t, exp, act, "entry") + assert.Equal(t, "entry", exp, act) } t.Run("basic", func(t *testing.T) { @@ -44,7 +44,19 @@ func TestEntry(t *testing.T) { Level: slog.LevelInfo, }, `0001-01-01 00:00:00.000 [INFO] <.:0> ... "msg": line1 -line2`) + line2`) + }) + + t.Run("multilineField", func(t *testing.T) { + t.Parallel() + + test(t, slog.SinkEntry{ + Message: "msg", + Level: slog.LevelInfo, + Fields: slog.M(slog.F("field", "line1\nline2")), + }, `0001-01-01 00:00:00.000 [INFO] <.:0> msg ... +"field": line1 + line2`) }) t.Run("named", func(t *testing.T) { @@ -77,6 +89,6 @@ line2`) slog.F("hey", "hi"), ), }) - assert.Equal(t, "0001-01-01 00:00:00.000 \x1b[91m[CRITICAL]\x1b[0m\t\x1b[36m<.:0>\x1b[0m\t\"\"\t{\x1b[34m\"hey\"\x1b[0m: \x1b[32m\"hi\"\x1b[0m}", act, "entry") + assert.Equal(t, "entry", "0001-01-01 00:00:00.000 \x1b[91m[CRITICAL]\x1b[0m\t\x1b[36m<.:0>\x1b[0m\t\"\"\t{\x1b[34m\"hey\"\x1b[0m: \x1b[32m\"hi\"\x1b[0m}", act) }) } diff --git a/internal/syncwriter/syncwriter_test.go b/internal/syncwriter/syncwriter_test.go index d6eb5e6..e001320 100644 --- a/internal/syncwriter/syncwriter_test.go +++ b/internal/syncwriter/syncwriter_test.go @@ -31,7 +31,7 @@ func TestWriter_Sync(t *testing.T) { tw := newWriter(nil) tw.w.Sync("test") - assert.Equal(t, 0, tw.errors, "errors") + assert.Equal(t, "errors", 0, tw.errors) }) t.Run("syncWriter", func(t *testing.T) { @@ -46,9 +46,9 @@ func TestWriter_Sync(t *testing.T) { }, }) tw.w.Write("hello", nil) - assert.Equal(t, 1, tw.errors, "errors") + assert.Equal(t, "errors", 1, tw.errors) tw.w.Sync("test") - assert.Equal(t, 2, tw.errors, "errors") + assert.Equal(t, "errors", 2, tw.errors) }) t.Run("stdout", func(t *testing.T) { @@ -56,7 +56,7 @@ func TestWriter_Sync(t *testing.T) { tw := newWriter(os.Stdout) tw.w.Sync("test") - assert.Equal(t, 0, tw.errors, "errors") + assert.Equal(t, "errors", 0, tw.errors) }) t.Run("errorf", func(t *testing.T) { @@ -77,8 +77,8 @@ func TestWriter_Sync(t *testing.T) { func Test_errorsIsAny(t *testing.T) { t.Parallel() - assert.True(t, errorsIsAny(io.EOF, io.ErrUnexpectedEOF, io.EOF), "err") - assert.False(t, errorsIsAny(io.EOF, io.ErrUnexpectedEOF, io.ErrClosedPipe), "err") + assert.True(t, "err", errorsIsAny(io.EOF, io.ErrUnexpectedEOF, io.EOF)) + assert.False(t, "err", errorsIsAny(io.EOF, io.ErrUnexpectedEOF, io.ErrClosedPipe)) } type syncWriter struct { diff --git a/map.go b/map.go index 6627f40..a1b594b 100644 --- a/map.go +++ b/map.go @@ -13,11 +13,6 @@ import ( // Map represents an ordered map of fields. type Map []Field -// SlogValue implements Value. -func (m Map) SlogValue() interface{} { - return ForceJSON(m) -} - var _ json.Marshaler = Map(nil) // MarshalJSON implements json.Marshaler. @@ -27,17 +22,19 @@ var _ json.Marshaler = Map(nil) // // Every field value is encoded with the following process: // -// 1. slog.Value is handled to allow any type to replace its representation for logging. +// 1. json.Marshaller is handled. // // 2. xerrors.Formatter is handled. // -// 3. Protobufs are handled with json.Marshal. +// 3. structs that have a field with a json tag are encoded with json.Marshal. // -// 4. error and fmt.Stringer are handled. +// 4. error and fmt.Stringer is handled. // -// 5. slices and arrays are handled to go through the encode function for every value. +// 5. slices and arrays go through the encode function for every element. // -// 6. json.Marshal is invoked as the default case. +// 6. For values that cannot be encoded with json.Marshal, fmt.Sprintf("%+v") is used. +// +// 7. json.Marshal(v) is used for all other values. func (m Map) MarshalJSON() ([]byte, error) { b := &bytes.Buffer{} b.WriteByte('{') @@ -56,23 +53,6 @@ func (m Map) MarshalJSON() ([]byte, error) { return b.Bytes(), nil } -// ForceJSON ensures the value is logged via json.Marshal even -// if it implements fmt.Stringer or error. -func ForceJSON(v interface{}) interface{} { - return jsonVal{v: v} -} - -type jsonVal struct { - v interface{} -} - -var _ json.Marshaler = jsonVal{} - -// MarshalJSON implements json.Marshaler. -func (v jsonVal) MarshalJSON() ([]byte, error) { - return json.Marshal(v.v) -} - func marshalList(rv reflect.Value) []byte { b := &bytes.Buffer{} b.WriteByte('[') @@ -91,40 +71,69 @@ func marshalList(rv reflect.Value) []byte { func encode(v interface{}) []byte { switch v := v.(type) { - case Value: - return encode(v.SlogValue()) + case json.Marshaler: + return encodeJSON(v) case xerrors.Formatter: return encode(errorChain(v)) - case interface { - ProtoMessage() - }: - return encode(ForceJSON(v)) + } + + rv := reflect.Indirect(reflect.ValueOf(v)) + if !rv.IsValid() { + return encodeJSON(v) + } + + if rv.Kind() == reflect.Struct { + b, ok := encodeStruct(rv) + if ok { + return b + } + } + + switch v.(type) { case error, fmt.Stringer: return encode(fmt.Sprint(v)) - default: - rv := reflect.Indirect(reflect.ValueOf(v)) - if rv.IsValid() { - switch rv.Type().Kind() { - case reflect.Slice: - if rv.IsNil() { - break - } - fallthrough - case reflect.Array: - return marshalList(rv) - } + } + + switch rv.Type().Kind() { + case reflect.Slice: + if !rv.IsNil() { + return marshalList(rv) } + case reflect.Array: + return marshalList(rv) + case reflect.Struct, reflect.Chan, reflect.Complex64, reflect.Complex128, reflect.Func: + // These types cannot be directly encoded with json.Marshal. + // See https://golang.org/pkg/encoding/json/#Marshal + return encodeJSON(fmt.Sprintf("%+v", v)) + } - b, err := json.Marshal(v) - if err != nil { - return encode(M( - Error(xerrors.Errorf("failed to marshal to JSON: %w", err)), - F("type", reflect.TypeOf(v)), - F("value", fmt.Sprintf("%+v", v)), - )) + return encodeJSON(v) +} + +func encodeStruct(rv reflect.Value) ([]byte, bool) { + if rv.Kind() == reflect.Struct { + for i := 0; i < rv.NumField(); i++ { + ft := rv.Type().Field(i) + // Found a field with a json tag. + if ft.Tag.Get("json") != "" { + return encodeJSON(rv.Interface()), true + } } - return b } + + return nil, false +} + +func encodeJSON(v interface{}) []byte { + b, err := json.Marshal(v) + if err != nil { + return encode(M( + Err(xerrors.Errorf("failed to marshal to JSON: %w", err)), + F("type", reflect.TypeOf(v)), + F("value", fmt.Sprintf("%+v", v)), + )) + } + return b } func errorChain(f xerrors.Formatter) []interface{} { diff --git a/map_test.go b/map_test.go index 57f44f0..03d7886 100644 --- a/map_test.go +++ b/map_test.go @@ -3,11 +3,11 @@ package slog_test import ( "bytes" "encoding/json" - "fmt" "io" "runtime" "strings" "testing" + "time" "golang.org/x/xerrors" @@ -24,7 +24,7 @@ func TestMap(t *testing.T) { t.Helper() exp = indentJSON(t, exp) act := marshalJSON(t, m) - assert.Equal(t, exp, act, "JSON") + assert.Equal(t, "JSON", exp, act) } t.Run("JSON", func(t *testing.T) { @@ -37,7 +37,7 @@ func TestMap(t *testing.T) { } test(t, slog.M( - slog.Error( + slog.Err( xerrors.Errorf("wrap1: %w", xerrors.Errorf("wrap2: %w", io.EOF, @@ -86,19 +86,19 @@ func TestMap(t *testing.T) { mapTestFile := strings.Replace(mapTestFile, "_test", "", 1) test(t, slog.M( - slog.F("meow", indentJSON), + slog.F("meow", complexJSON(complex(10, 10))), ), `{ "meow": { "error": [ { "msg": "failed to marshal to JSON", - "fun": "cdr.dev/slog.encode", - "loc": "`+mapTestFile+`:121" + "fun": "cdr.dev/slog.encodeJSON", + "loc": "`+mapTestFile+`:131" }, - "json: unsupported type: func(*testing.T, string) string" + "json: error calling MarshalJSON for type slog_test.complexJSON: json: unsupported type: complex128" ], - "type": "func(*testing.T, string) string", - "value": "`+fmt.Sprint(interface{}(indentJSON))+`" + "type": "slog_test.complexJSON", + "value": "(10+10i)" } }`) }) @@ -145,55 +145,99 @@ func TestMap(t *testing.T) { }`) }) - t.Run("forceJSON", func(t *testing.T) { + t.Run("array", func(t *testing.T) { t.Parallel() test(t, slog.M( - slog.F("error", slog.ForceJSON(io.EOF)), + slog.F("meow", [3]string{ + "1", + "2", + "3", + }), ), `{ - "error": {} + "meow": [ + "1", + "2", + "3" + ] }`) }) - t.Run("value", func(t *testing.T) { + t.Run("nilSlice", func(t *testing.T) { t.Parallel() test(t, slog.M( - slog.F("error", meow{1}), + slog.F("slice", []string(nil)), ), `{ - "error": "xdxd" + "slice": null }`) }) - t.Run("nilSlice", func(t *testing.T) { + t.Run("nil", func(t *testing.T) { t.Parallel() test(t, slog.M( - slog.F("slice", []string(nil)), + slog.F("val", nil), ), `{ - "slice": null + "val": null }`) }) -} -type meow struct { - a int -} + t.Run("json.Marshaler", func(t *testing.T) { + t.Parallel() -func (m meow) SlogValue() interface{} { - return "xdxd" + test(t, slog.M( + slog.F("val", time.Date(2000, 02, 05, 4, 4, 4, 0, time.UTC)), + ), `{ + "val": "2000-02-05T04:04:04Z" + }`) + }) + + t.Run("complex", func(t *testing.T) { + t.Parallel() + + test(t, slog.M( + slog.F("val", complex(10, 10)), + ), `{ + "val": "(10+10i)" + }`) + }) + + t.Run("privateStruct", func(t *testing.T) { + t.Parallel() + + test(t, slog.M( + slog.F("val", struct { + meow string + bar int + far uint + }{ + meow: "hi", + bar: 23, + far: 600, + }), + ), `{ + "val": "{meow:hi bar:23 far:600}" + }`) + }) } func indentJSON(t *testing.T, j string) string { b := &bytes.Buffer{} err := json.Indent(b, []byte(j), "", strings.Repeat(" ", 4)) - assert.Success(t, err, "indent JSON") + assert.Success(t, "indent JSON", err) return b.String() } func marshalJSON(t *testing.T, m slog.Map) string { actb, err := json.Marshal(m) - assert.Success(t, err, "marshal map to JSON") + assert.Success(t, "marshal map to JSON", err) return indentJSON(t, string(actb)) } + +type complexJSON complex128 + +func (c complexJSON) MarshalJSON() ([]byte, error) { + return json.Marshal(complex128(c)) +} diff --git a/s.go b/s.go index fa8917e..e85195c 100644 --- a/s.go +++ b/s.go @@ -3,6 +3,7 @@ package slog import ( "context" "log" + "os" "strings" ) @@ -15,14 +16,19 @@ import ( // You can redirect the stdlib default logger with log.SetOutput // to the Writer on the logger returned by this function. // See the example. -func Stdlib(ctx context.Context, l Logger) *log.Logger { - l.skip += 3 +func Stdlib(ctx context.Context) *log.Logger { + ctx = Named(ctx, "stdlib") - l = l.Named("stdlib") + l, ok := loggerFromContext(ctx) + if !ok { + // Give stderr logger if no slog. + return log.New(os.Stderr, "", 0) + } + l.skip += 3 + ctx = contextWithLogger(ctx, l) w := &stdlogWriter{ ctx: ctx, - l: l, } return log.New(w, "", 0) @@ -30,7 +36,6 @@ func Stdlib(ctx context.Context, l Logger) *log.Logger { type stdlogWriter struct { ctx context.Context - l Logger } func (w stdlogWriter) Write(p []byte) (n int, err error) { @@ -39,7 +44,7 @@ func (w stdlogWriter) Write(p []byte) (n int, err error) { // we do not want. msg = strings.TrimSuffix(msg, "\n") - w.l.Info(w.ctx, msg) + Info(w.ctx, msg) return len(p), nil } diff --git a/s_test.go b/s_test.go index 2cdf78f..5006c04 100644 --- a/s_test.go +++ b/s_test.go @@ -2,6 +2,7 @@ package slog_test import ( "bytes" + "context" "testing" "cdr.dev/slog" @@ -14,14 +15,16 @@ func TestStdlib(t *testing.T) { t.Parallel() b := &bytes.Buffer{} - l := slog.Make(sloghuman.Make(b)).With( + ctx := context.Background() + ctx = slog.Make(sloghuman.Make(ctx, b)) + ctx = slog.With(ctx, slog.F("hi", "we"), ) - stdlibLog := slog.Stdlib(bg, l) + stdlibLog := slog.Stdlib(ctx) stdlibLog.Println("stdlib") et, rest, err := entryhuman.StripTimestamp(b.String()) - assert.Success(t, err, "strip timestamp") - assert.False(t, et.IsZero(), "timestamp") - assert.Equal(t, " [INFO]\t(stdlib)\t\tstdlib\t{\"hi\": \"we\"}\n", rest, "entry") + assert.Success(t, "strip timestamp", err) + assert.False(t, "timestamp", et.IsZero()) + assert.Equal(t, "entry", " [INFO]\t(stdlib)\t\tstdlib\t{\"hi\": \"we\"}\n", rest) } diff --git a/slog.go b/slog.go index 6ca8160..bfbecde 100644 --- a/slog.go +++ b/slog.go @@ -4,8 +4,8 @@ // // The examples are the best way to understand how to use this library effectively. // -// The Logger type implements a high level API around the Sink interface. -// Logger implements Sink as well to allow composition. +// The logger type implements a high level API around the Sink interface. +// logger implements Sink as well to allow composition. // // Implementations of the Sink interface are available in the sloggers subdirectory. package slog // import "cdr.dev/slog" @@ -21,7 +21,7 @@ import ( "go.opencensus.io/trace" ) -// Sink is the destination of a Logger. +// Sink is the destination of a logger. // // All sinks must be safe for concurrent use. type Sink interface { @@ -33,7 +33,7 @@ type Sink interface { // underlying sinks. // // It extends the entry with the set fields and names. -func (l Logger) LogEntry(ctx context.Context, e SinkEntry) { +func (l logger) LogEntry(ctx context.Context, e SinkEntry) { if e.Level < l.level { return } @@ -46,17 +46,27 @@ func (l Logger) LogEntry(ctx context.Context, e SinkEntry) { } } -// Sync calls Sync on all the underlying sinks. -func (l Logger) Sync() { +func (l logger) Sync() { for _, s := range l.sinks { s.Sync() } } -// Logger wraps Sink with a nice API to log entries. +// Sync calls Sync on all the underlying sinks. +func Sync(ctx context.Context) { + l, ok := loggerFromContext(ctx) + if !ok { + return + } + l.Sync() + return +} + +// logger wraps Sink with a nice API to log entries. // -// Logger is safe for concurrent use. -type Logger struct { +// logger is safe for concurrent use. +// It is unexported because callers should only log via a context. +type logger struct { sinks []Sink level Level @@ -68,34 +78,53 @@ type Logger struct { } // Make creates a logger that writes logs to the passed sinks at LevelInfo. -func Make(sinks ...Sink) Logger { - return Logger{ - sinks: sinks, - level: LevelInfo, - - exit: os.Exit, +func Make(ctx context.Context, sinks ...Sink) SinkContext { + // Just in case the ctx has a logger, start with it. + l, _ := loggerFromContext(ctx) + l.sinks = append(l.sinks, sinks...) + if l.level == 0 { + l.level = LevelInfo } + l.exit = os.Exit + + return contextWithLogger(ctx, l) } // Debug logs the msg and fields at LevelDebug. -func (l Logger) Debug(ctx context.Context, msg string, fields ...Field) { +func Debug(ctx context.Context, msg string, fields ...Field) { + l, ok := loggerFromContext(ctx) + if !ok { + return + } l.log(ctx, LevelDebug, msg, fields) } // Info logs the msg and fields at LevelInfo. -func (l Logger) Info(ctx context.Context, msg string, fields ...Field) { +func Info(ctx context.Context, msg string, fields ...Field) { + l, ok := loggerFromContext(ctx) + if !ok { + return + } l.log(ctx, LevelInfo, msg, fields) } // Warn logs the msg and fields at LevelWarn. -func (l Logger) Warn(ctx context.Context, msg string, fields ...Field) { +func Warn(ctx context.Context, msg string, fields ...Field) { + l, ok := loggerFromContext(ctx) + if !ok { + return + } l.log(ctx, LevelWarn, msg, fields) } // Error logs the msg and fields at LevelError. // // It will then Sync(). -func (l Logger) Error(ctx context.Context, msg string, fields ...Field) { +func Error(ctx context.Context, msg string, fields ...Field) { + l, ok := loggerFromContext(ctx) + if !ok { + return + } l.log(ctx, LevelError, msg, fields) l.Sync() } @@ -103,7 +132,11 @@ func (l Logger) Error(ctx context.Context, msg string, fields ...Field) { // Critical logs the msg and fields at LevelCritical. // // It will then Sync(). -func (l Logger) Critical(ctx context.Context, msg string, fields ...Field) { +func Critical(ctx context.Context, msg string, fields ...Field) { + l, ok := loggerFromContext(ctx) + if !ok { + return + } l.log(ctx, LevelCritical, msg, fields) l.Sync() } @@ -111,41 +144,47 @@ func (l Logger) Critical(ctx context.Context, msg string, fields ...Field) { // Fatal logs the msg and fields at LevelFatal. // // It will then Sync() and os.Exit(1). -func (l Logger) Fatal(ctx context.Context, msg string, fields ...Field) { +func Fatal(ctx context.Context, msg string, fields ...Field) { + l, ok := loggerFromContext(ctx) + if !ok { + os.Stderr.WriteString("Fatal called but no Logger in context") + // The caller expects the program to terminate after Fatal no matter what. + l.exit(1) + return + } l.log(ctx, LevelFatal, msg, fields) l.Sync() l.exit(1) } -// With returns a Logger that prepends the given fields on every -// logged entry. -// -// It will append to any fields already in the Logger. -func (l Logger) With(fields ...Field) Logger { - l.fields = l.fields.append(fields) - return l -} - // Named appends the name to the set names // on the logger. -func (l Logger) Named(name string) Logger { +func Named(ctx context.Context, name string) context.Context { + l, ok := loggerFromContext(ctx) + if !ok { + return ctx + } l.names = appendNames(l.names, name) - return l + return contextWithLogger(ctx, l) } -// Leveled returns a Logger that only logs entries +// Leveled returns a logger that only logs entries // equal to or above the given level. -func (l Logger) Leveled(level Level) Logger { +func Leveled(ctx context.Context, level Level) context.Context { + l, ok := loggerFromContext(ctx) + if !ok { + return ctx + } l.level = level - return l + return contextWithLogger(ctx, l) } -func (l Logger) log(ctx context.Context, level Level, msg string, fields Map) { +func (l logger) log(ctx context.Context, level Level, msg string, fields Map) { ent := l.entry(ctx, level, msg, fields) l.LogEntry(ctx, ent) } -func (l Logger) entry(ctx context.Context, level Level, msg string, fields Map) SinkEntry { +func (l logger) entry(ctx context.Context, level Level, msg string, fields Map) SinkEntry { ent := SinkEntry{ Time: time.Now().UTC(), Level: level, @@ -226,16 +265,8 @@ func M(fs ...Field) Map { return fs } -// Value represents a log value. -// -// Implement SlogValue in your own types to override -// the value encoded when logging. -type Value interface { - SlogValue() interface{} -} - -// Error is the standard key used for logging a Go error value. -func Error(err error) Field { +// Err is the standard key used for logging a Go error value. +func Err(err error) Field { return F("error", err) } @@ -287,11 +318,23 @@ type Level int // // The default level is Info. const ( + // LevelDebug is used for development and debugging messages. LevelDebug Level = iota + + // LevelInfo is used for normal informational messages. LevelInfo + + // LevelWarn is used when something has possibly gone wrong. LevelWarn + + // LevelError is used when something has certainly gone wrong. LevelError + + // LevelCritical is used when when something has gone wrong and should + // be immediately investigated. LevelCritical + + // LevelFatal is used when the process is about to exit due to an error. LevelFatal ) diff --git a/slog_test.go b/slog_test.go index 741bddb..3c88989 100644 --- a/slog_test.go +++ b/slog_test.go @@ -28,8 +28,6 @@ func (s *fakeSink) Sync() { s.syncs++ } -var bg = context.Background() - func TestLogger(t *testing.T) { t.Parallel() @@ -38,35 +36,37 @@ func TestLogger(t *testing.T) { s1 := &fakeSink{} s2 := &fakeSink{} - l := slog.Make(s1, s2) - l = l.Leveled(slog.LevelError) + var ctx context.Context + ctx = slog.Make(context.Background(), s1, s2) + ctx = slog.Leveled(ctx, slog.LevelError) - l.Info(bg, "wow", slog.Error(io.EOF)) - l.Error(bg, "meow", slog.Error(io.ErrUnexpectedEOF)) + slog.Info(ctx, "wow", slog.Err(io.EOF)) + slog.Error(ctx, "meow", slog.Err(io.ErrUnexpectedEOF)) - assert.Equal(t, 1, s1.syncs, "syncs") - assert.Len(t, 1, s1.entries, "entries") + assert.Equal(t, "syncs", 1, s1.syncs) + assert.Len(t, "entries", 1, s1.entries) - assert.Equal(t, s1, s2, "sinks") + assert.Equal(t, "sinks", s1, s2) }) t.Run("helper", func(t *testing.T) { t.Parallel() s := &fakeSink{} - l := slog.Make(s) + var ctx context.Context + ctx = slog.Make(context.Background(), s) h := func(ctx context.Context) { slog.Helper() - l.Info(ctx, "logging in helper") + slog.Info(ctx, "logging in helper") } - ctx := slog.With(bg, slog.F( + ctx = slog.With(ctx, slog.F( "ctx", 1024), ) h(ctx) - assert.Len(t, 1, s.entries, "entries") - assert.Equal(t, slog.SinkEntry{ + assert.Len(t, "entries", 1, s.entries) + assert.Equal(t, "entry", slog.SinkEntry{ Time: s.entries[0].Time, Level: slog.LevelInfo, @@ -79,25 +79,26 @@ func TestLogger(t *testing.T) { Fields: slog.M( slog.F("ctx", 1024), ), - }, s.entries[0], "entry") + }, s.entries[0]) }) t.Run("entry", func(t *testing.T) { t.Parallel() s := &fakeSink{} - l := slog.Make(s) - l = l.Named("hello") - l = l.Named("hello2") + var ctx context.Context + ctx = slog.Make(context.Background(), s) + ctx = slog.Named(ctx, "hello") + ctx = slog.Named(ctx, "hello2") - ctx, span := trace.StartSpan(bg, "trace") + ctx, span := trace.StartSpan(ctx, "trace") ctx = slog.With(ctx, slog.F("ctx", io.EOF)) - l = l.With(slog.F("with", 2)) + ctx = slog.With(ctx, slog.F("with", 2)) - l.Info(ctx, "meow", slog.F("hi", "xd")) + slog.Info(ctx, "meow", slog.F("hi", "xd")) - assert.Len(t, 1, s.entries, "entries") - assert.Equal(t, slog.SinkEntry{ + assert.Len(t, "entries", 1, s.entries) + assert.Equal(t, "entry", slog.SinkEntry{ Time: s.entries[0].Time, Level: slog.LevelInfo, @@ -107,51 +108,52 @@ func TestLogger(t *testing.T) { File: slogTestFile, Func: "cdr.dev/slog_test.TestLogger.func3", - Line: 97, + Line: 98, SpanContext: span.SpanContext(), Fields: slog.M( - slog.F("with", 2), slog.F("ctx", io.EOF), + slog.F("with", 2), slog.F("hi", "xd"), ), - }, s.entries[0], "entry") + }, s.entries[0]) }) t.Run("levels", func(t *testing.T) { t.Parallel() s := &fakeSink{} - l := slog.Make(s) + var ctx context.Context + ctx = slog.Make(context.Background(), s) exits := 0 - l.SetExit(func(int) { + ctx = slog.SetExit(ctx, func(int) { exits++ }) - l = l.Leveled(slog.LevelDebug) - l.Debug(bg, "") - l.Info(bg, "") - l.Warn(bg, "") - l.Error(bg, "") - l.Critical(bg, "") - l.Fatal(bg, "") - - assert.Len(t, 6, s.entries, "entries") - assert.Equal(t, 3, s.syncs, "syncs") - assert.Equal(t, slog.LevelDebug, s.entries[0].Level, "level") - assert.Equal(t, slog.LevelInfo, s.entries[1].Level, "level") - assert.Equal(t, slog.LevelWarn, s.entries[2].Level, "level") - assert.Equal(t, slog.LevelError, s.entries[3].Level, "level") - assert.Equal(t, slog.LevelCritical, s.entries[4].Level, "level") - assert.Equal(t, slog.LevelFatal, s.entries[5].Level, "level") - assert.Equal(t, 1, exits, "exits") + ctx = slog.Leveled(ctx, slog.LevelDebug) + slog.Debug(ctx, "") + slog.Info(ctx, "") + slog.Warn(ctx, "") + slog.Error(ctx, "") + slog.Critical(ctx, "") + slog.Fatal(ctx, "") + + assert.Len(t, "entries", 6, s.entries) + assert.Equal(t, "syncs", 3, s.syncs) + assert.Equal(t, "level", slog.LevelDebug, s.entries[0].Level) + assert.Equal(t, "level", slog.LevelInfo, s.entries[1].Level) + assert.Equal(t, "level", slog.LevelWarn, s.entries[2].Level) + assert.Equal(t, "level", slog.LevelError, s.entries[3].Level) + assert.Equal(t, "level", slog.LevelCritical, s.entries[4].Level) + assert.Equal(t, "level", slog.LevelFatal, s.entries[5].Level) + assert.Equal(t, "exits", 1, exits) }) } func TestLevel_String(t *testing.T) { t.Parallel() - assert.Equal(t, "slog.Level(12)", slog.Level(12).String(), "level string") + assert.Equal(t, "level string", "slog.Level(12)", slog.Level(12).String()) } diff --git a/sloggers/sloghuman/sloghuman.go b/sloggers/sloghuman/sloghuman.go index 719db7c..18cbb58 100644 --- a/sloggers/sloghuman/sloghuman.go +++ b/sloggers/sloghuman/sloghuman.go @@ -17,8 +17,8 @@ import ( // // If the writer implements Sync() error then // it will be called when syncing. -func Make(w io.Writer) slog.Logger { - return slog.Make(&humanSink{ +func Make(ctx context.Context, w io.Writer) slog.SinkContext { + return slog.Make(ctx, &humanSink{ w: syncwriter.New(w), w2: w, }) @@ -29,7 +29,7 @@ type humanSink struct { w2 io.Writer } -func (s humanSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { +func (s humanSink) LogEntry(_ context.Context, ent slog.SinkEntry) { str := entryhuman.Fmt(s.w2, ent) lines := strings.Split(str, "\n") diff --git a/sloggers/sloghuman/sloghuman_test.go b/sloggers/sloghuman/sloghuman_test.go index 71b88ca..86fe888 100644 --- a/sloggers/sloghuman/sloghuman_test.go +++ b/sloggers/sloghuman/sloghuman_test.go @@ -11,18 +11,17 @@ import ( "cdr.dev/slog/sloggers/sloghuman" ) -var bg = context.Background() - func TestMake(t *testing.T) { t.Parallel() b := &bytes.Buffer{} - l := sloghuman.Make(b) - l.Info(bg, "line1\n\nline2", slog.F("wowow", "me\nyou")) - l.Sync() + ctx := context.Background() + ctx = sloghuman.Make(ctx, b) + slog.Info(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) + slog.Sync(ctx) et, rest, err := entryhuman.StripTimestamp(b.String()) - assert.Success(t, err, "strip timestamp") - assert.False(t, et.IsZero(), "timestamp") - assert.Equal(t, " [INFO]\t\t...\t{\"wowow\": \"me\\nyou\"}\n \"msg\": line1\n\n line2\n", rest, "entry") + assert.Success(t, "strip timestamp", err) + assert.False(t, "timestamp", et.IsZero()) + assert.Equal(t, "entry", " [INFO]\t\t...\t{\"wowow\": \"me\\nyou\"}\n \"msg\": line1\n\n line2\n", rest) } diff --git a/sloggers/slogjson/slogjson.go b/sloggers/slogjson/slogjson.go index 5069ddc..06c0446 100644 --- a/sloggers/slogjson/slogjson.go +++ b/sloggers/slogjson/slogjson.go @@ -34,8 +34,8 @@ import ( // for the format. // If the writer implements Sync() error then // it will be called when syncing. -func Make(w io.Writer) slog.Logger { - return slog.Make(jsonSink{ +func Make(ctx context.Context, w io.Writer) context.Context { + return slog.Make(ctx, jsonSink{ w: syncwriter.New(w), }) } @@ -44,7 +44,7 @@ type jsonSink struct { w *syncwriter.Writer } -func (s jsonSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { +func (s jsonSink) LogEntry(_ context.Context, ent slog.SinkEntry) { m := slog.M( slog.F("ts", ent.Time), slog.F("level", ent.Level), diff --git a/sloggers/slogjson/slogjson_test.go b/sloggers/slogjson/slogjson_test.go index e34307d..103be7a 100644 --- a/sloggers/slogjson/slogjson_test.go +++ b/sloggers/slogjson/slogjson_test.go @@ -24,12 +24,12 @@ func TestMake(t *testing.T) { ctx, s := trace.StartSpan(bg, "meow") b := &bytes.Buffer{} - l := slogjson.Make(b) - l = l.Named("named") - l.Error(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) + ctx = slogjson.Make(ctx, b) + ctx = slog.Named(ctx, "named") + slog.Error(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) j := entryjson.Filter(b.String(), "ts") exp := fmt.Sprintf(`{"level":"ERROR","msg":"line1\n\nline2","caller":"%v:29","func":"cdr.dev/slog/sloggers/slogjson_test.TestMake","logger_names":["named"],"trace":"%v","span":"%v","fields":{"wowow":"me\nyou"}} `, slogjsonTestFile, s.SpanContext().TraceID, s.SpanContext().SpanID) - assert.Equal(t, exp, j, "entry") + assert.Equal(t, "entry", exp, j) } diff --git a/sloggers/slogstackdriver/slogstackdriver.go b/sloggers/slogstackdriver/slogstackdriver.go index 3b4045e..bac1294 100644 --- a/sloggers/slogstackdriver/slogstackdriver.go +++ b/sloggers/slogstackdriver/slogstackdriver.go @@ -17,14 +17,14 @@ import ( "cdr.dev/slog/internal/syncwriter" ) -// Make creates a slog.Logger configured to write JSON logs +// Make creates a slog.logger configured to write JSON logs // to stdout for stackdriver. // // See https://cloud.google.com/logging/docs/agent -func Make(w io.Writer) slog.Logger { +func Make(ctx context.Context, w io.Writer) slog.SinkContext { projectID, _ := metadata.ProjectID() - return slog.Make(stackdriverSink{ + return slog.Make(ctx, stackdriverSink{ projectID: projectID, w: syncwriter.New(w), }) @@ -35,7 +35,7 @@ type stackdriverSink struct { w *syncwriter.Writer } -func (s stackdriverSink) LogEntry(ctx context.Context, ent slog.SinkEntry) { +func (s stackdriverSink) LogEntry(_ context.Context, ent slog.SinkEntry) { // https://cloud.google.com/logging/docs/agent/configuration#special-fields e := slog.M( slog.F("severity", sev(ent.Level)), diff --git a/sloggers/slogstackdriver/slogstackdriver_test.go b/sloggers/slogstackdriver/slogstackdriver_test.go index 5e2beda..025ad9d 100644 --- a/sloggers/slogstackdriver/slogstackdriver_test.go +++ b/sloggers/slogstackdriver/slogstackdriver_test.go @@ -24,22 +24,22 @@ func TestStackdriver(t *testing.T) { ctx, s := trace.StartSpan(bg, "meow") b := &bytes.Buffer{} - l := slogstackdriver.Make(b) - l = l.Named("meow") - l.Error(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) + ctx = slogstackdriver.Make(ctx, b) + ctx = slog.Named(ctx, "meow") + slog.Error(ctx, "line1\n\nline2", slog.F("wowow", "me\nyou")) j := entryjson.Filter(b.String(), "timestamp") exp := fmt.Sprintf(`{"severity":"ERROR","message":"line1\n\nline2","logging.googleapis.com/sourceLocation":{"file":"%v","line":29,"function":"cdr.dev/slog/sloggers/slogstackdriver_test.TestStackdriver"},"logging.googleapis.com/operation":{"producer":"meow"},"logging.googleapis.com/trace":"projects//traces/%v","logging.googleapis.com/spanId":"%v","logging.googleapis.com/trace_sampled":false,"wowow":"me\nyou"} `, slogstackdriverTestFile, s.SpanContext().TraceID, s.SpanContext().SpanID) - assert.Equal(t, exp, j, "entry") + assert.Equal(t, "entry", exp, j) } func TestSevMapping(t *testing.T) { t.Parallel() - assert.Equal(t, logpbtype.LogSeverity_DEBUG, slogstackdriver.Sev(slog.LevelDebug), "level") - assert.Equal(t, logpbtype.LogSeverity_INFO, slogstackdriver.Sev(slog.LevelInfo), "level") - assert.Equal(t, logpbtype.LogSeverity_WARNING, slogstackdriver.Sev(slog.LevelWarn), "level") - assert.Equal(t, logpbtype.LogSeverity_ERROR, slogstackdriver.Sev(slog.LevelError), "level") - assert.Equal(t, logpbtype.LogSeverity_CRITICAL, slogstackdriver.Sev(slog.LevelCritical), "level") + assert.Equal(t, "level", logpbtype.LogSeverity_DEBUG, slogstackdriver.Sev(slog.LevelDebug)) + assert.Equal(t, "level", logpbtype.LogSeverity_INFO, slogstackdriver.Sev(slog.LevelInfo)) + assert.Equal(t, "level", logpbtype.LogSeverity_WARNING, slogstackdriver.Sev(slog.LevelWarn)) + assert.Equal(t, "level", logpbtype.LogSeverity_ERROR, slogstackdriver.Sev(slog.LevelError)) + assert.Equal(t, "level", logpbtype.LogSeverity_CRITICAL, slogstackdriver.Sev(slog.LevelCritical)) } diff --git a/sloggers/slogtest/assert/assert.go b/sloggers/slogtest/assert/assert.go index d46b78b..7aafdd4 100644 --- a/sloggers/slogtest/assert/assert.go +++ b/sloggers/slogtest/assert/assert.go @@ -1,9 +1,16 @@ // Package assert is a helper package for test assertions. -package assert +// +// On failure, every assertion will fatal the test. +// +// The name parameter is available in each assertion for easier debugging. +package assert // import "cdr.dev/slog/sloggers/slogtest/assert" import ( + "strings" "testing" + "github.com/google/go-cmp/cmp" + "cdr.dev/slog" "cdr.dev/slog/internal/assert" "cdr.dev/slog/sloggers/slogtest" @@ -11,11 +18,14 @@ import ( // Equal asserts exp == act. // -// If they are not equal, it will fatal the test -// with a diff of the differences. -func Equal(t testing.TB, exp, act interface{}, name string) { +// If they are not equal, it will fatal the test with a diff of the +// two objects. +// +// Errors will be compared with errors.Is. +func Equal(t testing.TB, name string, exp, act interface{}, opts ...cmp.Option) { slog.Helper() - if diff := assert.CmpDiff(exp, act); diff != "" { + + if diff := assert.Diff(exp, act, opts...); diff != "" { slogtest.Fatal(t, "unexpected value", slog.F("name", name), slog.F("diff", diff), @@ -24,18 +34,54 @@ func Equal(t testing.TB, exp, act interface{}, name string) { } // Success asserts err == nil. -func Success(t testing.TB, err error, name string) { +func Success(t testing.TB, name string, err error) { slog.Helper() if err != nil { slogtest.Fatal(t, "unexpected error", slog.F("name", name), - slog.Error(err), + slog.Err(err), + ) + } +} + +// True asserts act == true. +func True(t testing.TB, name string, act bool) { + slog.Helper() + Equal(t, name, true, act) +} + +// Error asserts err != nil. +func Error(t testing.TB, name string, err error) { + slog.Helper() + if err == nil { + slogtest.Fatal(t, "expected error", + slog.F("name", name), ) } } -// True act == true. -func True(t testing.TB, act bool, name string) { +// ErrorContains asserts err != nil and err.Error() contains sub. +// +// The match will be case insensitive. +func ErrorContains(t testing.TB, name string, err error, sub string) { slog.Helper() - Equal(t, true, act, name) + + Error(t, name, err) + + errs := err.Error() + if !stringContainsFold(errs, sub) { + slogtest.Fatal(t, "unexpected error string", + slog.F("name", name), + slog.F("error_string", errs), + slog.F("expected_contains", sub), + ) + } +} + +func stringContainsFold(errs, sub string) bool { + errs = strings.ToLower(errs) + sub = strings.ToLower(sub) + + return strings.Contains(errs, sub) + } diff --git a/sloggers/slogtest/assert/assert_test.go b/sloggers/slogtest/assert/assert_test.go index 8b8255d..82d9a5d 100644 --- a/sloggers/slogtest/assert/assert_test.go +++ b/sloggers/slogtest/assert/assert_test.go @@ -1,6 +1,7 @@ package assert_test import ( + "fmt" "io" "testing" @@ -12,39 +13,79 @@ func TestEqual(t *testing.T) { t.Parallel() tb := &fakeTB{} - assert.Equal(tb, 3, 3, "meow") + assert.Equal(tb, "meow", 3, 3) defer func() { recover() - simpleassert.Equal(t, 1, tb.fatals, "fatals") + simpleassert.Equal(t, "fatals", 1, tb.fatals) }() - assert.Equal(tb, 3, 4, "meow") + assert.Equal(tb, "meow", 3, 4) } +func TestEqual_error(t *testing.T) { + t.Parallel() + + tb := &fakeTB{} + assert.Equal(tb, "meow", io.EOF, fmt.Errorf("failed: %w", io.EOF)) + + defer func() { + recover() + simpleassert.Equal(t, "fatals", 1, tb.fatals) + }() + assert.Equal(tb, "meow", io.ErrClosedPipe, fmt.Errorf("failed: %w", io.EOF)) +} + +func TestErrorContains(t *testing.T) { + t.Parallel() + + tb := &fakeTB{} + assert.ErrorContains(tb, "meow", io.EOF, "eof") + + defer func() { + recover() + simpleassert.Equal(t, "fatals", 1, tb.fatals) + + }() + assert.ErrorContains(tb, "meow", io.ErrClosedPipe, "eof") + +} func TestSuccess(t *testing.T) { t.Parallel() tb := &fakeTB{} - assert.Success(tb, nil, "meow") + assert.Success(tb, "meow", nil) defer func() { recover() - simpleassert.Equal(t, 1, tb.fatals, "fatals") + simpleassert.Equal(t, "fatals", 1, tb.fatals) }() - assert.Success(tb, io.EOF, "meow") + assert.Success(tb, "meow", io.EOF) } func TestTrue(t *testing.T) { t.Parallel() tb := &fakeTB{} - assert.True(tb, true, "meow") + assert.True(tb, "meow", true) + + defer func() { + recover() + simpleassert.Equal(t, "fatals", 1, tb.fatals) + }() + assert.True(tb, "meow", false) +} + +func TestError(t *testing.T) { + t.Parallel() + + tb := &fakeTB{} + assert.Error(tb, "meow", io.EOF) defer func() { recover() - simpleassert.Equal(t, 1, tb.fatals, "fatals") + simpleassert.Equal(t, "fatals", 1, tb.fatals) }() - assert.True(tb, false, "meow") + assert.Error(tb, "meow", nil) } type fakeTB struct { @@ -64,5 +105,5 @@ func (tb *fakeTB) Error(v ...interface{}) { func (tb *fakeTB) Fatal(v ...interface{}) { tb.fatals++ - panic("") + panic(fmt.Sprint(v...)) } diff --git a/sloggers/slogtest/t.go b/sloggers/slogtest/t.go index e315184..ffb972a 100644 --- a/sloggers/slogtest/t.go +++ b/sloggers/slogtest/t.go @@ -18,8 +18,8 @@ import ( // Ensure all stdlib logs go through slog. func init() { - l := sloghuman.Make(os.Stderr) - log.SetOutput(slog.Stdlib(context.Background(), l).Writer()) + ctx := sloghuman.Make(ctx, os.Stderr) + log.SetOutput(slog.Stdlib(ctx).Writer()) } // Options represents the options for the logger returned @@ -30,12 +30,12 @@ type Options struct { IgnoreErrors bool } -// Make creates a Logger that writes logs to tb in a human readable format. -func Make(tb testing.TB, opts *Options) slog.Logger { +// Make creates a logger that writes logs to tb in a human readable format. +func Make(tb testing.TB, opts *Options) slog.SinkContext { if opts == nil { opts = &Options{} } - return slog.Make(testSink{ + return slog.Make(context.Background(), testSink{ tb: tb, opts: opts, }) @@ -72,30 +72,30 @@ func (ts testSink) Sync() {} var ctx = context.Background() -func l(t testing.TB) slog.Logger { +func l(t testing.TB) context.Context { return Make(t, nil) } // Debug logs the given msg and fields to t via t.Log at the debug level. func Debug(t testing.TB, msg string, fields ...slog.Field) { slog.Helper() - l(t).Debug(ctx, msg, fields...) + slog.Debug(l(t), msg, fields...) } // Info logs the given msg and fields to t via t.Log at the info level. func Info(t testing.TB, msg string, fields ...slog.Field) { slog.Helper() - l(t).Info(ctx, msg, fields...) + slog.Info(l(t), msg, fields...) } // Error logs the given msg and fields to t via t.Error at the error level. func Error(t testing.TB, msg string, fields ...slog.Field) { slog.Helper() - l(t).Error(ctx, msg, fields...) + slog.Error(l(t), msg, fields...) } // Fatal logs the given msg and fields to t via t.Fatal at the fatal level. func Fatal(t testing.TB, msg string, fields ...slog.Field) { slog.Helper() - l(t).Fatal(ctx, msg, fields...) + slog.Fatal(l(t), msg, fields...) } diff --git a/sloggers/slogtest/t_test.go b/sloggers/slogtest/t_test.go index bd6c586..77942a5 100644 --- a/sloggers/slogtest/t_test.go +++ b/sloggers/slogtest/t_test.go @@ -17,11 +17,11 @@ func TestStateless(t *testing.T) { slogtest.Info(tb, "hello") slogtest.Error(tb, "hello") - assert.Equal(t, 1, tb.errors, "errors") + assert.Equal(t, "errors", 1, tb.errors) defer func() { recover() - assert.Equal(t, 1, tb.fatals, "fatals") + assert.Equal(t, "fatals", 1, tb.fatals) }() slogtest.Fatal(tb, "hello") @@ -31,23 +31,22 @@ func TestIgnoreErrors(t *testing.T) { t.Parallel() tb := &fakeTB{} - l := slog.Make(slogtest.Make(tb, &slogtest.Options{ + ctx := context.Background() + ctx = slog.Make(ctx, slogtest.Make(tb, &slogtest.Options{ IgnoreErrors: true, })) - l.Error(bg, "hello") - assert.Equal(t, 0, tb.errors, "errors") + slog.Error(ctx, "hello") + assert.Equal(t, "errors", 0, tb.errors) defer func() { recover() - assert.Equal(t, 0, tb.fatals, "fatals") + assert.Equal(t, "fatals", 0, tb.fatals) }() - l.Fatal(bg, "hello") + slog.Fatal(ctx, "hello") } -var bg = context.Background() - type fakeTB struct { testing.TB