Zerolog provides advanced types for logging complex data structures including arrays, nested objects, and custom types through marshaler interfaces.
import (
"net"
"time"
"github.com/rs/zerolog"
)The Array type allows you to build arrays of mixed types that can be added to log events. Arrays are pooled and reused for efficiency.
// Create new array
func Arr() *ArrayExample:
arr := zerolog.Arr().
Str("first").
Str("second").
Int(42)
logger.Info().
Array("items", arr).
Msg("array example")
// Output: "items":["first","second",42]All Array methods return *Array for chaining.
// Add string to array
func (a *Array) Str(val string) *Array
// Add bytes as string to array
func (a *Array) Bytes(val []byte) *Array
// Add bytes as hex string to array
func (a *Array) Hex(val []byte) *Array
// Add pre-encoded JSON to array
func (a *Array) RawJSON(val []byte) *ArrayExample:
arr := zerolog.Arr().
Str("hello").
Bytes([]byte("world")).
Hex([]byte{0x01, 0x02, 0x03})
logger.Info().Array("data", arr).Msg("mixed data")
// Output: "data":["hello","world","010203"]// Add boolean to array
func (a *Array) Bool(b bool) *ArrayExample:
arr := zerolog.Arr().
Bool(true).
Bool(false).
Bool(true)
logger.Info().Array("flags", arr).Msg("boolean array")
// Output: "flags":[true,false,true]// Add integers to array
func (a *Array) Int(i int) *Array
func (a *Array) Int8(i int8) *Array
func (a *Array) Int16(i int16) *Array
func (a *Array) Int32(i int32) *Array
func (a *Array) Int64(i int64) *Array
// Add unsigned integers to array
func (a *Array) Uint(i uint) *Array
func (a *Array) Uint8(i uint8) *Array
func (a *Array) Uint16(i uint16) *Array
func (a *Array) Uint32(i uint32) *Array
func (a *Array) Uint64(i uint64) *ArrayExample:
arr := zerolog.Arr().
Int(1).
Int(2).
Int(3).
Int64(1234567890)
logger.Info().Array("numbers", arr).Msg("integer array")
// Output: "numbers":[1,2,3,1234567890]// Add floats to array
func (a *Array) Float32(f float32) *Array
func (a *Array) Float64(f float64) *ArrayExample:
arr := zerolog.Arr().
Float64(1.5).
Float64(2.7).
Float32(3.14)
logger.Info().Array("values", arr).Msg("float array")
// Output: "values":[1.5,2.7,3.14]// Add time to array
func (a *Array) Time(t time.Time) *Array
// Add duration to array
func (a *Array) Dur(d time.Duration) *ArrayExample:
now := time.Now()
later := now.Add(1 * time.Hour)
arr := zerolog.Arr().
Time(now).
Time(later).
Dur(1 * time.Hour)
logger.Info().Array("times", arr).Msg("time array")// Add IP address to array
func (a *Array) IPAddr(ip net.IP) *Array
// Add IP prefix/CIDR to array
func (a *Array) IPPrefix(pfx net.IPNet) *Array
// Add MAC address to array
func (a *Array) MACAddr(ha net.HardwareAddr) *ArrayExample:
arr := zerolog.Arr().
IPAddr(net.ParseIP("192.168.1.1")).
IPAddr(net.ParseIP("192.168.1.2"))
logger.Info().Array("ips", arr).Msg("ip array")
// Output: "ips":["192.168.1.1","192.168.1.2"]// Add error to array
func (a *Array) Err(err error) *ArrayExample:
err1 := errors.New("error 1")
err2 := errors.New("error 2")
arr := zerolog.Arr().
Err(err1).
Err(err2)
logger.Info().Array("errors", arr).Msg("error array")
// Output: "errors":["error 1","error 2"]// Add object to array using LogObjectMarshaler
func (a *Array) Object(obj LogObjectMarshaler) *Array
// Add nested dictionary to array
func (a *Array) Dict(dict *Event) *Array
// Add interface to array using reflection
func (a *Array) Interface(i interface{}) *ArrayExample:
dict1 := zerolog.Dict().Str("name", "alice").Int("age", 30)
dict2 := zerolog.Dict().Str("name", "bob").Int("age", 25)
arr := zerolog.Arr().
Dict(dict1).
Dict(dict2)
logger.Info().Array("users", arr).Msg("nested objects")
// Output: "users":[{"name":"alice","age":30},{"name":"bob","age":25}]// Interface for types that can marshal themselves as arrays
type LogArrayMarshaler interface {
MarshalZerologArray(a *Array)
}Implementation example:
type Coordinates []float64
func (c Coordinates) MarshalZerologArray(a *zerolog.Array) {
for _, coord := range c {
a.Float64(coord)
}
}
// Usage
coords := Coordinates{37.7749, -122.4194}
logger.Info().Array("location", coords).Msg("coordinates")
// Output: "location":[37.7749,-122.4194]Example with custom type:
type UserList []User
func (ul UserList) MarshalZerologArray(a *zerolog.Array) {
for _, user := range ul {
a.Object(user)
}
}
type User struct {
Name string
Age int
}
func (u User) MarshalZerologObject(e *zerolog.Event) {
e.Str("name", u.Name).Int("age", u.Age)
}
// Usage
users := UserList{
User{Name: "alice", Age: 30},
User{Name: "bob", Age: 25},
}
logger.Info().Array("users", users).Msg("user list")
// Output: "users":[{"name":"alice","age":30},{"name":"bob","age":25}]Create nested dictionaries (objects) within log events.
// Create dictionary event for nested objects
func Dict() *EventExample:
dict := zerolog.Dict().
Str("name", "alice").
Int("age", 30).
Bool("active", true)
logger.Info().
Dict("user", dict).
Msg("user info")
// Output: "user":{"name":"alice","age":30,"active":true}Nested dictionaries:
address := zerolog.Dict().
Str("street", "123 Main St").
Str("city", "Springfield")
user := zerolog.Dict().
Str("name", "alice").
Dict("address", address)
logger.Info().
Dict("user", user).
Msg("user with address")
// Output: "user":{"name":"alice","address":{"street":"123 Main St","city":"Springfield"}}Dict in arrays:
arr := zerolog.Arr().
Dict(zerolog.Dict().Str("type", "info").Int("code", 100)).
Dict(zerolog.Dict().Str("type", "warning").Int("code", 200))
logger.Info().Array("messages", arr).Msg("message array")
// Output: "messages":[{"type":"info","code":100},{"type":"warning","code":200}]The LogObjectMarshaler interface allows custom types to control how they're logged as objects.
// Interface for types that can marshal themselves as objects
type LogObjectMarshaler interface {
MarshalZerologObject(e *Event)
}type User struct {
ID int
Username string
Email string
Active bool
}
func (u User) MarshalZerologObject(e *zerolog.Event) {
e.Int("id", u.ID).
Str("username", u.Username).
Str("email", u.Email).
Bool("active", u.Active)
}
// Usage
user := User{
ID: 123,
Username: "alice",
Email: "alice@example.com",
Active: true,
}
logger.Info().
Object("user", user).
Msg("user logged in")
// Output: "user":{"id":123,"username":"alice","email":"alice@example.com","active":true}// Add object field using LogObjectMarshaler
func (e *Event) Object(key string, obj LogObjectMarshaler) *Event
func (c Context) Object(key string, obj LogObjectMarshaler) ContextExample:
type Server struct {
Host string
Port int
TLS bool
}
func (s Server) MarshalZerologObject(e *zerolog.Event) {
e.Str("host", s.Host).Int("port", s.Port).Bool("tls", s.TLS)
}
server := Server{Host: "localhost", Port: 8080, TLS: true}
logger.Info().Object("server", server).Msg("server config")
// Output: "server":{"host":"localhost","port":8080,"tls":true}// Embed object fields at top level (no key)
func (e *Event) EmbedObject(obj LogObjectMarshaler) *Event
func (c Context) EmbedObject(obj LogObjectMarshaler) ContextExample:
type RequestInfo struct {
Method string
Path string
Status int
}
func (r RequestInfo) MarshalZerologObject(e *zerolog.Event) {
e.Str("method", r.Method).
Str("path", r.Path).
Int("status", r.Status)
}
req := RequestInfo{Method: "GET", Path: "/api/users", Status: 200}
logger.Info().
EmbedObject(req).
Msg("request completed")
// Output: "method":"GET","path":"/api/users","status":200,"message":"request completed"
// Note: No "request" key - fields are embedded at top leveltype Address struct {
Street string
City string
Zip string
}
func (a Address) MarshalZerologObject(e *zerolog.Event) {
e.Str("street", a.Street).
Str("city", a.City).
Str("zip", a.Zip)
}
type Person struct {
Name string
Age int
Address Address
}
func (p Person) MarshalZerologObject(e *zerolog.Event) {
e.Str("name", p.Name).
Int("age", p.Age).
Object("address", p.Address)
}
person := Person{
Name: "alice",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Springfield",
Zip: "12345",
},
}
logger.Info().Object("person", person).Msg("person info")
// Output: "person":{"name":"alice","age":30,"address":{"street":"123 Main St","city":"Springfield","zip":"12345"}}type Metadata struct {
Tags []string
Priority int
Flags map[string]bool
}
func (m Metadata) MarshalZerologObject(e *zerolog.Event) {
e.Strs("tags", m.Tags).
Int("priority", m.Priority).
Interface("flags", m.Flags)
}
metadata := Metadata{
Tags: []string{"api", "production"},
Priority: 1,
Flags: map[string]bool{"debug": false, "trace": true},
}
logger.Info().Object("metadata", metadata).Msg("with metadata")type Config struct {
Name string
Timeout *time.Duration
MaxRetry *int
}
func (c Config) MarshalZerologObject(e *zerolog.Event) {
e.Str("name", c.Name)
if c.Timeout != nil {
e.Dur("timeout", *c.Timeout)
}
if c.MaxRetry != nil {
e.Int("max_retry", *c.MaxRetry)
}
}
// Only includes non-nil fields
timeout := 5 * time.Second
config := Config{
Name: "api",
Timeout: &timeout,
// MaxRetry is nil, won't be included
}
logger.Info().Object("config", config).Msg("configuration")
// Output: "config":{"name":"api","timeout":5000}type LogEntry struct {
Timestamp time.Time
Level string
Message string
}
func (l LogEntry) MarshalZerologObject(e *zerolog.Event) {
e.Time("timestamp", l.Timestamp).
Str("level", l.Level).
Str("message", l.Message)
}
type LogBatch []LogEntry
func (lb LogBatch) MarshalZerologArray(a *zerolog.Array) {
for _, entry := range lb {
a.Object(entry)
}
}
batch := LogBatch{
{Timestamp: time.Now(), Level: "info", Message: "msg1"},
{Timestamp: time.Now(), Level: "warn", Message: "msg2"},
}
logger.Info().Array("batch", batch).Msg("log batch")type ServiceInfo struct {
Name string
Version string
Region string
}
func (s ServiceInfo) MarshalZerologObject(e *zerolog.Event) {
e.Str("service", s.Name).
Str("version", s.Version).
Str("region", s.Region)
}
serviceInfo := ServiceInfo{
Name: "api",
Version: "1.0.0",
Region: "us-west-1",
}
// Add to context for all logs
logger := logger.With().
EmbedObject(serviceInfo).
Logger()
logger.Info().Msg("started")
// Output includes: "service":"api","version":"1.0.0","region":"us-west-1"Define MarshalZerologObject for types you log frequently:
// Good - consistent representation
type User struct {
ID int
Name string
}
func (u User) MarshalZerologObject(e *zerolog.Event) {
e.Int("id", u.ID).Str("name", u.Name)
}
logger.Info().Object("user", user).Msg("user action")
// Avoid - inconsistent manual logging
logger.Info().Int("user_id", user.ID).Str("user_name", user.Name).Msg("user action")Use EmbedObject for fields that should appear at the top level:
// Good - common fields embedded
logger := logger.With().EmbedObject(requestInfo).Logger()
logger.Info().Msg("request")
// Output: "method":"GET","path":"/api","message":"request"
// Less readable - nested
logger := logger.With().Object("request", requestInfo).Logger()
logger.Info().Msg("request")
// Output: "request":{"method":"GET","path":"/api"},"message":"request"Don't perform expensive operations in marshalers:
// Good
func (u User) MarshalZerologObject(e *zerolog.Event) {
e.Int("id", u.ID).Str("name", u.Name)
}
// Avoid - expensive operation
func (u User) MarshalZerologObject(e *zerolog.Event) {
permissions := u.LoadPermissions() // Database call - BAD
e.Int("id", u.ID).Interface("permissions", permissions)
}Use typed slices with marshaler interface for homogeneous arrays:
// Good - clean and efficient
type IDs []int64
func (ids IDs) MarshalZerologArray(a *zerolog.Array) {
for _, id := range ids {
a.Int64(id)
}
}
logger.Info().Array("ids", IDs{1, 2, 3}).Msg("ids")
// Avoid - verbose
arr := zerolog.Arr().Int64(1).Int64(2).Int64(3)
logger.Info().Array("ids", arr).Msg("ids")Don't create unnecessarily complex structures:
// Good - simple when possible
logger.Info().
Str("user", "alice").
Int("age", 30).
Msg("user info")
// Avoid - unnecessary nesting
dict := zerolog.Dict().Str("user", "alice").Int("age", 30)
logger.Info().Dict("info", dict).Msg("user info")