// slog-style attribute helpers. These produce values that Logger.With and
// Logger.WithFields accept; the helpers exist mainly to make call sites read
// better and to centralise the (relatively few) type coercions we perform.
package lambdalog
// Attr is a named value suitable for inclusion on a log record. It maps 1:1
// onto the internal attr type but is exported so callers can build slices of
// attributes ahead of time and pass them as a group.
type Attr struct {
Key string
Value any
}
// String builds an attribute with a string value.
func String(key, value string) Attr { return Attr{Key: key, Value: value} }
// Int builds an attribute with an int value. For callers working with int64
// directly use Int64 to avoid the widening copy.
func Int(key string, value int) Attr { return Attr{Key: key, Value: value} }
// Int64 builds an attribute with an int64 value.
func Int64(key string, value int64) Attr { return Attr{Key: key, Value: value} }
// Bool builds an attribute with a boolean value.
func Bool(key string, value bool) Attr { return Attr{Key: key, Value: value} }
// Err builds an attribute named "err" from the given error. If err is nil
// the Value is set to nil so downstream encoding emits JSON null.
func Err(err error) Attr {
if err == nil {
return Attr{Key: "err", Value: nil}
}
return Attr{Key: "err", Value: err.Error()}
}
// Group builds a nested attribute: the key maps to a flattened map whose
// entries are the nested attrs. Groups nest: Group("outer", Group("inner",
// String("k", "v"))) produces {"outer":{"inner":{"k":"v"}}}.
func Group(key string, attrs ...Attr) Attr {
return Attr{Key: key, Value: flattenGroup(attrs)}
}
func flattenGroup(attrs []Attr) map[string]any {
out := make(map[string]any, len(attrs))
for _, a := range attrs {
out[a.Key] = a.Value
}
return out
}
// Merge combines two attribute slices into a new one. The right-hand slice
// wins on key collisions. The returned slice is always a new allocation so
// callers can mutate either input afterwards.
func Merge(left, right []Attr) []Attr {
out := make([]Attr, 0, len(left)+len(right))
seen := make(map[string]int, len(left)+len(right))
for _, a := range left {
seen[a.Key] = len(out)
out = append(out, a)
}
for _, a := range right {
if idx, ok := seen[a.Key]; ok {
out[idx] = a
continue
}
seen[a.Key] = len(out)
out = append(out, a)
}
return out
}
// Copy returns a deep-enough copy of the attribute slice that the caller
// can mutate the result without aliasing the input. Nested groups are
// copied one level deep because that is the depth Group produces.
func Copy(attrs []Attr) []Attr {
out := make([]Attr, len(attrs))
for i, a := range attrs {
if g, ok := a.Value.(map[string]any); ok {
clone := make(map[string]any, len(g))
for k, v := range g {
clone[k] = v
}
out[i] = Attr{Key: a.Key, Value: clone}
continue
}
out[i] = a
}
return out
}
// WithAttrs returns a child logger carrying every attribute in attrs. The
// parent logger's attribute slice is copied, matching the semantics of With.
func (l *Logger) WithAttrs(attrs ...Attr) *Logger {
c := l.clone()
for _, a := range attrs {
c.attrs = append(c.attrs, attr{Key: a.Key, Value: a.Value})
}
return c
}
// asPairs flattens an Attr slice into the variadic ...any form expected by
// the Debug/Info/Warn/Error methods. Useful when callers build attributes
// programmatically and want to hand them to a single log call.
func asPairs(attrs []Attr) []any {
out := make([]any, 0, 2*len(attrs))
for _, a := range attrs {
out = append(out, a.Key, a.Value)
}
return out
}