Skip to main content
Velo’s typed fields API eliminates the interface boxing overhead of loosely-typed key-value pairs, enabling true zero-allocation logging on the hot path.

Why typed fields?

Standard logging with loosely-typed arguments boxes values into interface{}, causing heap allocations:
// This causes allocations due to interface boxing
logger.Info("request completed", "duration", duration, "status", 200)
Typed fields avoid this overhead by storing values in a stack-allocated struct:
// Zero allocations on the hot path
logger.InfoFields("request completed",
  velo.Duration("duration", duration),
  velo.Int("status", 200),
)
Benchmarks show typed fields can be 22% faster than standard logging and achieve true zero allocations when logging with context fields.

Available field types

Velo provides constructors for all common types:

String fields

velo.String("url", "https://api.example.com")
velo.String("method", "GET")

Numeric fields

velo.Int("status", 200)
velo.Int64("bytes", int64(1024))

Boolean fields

velo.Bool("success", true)
velo.Bool("cached", false)

Time fields

velo.Time("timestamp", time.Now())
velo.Duration("latency", 150*time.Millisecond)

Error fields

velo.Err(err) // Automatically uses "error" as the key

Array fields

velo.Ints("response_codes", []int{200, 201, 204})
velo.Strings("tags", []string{"api", "production"})
velo.Times("checkpoints", []time.Time{t1, t2, t3})

Generic fields

velo.Any("data", complexStruct) // Falls back to interface{}
The Any() field type incurs allocation overhead due to interface boxing. Use strongly-typed constructors whenever possible.

Using typed fields

Every standard log method has a typed equivalent with the Fields suffix:
logger.InfoFields("user login",
  velo.String("username", username),
  velo.String("ip", clientIP),
  velo.Time("timestamp", time.Now()),
)

Advanced field types

For complex objects, implement ObjectMarshaler to control serialization:
type User struct {
  ID   int
  Name string
}

func (u User) MarshalLogObject(enc velo.ObjectEncoder) error {
  enc.AddInt("id", u.ID)
  enc.AddString("name", u.Name)
  return nil
}

// Use with Object field
logger.InfoFields("user created",
  velo.Object("user", user),
)
For arrays, implement ArrayMarshaler:
type Users []User

func (u Users) MarshalLogArray(enc velo.ArrayEncoder) error {
  for _, user := range u {
    enc.AppendObject(user)
  }
  return nil
}

// Use with Array field
logger.InfoFields("batch created",
  velo.Array("users", users),
)

Performance comparison

Real benchmarks from the Velo test suite:
OperationTyped FieldsStandardImprovement
Log 10 fields513 ns/op591 ns/op22% faster
Log with context55 ns/op57 ns/op4% faster
Allocations (10 fields)1 alloc/op6 allocs/op83% fewer
Allocations (context)0 allocs/op0 allocs/opZero allocs
For maximum performance in high-throughput applications, always prefer typed fields over loosely-typed key-value pairs.

Mixing field types

You can freely mix typed and loosely-typed fields, though this negates some performance benefits:
// Works but not optimal
logger.Info("mixed example", "key1", value1, "key2", value2)

// Better: use LogFields or typed fields consistently
logger.LogFields(velo.InfoLevel, "typed example",
  velo.String("key1", value1),
  velo.String("key2", value2),
)

Using LogFields directly

For maximum flexibility, use the LogFields method:
logger.LogFields(velo.InfoLevel, "failed to fetch URL",
  velo.String("url", url),
  velo.Int("attempt", 3),
  velo.Duration("backoff", time.Second),
)
This is equivalent to calling InfoFields, but allows you to specify the level dynamically.

Next steps

Context logging

Add context-aware fields to your logs

Performance

Optimize logging for high-throughput applications

Build docs developers (and LLMs) love