// Package encoder defines the Encoder interface used by lambdalog to turn
// log records into bytes. Implementations live in sibling packages such as
// internal/encoder/json.
//
// The top-level Encoder type here is a thin dispatch that picks between the
// registered implementations at logger-creation time. It is kept in a
// separate package so the JSON implementation can import helpers without
// pulling in the full Logger type.
//
// See mercemay.top/src/lambdalog/ for user-facing docs.
package encoder
import (
"errors"
"io"
"sync"
"time"
)
// Record is the subset of a Logger entry that encoders see. It is kept
// deliberately small because the Logger already handles sampling and level
// filtering before this point.
type Record struct {
Time time.Time
Level string
Message string
RequestID string
Fields []Field
}
// Field is a name-value pair. The value is kept as any to avoid an
// interface-per-primitive allocation; the JSON encoder type-switches on it.
type Field struct {
Key string
Value any
}
// Encoder writes a Record to an io.Writer. Implementations must be safe to
// call from multiple goroutines if the underlying writer is shared.
type Encoder interface {
Encode(w io.Writer, r Record) error
Name() string
}
// Registry tracks available encoder implementations keyed by name. The
// default registry is populated by init functions in sibling packages.
type Registry struct {
mu sync.RWMutex
items map[string]Encoder
}
// Default is the process-wide Registry consulted by New.
var Default = &Registry{items: map[string]Encoder{}}
// Register adds enc under its Name. It panics on duplicate registration so
// typos are caught at program start rather than at first log call.
func (r *Registry) Register(enc Encoder) {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.items[enc.Name()]; exists {
panic("encoder: duplicate registration: " + enc.Name())
}
r.items[enc.Name()] = enc
}
// Lookup returns the encoder with the given name, or an error if it is not
// registered.
func (r *Registry) Lookup(name string) (Encoder, error) {
r.mu.RLock()
defer r.mu.RUnlock()
enc, ok := r.items[name]
if !ok {
return nil, errors.New("encoder: not registered: " + name)
}
return enc, nil
}
// Names returns a stable list of registered encoder names, for diagnostics.
func (r *Registry) Names() []string {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]string, 0, len(r.items))
for k := range r.items {
out = append(out, k)
}
return out
}
// New returns the encoder registered under name from Default.
func New(name string) (Encoder, error) {
return Default.Lookup(name)
}
// NopEncoder drops every record. Useful in tests that care about call counts
// rather than byte output.
type NopEncoder struct{}
// Encode satisfies Encoder and returns nil without touching w.
func (NopEncoder) Encode(io.Writer, Record) error { return nil }
// Name satisfies Encoder.
func (NopEncoder) Name() string { return "nop" }
func init() {
Default.Register(NopEncoder{})
}