Added variadic StrsV, ObjectsV, and StringersV#752
Added variadic StrsV, ObjectsV, and StringersV#752IDisposable wants to merge 3 commits intors:masterfrom
Conversation
This allows you to just list all the string, object, or stringer that you want added to a Event or Context. The variadic versions are suffixed with V so as to not be a breaking change. If you have an existing array of objects that implement the LogObjectMarshaler or fmt.Stringer interfaces, unfortunately go doesn't consider those slices as identical types, so you have to allocate an array and copy the entries, there are generic helper methods in global_118.go zerolog.AsLogObjectMarshalers and zerolog.AsStringers if you're using go 1.18 or later. Somewhat addresses rs#551
There was a problem hiding this comment.
Pull request overview
Adds variadic convenience helpers for adding arrays of strings, stringers, and log objects to Event and Context without breaking existing slice-based APIs, plus Go 1.18+ helpers to convert typed slices into the required interface slices.
Changes:
- Added
ObjectsV,StrsV, andStringersVtoEventandContextas variadic wrappers over existing plural methods. - Added Go 1.18+ generic helpers
AsLogObjectMarshalersandAsStringersto ease converting[]Tinto[]LogObjectMarshaler/[]fmt.Stringer. - Updated unit tests, examples, benchmarks, and CI (added
-raceto CBOR test run).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
event.go |
Adds variadic *Event APIs and updates doc comments around plural helpers. |
context.go |
Adds variadic Context APIs mirroring Event behavior. |
globals_118.go |
Introduces Go 1.18+ generic slice conversion helpers for Objects/Stringers. |
log_test.go |
Expands plural-field tests to cover new variadic helpers and object arrays. |
log_example_test.go |
Adds examples demonstrating Objects(…), ObjectsV(…), and Stringers(…)/StringersV(…) usage. |
event_test.go |
Adds nil-event coverage for the new variadic methods. |
benchmark_test.go |
Extends benchmarks to include the new variadic methods. |
.github/workflows/test.yml |
Runs CBOR-tagged tests with -race. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
log_test.go
Outdated
| log.Log().Msg("") | ||
| if got, want := decodeIfBinaryToString(out.Bytes()), | ||
| `{"strings":["foo","bar"],"strings_nil":[],"stringers":["127.0.0.1",null],"stringers_nil":null,"errs":["some error","some other error",null,{"l":"OOPS"},"whoops"],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want { | ||
| `{"objs":[{"Pub":"a","Tag":"z","priv":1}],"objs_nil":[],"objs_v":[{"Pub":"A","Tag":"Z","priv":2}],"objs_v_empty":[],"strs":["foo","bar"],"strs_nil":[],"strs_v":["baz","fizz"],"strs_v_empty":[],"stringers":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_nil":null,"stringers_v":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_v_empty":null,"errs":["some error","some other error",null,{"l":"OOPS"},"whoops"],"errs_nil":[],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want { |
There was a problem hiding this comment.
StringersV("stringers_v_empty") is called with no variadic args, which passes an empty (non-nil) slice into Context.Stringers and will be encoded as an empty array ([]), not null. The expected JSON string should be updated accordingly (or, if you intend StringersV with no args to encode null, special-case len(vals)==0 in StringersV to forward nil).
| `{"objs":[{"Pub":"a","Tag":"z","priv":1}],"objs_nil":[],"objs_v":[{"Pub":"A","Tag":"Z","priv":2}],"objs_v_empty":[],"strs":["foo","bar"],"strs_nil":[],"strs_v":["baz","fizz"],"strs_v_empty":[],"stringers":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_nil":null,"stringers_v":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_v_empty":null,"errs":["some error","some other error",null,{"l":"OOPS"},"whoops"],"errs_nil":[],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want { | |
| `{"objs":[{"Pub":"a","Tag":"z","priv":1}],"objs_nil":[],"objs_v":[{"Pub":"A","Tag":"Z","priv":2}],"objs_v_empty":[],"strs":["foo","bar"],"strs_nil":[],"strs_v":["baz","fizz"],"strs_v_empty":[],"stringers":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_nil":null,"stringers_v":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_v_empty":[],"errs":["some error","some other error",null,{"l":"OOPS"},"whoops"],"errs_nil":[],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want { |
There was a problem hiding this comment.
@rs , this is interesting. When you call a variadic method without any of the varying parts (e.g. StringersV("foo") then go passes a nil slice, not an empty one! That causes the call to Stringers(key, vals) to pass a nil... which is special-cased to use enc.AppendInterface with a nil. I don't know why that special-case exists. enc.AppendStringers() should just emit the empty array.
Thus the existing test data is correct, but seems REALLY inconsistent with other places where an array might be used. I could change the behavior of context.go/Stringers (i.e. the one that was added in PR #735 and thus is not yet released) to encode an empty array instead and then both Stringers("foo", nil) and StringersV("foo") would result in "foo":[] which seems more appropriate.
I'm going to make that change in this PR (since again PR #735 hasn't been released)
There was a problem hiding this comment.
@rs please let me know if this works and if so, just resolve this conversation. If not ... let's discuss other options.
|
We could have a generic free function (non member method) doing something like this (conditionally compiled for go 1.18 or later) but it would have to duplicate the code of the existing methods* //go:build go1.18
// +build go1.18
package zerolog
import "fmt"
// ObjectsV adds the field key with objs as an array of objects that
// implement the LogObjectMarshaler interface to the event.
//
// This is a generic free function version that accepts a slice of
// LogObjectMarshaler-derived values.
func ObjectsE[T LogObjectMarshaler](e *Event, key string, objs []T) *Event {
if e == nil {
return e
}
e.buf = enc.AppendArrayStart(enc.AppendKey(e.buf, key))
for i, obj := range objs {
e.buf = appendObject(e.buf, obj, e.stack, e.ctx, e.ch)
if i < (len(objs) - 1) {
e.buf = enc.AppendArrayDelim(e.buf)
}
}
e.buf = enc.AppendArrayEnd(e.buf)
return e
}Unfortunately, that requires we pass the func ExampleObjects() {
// User implements zerolog.LogObjectMarshaler
u := User{"John", 35, time.Time{}}
u2 := User{"Bono", 54, time.Time{}}
users := []User{u, u2}
logger := zerolog.New(os.Stdout)
event := logger.Log()
zerolog.ObjectsE(event, "users", users).Msg("hello world")
// Output: {"users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"}
}For // StringersV adds the field key with vals to the *Event context.
// If any val is not nil, it is added by calling val.String().
// If val is nil, it is encoded as null without calling String().
//
// This is a generic free function version that accepts a listslice of
// fmt.Stringer values.
func StringersV(e *Event, key string, vals []fmt.Stringer) *Event {
if e == nil {
return e
}
e.buf = // insert loop from enc.AppendStringers ... ugh
// enc.AppendStringers(enc.AppendKey(e.buf, key), vals)
return e
}
|
|
@rs I'm sure you're really busy... is there anything I can do to help? |
|
We could just not expose those methods to older versions of go with no fallback implementation. |
|
Is there a minimum version of go you're targeting? |
|
Not really. We may set it to 1.18, seems reasonable |
IDisposable
left a comment
There was a problem hiding this comment.
I think this is ready to merge @rs
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ObjectsV("users", u, u2). | ||
| Msg("hello world") | ||
|
|
||
| // Output: {"users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"} |
There was a problem hiding this comment.
The ExampleEvent_ObjectsV expected output has an extra leading space after // Output: (// Output: { ... }). Go’s example output matcher treats that space as part of the expected output, so this example will fail. Remove the extra space so the expected output starts with {.
| // Output: {"users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"} | |
| // Output: {"users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"} |
| // net.IP values implement fmt.Stringer and can be used with StringersV | ||
| a := net.IP{127, 0, 0, 1} | ||
| b := net.IP{127, 0, 0, 2} | ||
| ips := []net.IP{a, b} | ||
|
|
||
| log.Log(). | ||
| Stringers("ips", zerolog.AsStringers(ips)). |
There was a problem hiding this comment.
This comment says net.IP values “can be used with StringersV”, but the example demonstrates Stringers + AsStringers(...). Please align the comment with what’s being demonstrated (or mention both APIs) to avoid confusing readers.
| func AsLogObjectMarshalers[T LogObjectMarshaler](objs []T) []LogObjectMarshaler { | ||
| if objs == nil { | ||
| return nil | ||
| } | ||
| s := make([]LogObjectMarshaler, len(objs)) | ||
| for i, v := range objs { | ||
| s[i] = v | ||
| } | ||
| return s | ||
| } | ||
|
|
||
| func AsStringers[T fmt.Stringer](objs []T) []fmt.Stringer { | ||
| if objs == nil { | ||
| return nil | ||
| } | ||
| s := make([]fmt.Stringer, len(objs)) | ||
| for i, v := range objs { | ||
| s[i] = v | ||
| } | ||
| return s |
There was a problem hiding this comment.
AsLogObjectMarshalers and AsStringers are exported, but they don’t have GoDoc comments. If this repo runs any linting (or for pkg.go.dev quality), add doc comments starting with the function name and describing the allocation/copy behavior and intended use case.
| // individual val is added by calling val.String(). | ||
| // | ||
| // This is the array version that accepts a slice of fmt.Stringer values. | ||
| func (c Context) Stringers(key string, vals []fmt.Stringer) Context { |
There was a problem hiding this comment.
Context.Stringers changed behavior for vals == nil: it now encodes an empty array ([]) instead of null (previously done via AppendInterface(..., nil)). If this is intentional for consistency with other plural helpers, it should be called out explicitly as a behavior change (it can affect downstream log consumers). If it’s not intended, restore the nil special-case.
| func (c Context) Stringers(key string, vals []fmt.Stringer) Context { | |
| func (c Context) Stringers(key string, vals []fmt.Stringer) Context { | |
| if vals == nil { | |
| c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), nil) | |
| return c | |
| } |
| return c | ||
| } | ||
|
|
||
| // StringersV adds the field key with vals to the logger context where each |
There was a problem hiding this comment.
There’s an extra double-space in the doc comment ("vals to"). Consider tightening it to a single space for readability.
| // StringersV adds the field key with vals to the logger context where each | |
| // StringersV adds the field key with vals to the logger context where each |
This allows you to just list all the string, object, or stringer that you want added to a Event or Context.
The variadic versions are suffixed with V so as to not be a breaking change.
If you have an existing array of objects that implement the LogObjectMarshaler or fmt.Stringer interfaces, unfortunately go doesn't consider those slices as identical types, so you have to allocate an array and copy the entries, there are generic helper methods in global_118.go zerolog.AsLogObjectMarshalers and zerolog.AsStringers if you're using go 1.18 or later.
Somewhat addresses #551