diff --git a/example_test.go b/example_test.go index af7df0e25..c479a0aed 100644 --- a/example_test.go +++ b/example_test.go @@ -388,6 +388,51 @@ func ExampleObjects() { // {"level":"debug","msg":"opening connections","addrs":[{"ip":"123.45.67.89","port":4040},{"ip":"127.0.0.1","port":4041},{"ip":"192.168.0.1","port":4042}]} } +func ExampleDictObject() { + logger := zap.NewExample() + defer logger.Sync() + + // Use DictObject to create zapcore.ObjectMarshaler implementations from Field arrays, + // then use the Object and Objects field constructors to turn them back into a Field. + + logger.Debug("worker received job", + zap.Object("w1", + zap.DictObject( + zap.Int("id", 402000), + zap.String("description", "compress image data"), + zap.Int("priority", 3), + ), + )) + + d1 := 68 * time.Millisecond + d2 := 79 * time.Millisecond + d3 := 57 * time.Millisecond + + logger.Info("worker status checks", + zap.Objects("job batch enqueued", + []zapcore.ObjectMarshaler{ + zap.DictObject( + zap.String("worker", "w1"), + zap.Int("load", 419), + zap.Duration("latency", d1), + ), + zap.DictObject( + zap.String("worker", "w2"), + zap.Int("load", 520), + zap.Duration("latency", d2), + ), + zap.DictObject( + zap.String("worker", "w3"), + zap.Int("load", 310), + zap.Duration("latency", d3), + ), + }, + )) + // Output: + // {"level":"debug","msg":"worker received job","w1":{"id":402000,"description":"compress image data","priority":3}} + // {"level":"info","msg":"worker status checks","job batch enqueued":[{"worker":"w1","load":419,"latency":"68ms"},{"worker":"w2","load":520,"latency":"79ms"},{"worker":"w3","load":310,"latency":"57ms"}]} +} + func ExampleObjectValues() { logger := zap.NewExample() defer logger.Sync() diff --git a/field.go b/field.go index 6743930b8..8441d1afb 100644 --- a/field.go +++ b/field.go @@ -431,6 +431,13 @@ func (d dictObject) MarshalLogObject(enc zapcore.ObjectEncoder) error { return nil } +// DictObject constructs a [zapcore.ObjectMarshaler] with the given list of fields. +// The resulting object marshaler can be used as input to [Object], [Objects], or +// any other functions that expect an object marshaler. +func DictObject(val ...Field) zapcore.ObjectMarshaler { + return dictObject(val) +} + // We discovered an issue where zap.Any can cause a performance degradation // when used in new goroutines. // diff --git a/field_test.go b/field_test.go index f87f1592e..ee3d5fe5b 100644 --- a/field_test.go +++ b/field_test.go @@ -314,3 +314,45 @@ func TestDict(t *testing.T) { }) } } + +func TestDictObject(t *testing.T) { + tests := []struct { + desc string + field Field + expected any + }{ + { + "empty", + Object("", DictObject()), + map[string]any{}, + }, + { + "object", + Object("", DictObject(String("k", "v"))), + map[string]any{"k": "v"}, + }, + { + "objects", + Objects("", []zapcore.ObjectMarshaler{ + DictObject(String("k", "v")), + DictObject(String("k2", "v2")), + }), + []any{ + map[string]any{"k": "v"}, + map[string]any{"k2": "v2"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + enc := zapcore.NewMapObjectEncoder() + tt.field.Key = "k" + tt.field.AddTo(enc) + assert.Equal(t, tt.expected, enc.Fields["k"], "unexpected map contents") + assert.Len(t, enc.Fields, 1, "found extra keys in map: %v", enc.Fields) + + assertCanBeReused(t, tt.field) + }) + } +} diff --git a/zapcore/field_test.go b/zapcore/field_test.go index 06bcef2e1..2b6d8c369 100644 --- a/zapcore/field_test.go +++ b/zapcore/field_test.go @@ -320,6 +320,16 @@ func TestEquals(t *testing.T) { b: zap.Dict("k", zap.String("a", "d")), want: false, }, + { + a: zap.Object("k", zap.DictObject(zap.String("a", "b"))), + b: zap.Object("k", zap.DictObject(zap.String("a", "b"))), + want: true, + }, + { + a: zap.Object("k", zap.DictObject(zap.String("a", "b"))), + b: zap.Object("k", zap.DictObject(zap.String("a", "d"))), + want: false, + }, } for _, tt := range tests {