Skip to content

Added variadic StrsV, ObjectsV, and StringersV#752

Open
IDisposable wants to merge 3 commits intors:masterfrom
IDisposable:feature/variadic-stringers
Open

Added variadic StrsV, ObjectsV, and StringersV#752
IDisposable wants to merge 3 commits intors:masterfrom
IDisposable:feature/variadic-stringers

Conversation

@IDisposable
Copy link
Contributor

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

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
Copilot AI review requested due to automatic review settings January 29, 2026 01:15
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and StringersV to Event and Context as variadic wrappers over existing plural methods.
  • Added Go 1.18+ generic helpers AsLogObjectMarshalers and AsStringers to ease converting []T into []LogObjectMarshaler / []fmt.Stringer.
  • Updated unit tests, examples, benchmarks, and CI (added -race to 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 {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
`{"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 {

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@IDisposable IDisposable Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rs please let me know if this works and if so, just resolve this conversation. If not ... let's discuss other options.

@IDisposable
Copy link
Contributor Author

IDisposable commented Jan 29, 2026

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 *Event explicitly (not as a "this pointer") so the syntax in use looks hideous

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 Stringers it's even uglier because we have to inline the code for enc.AppendStringers() so nope...

// 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
}
  • The free-function versions could be the only place the emitting code exists, since they could be called from the *Event and Context methods

@IDisposable
Copy link
Contributor Author

@rs I'm sure you're really busy... is there anything I can do to help?

@rs
Copy link
Owner

rs commented Feb 21, 2026

We could just not expose those methods to older versions of go with no fallback implementation.

@IDisposable
Copy link
Contributor Author

Is there a minimum version of go you're targeting?

@rs
Copy link
Owner

rs commented Feb 21, 2026

Not really. We may set it to 1.18, seems reasonable

Copy link
Contributor Author

@IDisposable IDisposable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ready to merge @rs

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {.

Suggested change
// 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"}

Copilot uses AI. Check for mistakes.
Comment on lines +663 to +669
// 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)).
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +29
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
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// 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 {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
return c
}

// StringersV adds the field key with vals to the logger context where each
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s an extra double-space in the doc comment ("vals to"). Consider tightening it to a single space for readability.

Suggested change
// 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

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants