internal/encoder/encoder.go

// 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{})
}