Documentation
¶
Overview ¶
Package logger is a pluggable, adapter-based, slog-native structured logging core: a correct slog.Handler, a single processor pipeline as the only extension seam, swappable async transports with explicit backpressure, and batteries (rotation, redaction, sampling, OTEL correlation) built in.
See MISSION.md / FEATURES.md in the plan repo for the design rationale.
Example ¶
Basic structured logging with typed, zero-allocation fields.
package main
import (
"os"
logger "github.com/ubgo/logger"
)
func main() {
log := logger.New(
logger.WithLevel(logger.LevelInfo),
logger.WithSink(logger.NewWriterSink(os.Stdout, logger.NewJSONEncoder(), logger.LevelInfo)),
)
defer log.Close()
log.Info("server started",
logger.String("addr", ":8080"),
logger.Int("workers", 8),
)
}
Output:
Index ¶
- Constants
- Variables
- func ContextWith(ctx context.Context, fields ...Field) context.Context
- func CycleLevelOnSignal(lv *LevelVar, sig os.Signal, normal, verbose Level) (stop func())
- func FCScope(ctx context.Context) context.Context
- func OnSIGHUP(fn func()) (stop func())
- func RedirectStdLog(l *Logger, level Level) func()
- type AuditResult
- type AuditSink
- type ChannelTransport
- type ConfigWatcher
- type ConsoleEncoder
- type DedupProcessor
- type DisruptorTransport
- type Encoder
- type EnrichProcessor
- type Fanout
- type Field
- func Any(key string, v any) Field
- func Bool(key string, v bool) Field
- func Dur(key string, d time.Duration) Field
- func Err(err error) Field
- func Float[T float](key string, v T) Field
- func Int[T integer](key string, v T) Field
- func NamedErr(key string, err error) Field
- func String(key, val string) Field
- func Time(key string, t time.Time) Field
- type FileConfig
- type FingersCrossed
- type HTTPBatchSink
- func NewDatadogSink(intakeURL, apiKey string, minLevel Level) *HTTPBatchSink
- func NewElasticsearchSink(bulkURL, index string, minLevel Level) *HTTPBatchSink
- func NewHTTPBatchSink(url string, minLevel Level, build func([]*Record) ([]byte, string)) *HTTPBatchSink
- func NewLokiSink(pushURL string, labels map[string]string, minLevel Level) *HTTPBatchSink
- type JSONEncoder
- type Level
- type LevelHandler
- type LevelVar
- type Leveler
- type LogfmtEncoder
- type Logger
- func (l *Logger) Close() error
- func (l *Logger) Debug(msg string, f ...Field)
- func (l *Logger) DebugContext(ctx context.Context, msg string, f ...Field)
- func (l *Logger) Debugt(tmpl string, args ...any)
- func (l *Logger) Enabled(level Level) bool
- func (l *Logger) Error(msg string, f ...Field)
- func (l *Logger) ErrorContext(ctx context.Context, msg string, f ...Field)
- func (l *Logger) Errort(tmpl string, args ...any)
- func (l *Logger) Event(name string, f ...Field)
- func (l *Logger) EventAt(ctx context.Context, level Level, name string, f ...Field)
- func (l *Logger) Go(ctx context.Context, fn func())
- func (l *Logger) Handler() slog.Handler
- func (l *Logger) Info(msg string, f ...Field)
- func (l *Logger) InfoContext(ctx context.Context, msg string, f ...Field)
- func (l *Logger) Infot(tmpl string, args ...any)
- func (l *Logger) Log(ctx context.Context, level Level, msg string, f ...Field)
- func (l *Logger) Logt(ctx context.Context, level Level, tmpl string, args ...any)
- func (l *Logger) Metrics() *Metrics
- func (l *Logger) NewSlog() *slog.Logger
- func (l *Logger) Recover(ctx context.Context)
- func (l *Logger) RecoverAndContinue(ctx context.Context)
- func (l *Logger) StartSpan(ctx context.Context, name string, fields ...Field) (context.Context, *Span)
- func (l *Logger) StdLogWriter(level Level) *stdWriter
- func (l *Logger) StdLogger(level Level) *log.Logger
- func (l *Logger) Sync() error
- func (l *Logger) Trace(msg string, f ...Field)
- func (l *Logger) TraceContext(ctx context.Context, msg string, f ...Field)
- func (l *Logger) Warn(msg string, f ...Field)
- func (l *Logger) WarnContext(ctx context.Context, msg string, f ...Field)
- func (l *Logger) Warnt(tmpl string, args ...any)
- func (l *Logger) With(fields ...Field) *Logger
- type Metrics
- type NetSink
- type Option
- type OverflowPolicy
- type PathRedactor
- type Processor
- type ProcessorFunc
- type Record
- type RedactProcessor
- type RedactStrategy
- type RingTransport
- type RotatingFile
- type SampleProcessor
- type Sink
- type Snapshot
- type Source
- type Span
- type SyncTransport
- type SyslogSink
- type TraceExtractor
- type Transport
- type WriterSink
Examples ¶
Constants ¶
const GenesisHash = "0000000000000000000000000000000000000000000000000000000000000000"
GenesisHash is the chain anchor for the first record.
Variables ¶
var ErrDrop = errors.New("logger: record dropped")
ErrDrop is the sentinel a Processor returns to drop the record silently (this is how sampling/rate-limiting is expressed — one concept, not a separate subsystem). Any other non-nil error is a processing failure.
Functions ¶
func ContextWith ¶
ContextWith returns a context carrying fields that EnrichProcessor will merge into every record logged with that context — the idiomatic-Go replacement for thread-local MDC. Repeated calls accumulate.
func CycleLevelOnSignal ¶
CycleLevelOnSignal toggles lv between verbose and normal on each delivery of sig (e.g. syscall.SIGUSR2): first hit → verbose, next → back to normal. Lets you flip a stuck prod process to debug without a redeploy or an HTTP endpoint. Returns a stop func.
func FCScope ¶
FCScope returns a context carrying a fresh per-scope FingersCrossed buffer. Call it at the start of a request; all logs made with this ctx share the buffer and flush together on the first error.
func OnSIGHUP ¶
func OnSIGHUP(fn func()) (stop func())
OnSIGHUP runs fn on every SIGHUP — wire it to RotatingFile.Reopen for logrotate compatibility. Returns a stop func.
stop := logger.OnSIGHUP(func() { _ = rf.Reopen() })
defer stop()
func RedirectStdLog ¶
RedirectStdLog routes the global stdlib logger (log.Default) through this logger and returns a function restoring the previous output (handy in tests — the global-logger boundary, made explicit and reversible).
Types ¶
type AuditResult ¶
type AuditResult struct {
Records uint64
OK bool
// BrokenAtSeq is the sequence number where the chain first failed
// (-1 if OK).
BrokenAtSeq int64
Reason string
}
AuditResult reports the outcome of chain verification.
func VerifyAudit ¶
func VerifyAudit(rd io.Reader) AuditResult
VerifyAudit re-walks a hash-chained audit stream and proves it is intact: every line's hash must equal sha256(prev || canonical), each line's prev must equal the previous line's hash, and seq must be contiguous from 0.
type AuditSink ¶
type AuditSink struct {
// contains filtered or unexported fields
}
AuditSink is a tamper-evident sink: every record is hash-chained (hash = sha256(prevHash || canonicalRecordBytes)) so any insertion, deletion, reordering or edit of a past entry breaks the chain and is detectable by VerifyAudit. For security/audit logs that must be provably intact.
Line format (greppable, verifier-friendly):
<hash-hex> <prev-hex> <seq> <canonical-json>\n
The canonical JSON is taken verbatim from the line during verification, so the hash covers the exact emitted bytes.
func NewAuditSink ¶
NewAuditSink wraps w. The encoder must be deterministic (the built-in JSONEncoder is — fields are emitted in slice order).
type ChannelTransport ¶
type ChannelTransport struct {
// contains filtered or unexported fields
}
ChannelTransport: bounded buffered channel feeding one worker goroutine. The pragmatic default async engine — correct, simple, good for ~99%.
func NewChannelTransport ¶
func NewChannelTransport(s Sink, capacity int, policy OverflowPolicy) *ChannelTransport
NewChannelTransport starts a worker draining a queue of the given capacity.
func (*ChannelTransport) Close ¶
func (t *ChannelTransport) Close() error
Close drains the queue, stops the worker, and closes the sink.
func (*ChannelTransport) Dispatch ¶
func (t *ChannelTransport) Dispatch(r *Record)
Dispatch implements Transport.
func (*ChannelTransport) Dropped ¶
func (t *ChannelTransport) Dropped() uint64
func (*ChannelTransport) Sync ¶
func (t *ChannelTransport) Sync() error
type ConfigWatcher ¶
type ConfigWatcher struct {
// contains filtered or unexported fields
}
ConfigWatcher polls a JSON config file and applies changes to a LevelVar without a restart — no fsnotify dependency (mtime poll keeps the core zero-dep). Changes are applied only when the file's mtime changes.
func WatchConfigFile ¶
func WatchConfigFile(path string, lv *LevelVar, interval time.Duration) (*ConfigWatcher, func())
WatchConfigFile starts a goroutine that re-reads path every interval (min 1s) and applies cfg.Level to lv. Returns a stop func. A missing or invalid file is ignored (keeps the last good level) rather than crashing — graceful degradation, not a silent zero.
func (*ConfigWatcher) OnReload ¶
func (w *ConfigWatcher) OnReload(fn func(FileConfig))
OnReload registers a callback invoked with the parsed config on every applied change (for app-specific settings beyond level).
type ConsoleEncoder ¶
ConsoleEncoder produces human-readable, optionally colored output for dev terminals. Color is opt-in by the sink (TTY-aware), never forced here.
func NewConsoleEncoder ¶
func NewConsoleEncoder() ConsoleEncoder
NewConsoleEncoder returns a ConsoleEncoder with sensible defaults.
func (ConsoleEncoder) Encode ¶
func (e ConsoleEncoder) Encode(buf *buffer, r *Record)
Encode implements Encoder.
type DedupProcessor ¶
type DedupProcessor struct {
Window time.Duration // default 5s
// contains filtered or unexported fields
}
DedupProcessor throttles identical (level + message) records: the first in a window passes (annotated with how many were suppressed since the last pass), the rest are dropped. Stops one hot error from burying every other line, without silently losing the fact that it happened.
func NewDedupProcessor ¶
func NewDedupProcessor(window time.Duration) *DedupProcessor
NewDedupProcessor builds a deduper with the given window.
type DisruptorTransport ¶
type DisruptorTransport struct {
// contains filtered or unexported fields
}
DisruptorTransport is the lock-free async engine: a Vyukov bounded MPMC queue (the LMAX-Disruptor-class algorithm — per-slot sequence numbers, CAS only, no mutex) drained by one consumer goroutine. Use it over ChannelTransport when many goroutines log hot and channel-send overhead shows up in profiles.
The Vyukov algorithm is well-known and proven; it is implemented verbatim here (not improvised) and exercised under -race with 8+ concurrent producers asserting zero loss under Block.
func NewDisruptorTransport ¶
func NewDisruptorTransport(s Sink, capacity int, policy OverflowPolicy) *DisruptorTransport
NewDisruptorTransport starts a consumer draining a lock-free ring of the given capacity (rounded up to a power of two).
func (*DisruptorTransport) Close ¶
func (t *DisruptorTransport) Close() error
Close stops accepting, drains the ring, stops the consumer, closes the sink.
func (*DisruptorTransport) Dispatch ¶
func (t *DisruptorTransport) Dispatch(r *Record)
Dispatch implements Transport.
func (*DisruptorTransport) Dropped ¶
func (t *DisruptorTransport) Dropped() uint64
func (*DisruptorTransport) Sync ¶
func (t *DisruptorTransport) Sync() error
type Encoder ¶
type Encoder interface {
// Encode writes one fully-formed log line (including trailing newline)
// into buf.
Encode(buf *buffer, r *Record)
// Name identifies the encoder for diagnostics/config.
Name() string
}
Encoder serializes a Record into a buffer. It must not retain the Record. Encoders are values (cheap to copy) and safe for concurrent use.
type EnrichProcessor ¶
type EnrichProcessor struct {
Extractors []TraceExtractor
TraceKey string // default "trace_id"
SpanKey string // default "span_id"
}
EnrichProcessor injects ctx-bound fields + trace correlation into every record. Place it early in the pipeline so redaction/sampling see the enriched record.
func NewEnrichProcessor ¶
func NewEnrichProcessor(ex ...TraceExtractor) *EnrichProcessor
NewEnrichProcessor builds an enricher with optional trace extractors.
type Fanout ¶
Fanout broadcasts a record to N sinks with per-sink failure isolation: one sink returning an error never prevents the others from receiving the record.
type Field ¶
type Field struct {
Key string
// contains filtered or unexported fields
}
Field is one structured key/value pair. Scalars are stored unboxed (num/str) so the typed API allocates nothing; only kindAny escapes to interface{}.
func Any ¶
Any is the escape hatch: arbitrary value via reflection at encode time. Prefer a typed constructor on hot paths.
func Err ¶
Err adds an error under the conventional "error" key. nil is preserved so the encoder can emit an explicit null rather than dropping the field.
type FileConfig ¶
type FileConfig struct {
Level string `json:"level"` // band name or OTEL SeverityNumber 1..24
}
FileConfig is the hot-reloadable on-disk config. Kept intentionally tiny — the level is the thing operators actually flip in production. Extend via OnReload for app-specific knobs.
type FingersCrossed ¶
type FingersCrossed struct {
Activation Level // default LevelError
BufferSize int // per-scope ring capacity, default 256
// PassThrough emits records at/above this level even when not activated
// (so WARN-and-below stay buffered but you still see, e.g., nothing until
// error). Default 0 = nothing passes until activation.
PassThrough Level
// contains filtered or unexported fields
}
FingersCrossed is a Sink decorator implementing debug-on-error buffering (Monolog's signature, near-absent in Go): records below ActivationLevel are held in a bounded ring and emitted ONLY if a record at/above ActivationLevel occurs in the same scope — then the whole buffered trail is flushed for full failure context. Successful scopes pay ~nothing and emit nothing below the activation level.
Scope it per request with FCScope(ctx); without a scope it falls back to a single process-global ring (still correct, less precise).
Example ¶
FingersCrossed: a successful request emits nothing below the activation level; the first error flushes the whole buffered debug trail.
package main
import (
"context"
"os"
logger "github.com/ubgo/logger"
)
func main() {
fc := logger.NewFingersCrossed(
logger.NewWriterSink(os.Stdout, logger.NewJSONEncoder(), logger.LevelTrace),
)
log := logger.New(logger.WithTransport(logger.NewSyncTransport(fc)), logger.WithLevel(logger.LevelTrace))
ctx := logger.FCScope(context.Background())
log.DebugContext(ctx, "step 1") // buffered
log.DebugContext(ctx, "step 2") // buffered
log.ErrorContext(ctx, "failed") // flushes step 1 + step 2 + this
}
Output:
func NewFingersCrossed ¶
func NewFingersCrossed(inner Sink) *FingersCrossed
NewFingersCrossed wraps inner with debug-on-error buffering.
func (*FingersCrossed) Close ¶
func (f *FingersCrossed) Close() error
Close flushes any still-buffered global records (best effort) and closes inner. Scoped buffers that never activated are intentionally discarded — that is the whole point (no error ⇒ no debug noise).
type HTTPBatchSink ¶
type HTTPBatchSink struct {
URL string
Headers map[string]string
MaxBatch int
Flush time.Duration
MinLvl Level
Client *http.Client
// Build turns a batch of records into a request body + content-type.
Build func(recs []*Record) (body []byte, contentType string)
// contains filtered or unexported fields
}
HTTPBatchSink is the shared primitive behind the cloud sinks: it buffers encoded records and flushes them as one HTTP POST when the batch hits MaxBatch records or FlushInterval elapses. One bad flush never blocks the app (delivery happens on a background goroutine); failures increment a dropped counter rather than panicking.
func NewDatadogSink ¶
func NewDatadogSink(intakeURL, apiKey string, minLevel Level) *HTTPBatchSink
NewDatadogSink ships to the Datadog logs intake (ndjson array). apiKey is sent via the DD-API-KEY header.
func NewElasticsearchSink ¶
func NewElasticsearchSink(bulkURL, index string, minLevel Level) *HTTPBatchSink
NewElasticsearchSink uses the ES _bulk API (action + source per record).
func NewHTTPBatchSink ¶
func NewHTTPBatchSink(url string, minLevel Level, build func([]*Record) ([]byte, string)) *HTTPBatchSink
NewHTTPBatchSink constructs a batch sink. Build is required.
func NewLokiSink ¶
func NewLokiSink(pushURL string, labels map[string]string, minLevel Level) *HTTPBatchSink
NewLokiSink pushes to Grafana Loki's HTTP push API. labels become the stream labels; the log line is the JSON-encoded record.
func (*HTTPBatchSink) Close ¶
func (h *HTTPBatchSink) Close() error
Close flushes and stops accepting.
func (*HTTPBatchSink) Dropped ¶
func (h *HTTPBatchSink) Dropped() uint64
Dropped reports records lost to delivery failures (never silent).
func (*HTTPBatchSink) Emit ¶
func (h *HTTPBatchSink) Emit(r *Record) error
Emit implements Sink (buffers; flushes on size/timer).
func (*HTTPBatchSink) Sync ¶
func (h *HTTPBatchSink) Sync() error
Sync flushes the pending batch synchronously.
func (*HTTPBatchSink) WithHeader ¶
func (h *HTTPBatchSink) WithHeader(k, v string) *HTTPBatchSink
WithHeader adds a request header (e.g. auth) and returns the sink.
type JSONEncoder ¶
type JSONEncoder struct {
TimeKey string
LevelKey string
MsgKey string
// NumericLevel emits the OTEL SeverityNumber instead of the band name.
NumericLevel bool
}
JSONEncoder emits one JSON object per line (ndjson) with OTEL-aligned keys. Configurable key names keep it compatible with downstream log backends.
func NewJSONEncoder ¶
func NewJSONEncoder() JSONEncoder
NewJSONEncoder returns a JSONEncoder with conventional keys.
func (JSONEncoder) Encode ¶
func (e JSONEncoder) Encode(buf *buffer, r *Record)
Encode implements Encoder.
type Level ¶
type Level int
Level is a log severity modeled directly on the OpenTelemetry Logs SeverityNumber (1..24, four sub-steps per band) so the level survives an OTEL bridge without a lossy remap. 0 means "unspecified".
TRACE 1-4 · DEBUG 5-8 · INFO 9-12 · WARN 13-16 · ERROR 17-20 · FATAL 21-24
Any SeverityNumber >= 17 (ERROR) denotes an erroneous record.
const ( LevelTrace Level = 1 LevelDebug Level = 5 LevelInfo Level = 9 LevelWarn Level = 13 LevelError Level = 17 LevelFatal Level = 21 )
Canonical band anchors. Custom levels are just other ints in a band, e.g. LevelDebug+1 for "DEBUG2".
func (Level) MarshalText ¶
MarshalText implements encoding.TextMarshaler (lowercase band name).
func (Level) SeverityNumber ¶
SeverityNumber returns the raw OTEL SeverityNumber.
type LevelHandler ¶
type LevelHandler struct {
// contains filtered or unexported fields
}
LevelHandler is an http.Handler for runtime level control without a restart. GET returns the current level; PUT/POST with body or ?level= sets it. Levels accept band names (trace/debug/info/warn/error/fatal) or the raw OTEL SeverityNumber.
mux.Handle("/loglevel", logger.NewLevelHandler(lv))
func NewLevelHandler ¶
func NewLevelHandler(lv *LevelVar) *LevelHandler
NewLevelHandler exposes a *LevelVar over HTTP.
func (*LevelHandler) ServeHTTP ¶
func (h *LevelHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
type LevelVar ¶
type LevelVar struct {
// contains filtered or unexported fields
}
LevelVar is a Leveler whose value can be swapped atomically at runtime — the basis for dynamic per-module level control without a restart.
type Leveler ¶
type Leveler interface {
Level() Level
}
Leveler reports the minimum level a logger currently emits. Implementations may change the returned value at runtime (see LevelVar) so callers must call Level() per-decision, never cache it.
type LogfmtEncoder ¶
type LogfmtEncoder struct{}
LogfmtEncoder emits `key=value` pairs (Heroku/Go-kit style) — the human-skimmable yet machine-parseable middle ground. Values needing quoting (spaces, quotes, =) are double-quoted.
func NewLogfmtEncoder ¶
func NewLogfmtEncoder() LogfmtEncoder
NewLogfmtEncoder returns a LogfmtEncoder.
func (LogfmtEncoder) Encode ¶
func (LogfmtEncoder) Encode(buf *buffer, r *Record)
Encode implements Encoder.
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger is the core. It is immutable after construction except via With, which returns a child sharing the same pipeline + transport but with extra bound fields. Safe for concurrent use.
func Development ¶
func Development() *Logger
Development returns a logger tuned for local work: pretty, colored, TTY-aware console on stderr, Debug level, caller location on. One line, no boilerplate.
func Production ¶
func Production() *Logger
Production returns a logger tuned for services: JSON to stderr, Info level, async (bounded channel, drop-newest under pressure so a log storm can't stall request handling), light sampling that never drops errors. Call Close() on shutdown to drain.
func Test ¶
Test returns a logger writing pretty output to the given writer at Trace with no async/sampling — deterministic for examples and manual debugging. For assertions use the logtest package instead.
func (*Logger) DebugContext ¶
func (*Logger) Enabled ¶
Enabled reports whether a record at level l would be emitted. Call this to guard expensive field construction.
func (*Logger) ErrorContext ¶
func (*Logger) Event ¶
Event logs a named typed event with NO message (mulog "events not messages"): the event name is the primary index — better for analytics and AI consumption than free-form prose. Defaults to Info; use EventAt for a level/ctx.
func (*Logger) Go ¶
Go runs fn in a goroutine whose panics are logged (ERROR) instead of crashing the process — a safe-goroutine primitive.
func (*Logger) Handler ¶
Handler returns an slog.Handler backed by this Logger, so the whole slog ecosystem (samber/slog-*, otelslog) composes on top of us.
func (*Logger) InfoContext ¶
func (*Logger) Infot ¶
Example ¶
Message templates keep a stable key, render text, and emit structured fields — all from one call.
package main
import (
logger "github.com/ubgo/logger"
)
func main() {
log := logger.New()
log.Infot("processed {count} files for {user}", 12, "ada")
// msg="processed 12 files for ada", msg_template kept, count/user structured
}
Output:
func (*Logger) Metrics ¶
Metrics returns the logger's self-observability counters (emitted/dropped/ sink-errors/by-level). Child loggers from With share the parent's metrics.
func (*Logger) NewSlog ¶
NewSlog returns an *slog.Logger writing through this Logger.
Example ¶
The slog bridge: the whole slog ecosystem composes on top.
package main
import (
logger "github.com/ubgo/logger"
)
func main() {
log := logger.New()
sl := log.NewSlog()
sl.Info("via slog", "key", "value")
}
Output:
func (*Logger) Recover ¶
Recover logs a panic (with stack) at FATAL and re-panics, so crashes are never invisible while the original crash semantics are preserved. Use as the first deferred call:
defer log.Recover(ctx)
func (*Logger) RecoverAndContinue ¶
RecoverAndContinue logs a panic at ERROR and swallows it — for worker loops / request handlers that must not take the process down. MUST be used as a direct deferred call (Go's recover() only works when the deferred function itself calls it):
defer log.RecoverAndContinue(ctx)
func (*Logger) StartSpan ¶
func (l *Logger) StartSpan(ctx context.Context, name string, fields ...Field) (context.Context, *Span)
StartSpan opens a span. The returned context must be used for all logs that should belong to it (and passed to child StartSpan calls to nest). Always pair with defer span.End() (or span.Fail).
ctx, span := log.StartSpan(ctx, "handle_order", logger.String("order", id))
defer span.End()
Example ¶
Spans give scoped context + a causal tree from a flat log stream.
package main
import (
"context"
logger "github.com/ubgo/logger"
)
func main() {
log := logger.New()
ctx, span := log.StartSpan(context.Background(), "handle_order",
logger.String("order_id", "o-42"))
defer span.End() // emits span.end with duration + ok
log.InfoContext(ctx, "charging card") // inherits span_id/order_id
}
Output:
func (*Logger) StdLogWriter ¶
StdLogWriter returns an io.Writer suitable for log.SetOutput, so existing `log.Print*` calls flow through this logger at the given level.
func (*Logger) StdLogger ¶
StdLogger returns a *log.Logger that writes through this logger — a drop-in for code that takes a *log.Logger.
func (*Logger) TraceContext ¶
func (*Logger) WarnContext ¶
func (*Logger) With ¶
With returns a child logger that prepends fields to every record. Inherited fields are copied so the parent is unaffected.
Example ¶
A child logger inherits bound fields; the parent is unaffected.
package main
import (
logger "github.com/ubgo/logger"
)
func main() {
log := logger.New()
reqLog := log.With(logger.String("request_id", "abc123"))
reqLog.Info("handling") // includes request_id
log.Info("global event") // does not
}
Output:
type Metrics ¶
type Metrics struct {
// contains filtered or unexported fields
}
Metrics is the logger's self-observability: how many records it emitted, dropped (sampling/dedup/pipeline), and how many sink errors occurred — so you can monitor your monitoring. Cheap atomics; always on.
func (*Metrics) IncSinkError ¶
func (m *Metrics) IncSinkError()
IncSinkError records a sink failure. Wire it through Fanout.OnError:
fan.OnError = func(Sink, error) { log.Metrics().IncSinkError() }
type NetSink ¶
type NetSink struct {
// contains filtered or unexported fields
}
NetSink streams encoded records over TCP/UDP/TLS. It dials lazily and transparently re-dials on a write error so a transient network blip drops at most the in-flight line (counted) rather than wedging the app.
func NewTCPSink ¶
NewTCPSink streams to a TCP endpoint.
func NewTLSSink ¶
NewTLSSink streams over TLS-wrapped TCP.
func NewUDPSink ¶
NewUDPSink streams to a UDP endpoint (fire-and-forget).
type Option ¶
type Option func(*config)
Option configures a Logger at construction.
func WithCaller ¶
WithCaller enables file:line capture (lazily resolved by encoders).
func WithLeveler ¶
WithLeveler sets a dynamic level source (e.g. *LevelVar) for runtime control.
func WithProcessors ¶
WithProcessors sets the pipeline (enrich/redact/sample/...). Order matters.
func WithSink ¶
WithSink sets the terminal destination, delivered inline (SyncTransport). Use WithTransport for async.
func WithTransport ¶
WithTransport sets an explicit transport (Sync/Channel/Ring). Overrides WithSink.
type OverflowPolicy ¶
type OverflowPolicy uint8
OverflowPolicy is the explicit, named backpressure choice for async transports (spdlog/Logback model). The library never silently guesses.
const ( // Block: the caller waits until the queue has room (lossless, adds latency). Block OverflowPolicy = iota // DropNewest: discard the incoming record (protects latency, loses newest). DropNewest // DropOldest: evict the oldest queued record to make room. DropOldest )
type PathRedactor ¶
type PathRedactor struct {
// contains filtered or unexported fields
}
PathRedactor is the compiled declarative redaction stage (pino/LogTape model). Patterns are dotted paths over field keys — which already carry slog group prefixes ("http.headers.authorization"). Segment wildcards:
- matches exactly one segment ** matches zero or more segments (must be the last segment)
Examples: "password", "*.password", "req.headers.authorization", "user.**".
func NewPathRedactor ¶
func NewPathRedactor(strategy RedactStrategy, censor string, patterns ...string) *PathRedactor
NewPathRedactor compiles patterns once. Strategy + censor are fixed for the stage (compose multiple stages for mixed policies).
Example ¶
Compiled path-DSL redaction masks secrets before any sink sees them.
package main
import (
"os"
logger "github.com/ubgo/logger"
)
func main() {
pr := logger.NewPathRedactor(logger.Mask, "[SECRET]",
"*.password", "req.headers.authorization")
log := logger.New(
logger.WithProcessors(pr),
logger.WithSink(logger.NewWriterSink(os.Stdout, logger.NewJSONEncoder(), logger.LevelInfo)),
)
log.Info("login", logger.String("user.password", "hunter2")) // → [SECRET]
}
Output:
type Processor ¶
Processor is the single extension seam (structlog model). Enrichment, redaction, sampling, dedup are all Processors composed into a pipeline. Mutate r in place; return ErrDrop to discard; return nil to continue.
type ProcessorFunc ¶
ProcessorFunc adapts a function to Processor.
type Record ¶
type Record struct {
Time time.Time
Level Level
Message string
// PC is the caller program counter (0 if caller capture is disabled);
// resolved lazily so the cost is only paid when actually formatted.
PC uint64
// EventName, when set, is the stable identity of a named typed event
// (mulog "events not messages" + OTEL EventName). Use it instead of
// prose for analytics/AI-friendly logs.
EventName string
// Fields are this event's own attributes plus any inherited via With().
Fields []Field
// Ctx carries request-scoped values + the active trace span. Never nil
// in the pipeline (defaults to context.Background()).
Ctx context.Context
// contains filtered or unexported fields
}
Record is one log event flowing through the processor pipeline. It is pooled: never retain a *Record past the call that produced it. Copy fields out (or call Clone) if you must keep them (e.g. async transport, buffering).
type RedactProcessor ¶
type RedactProcessor struct {
Deny map[string]struct{}
Censor string // replacement, default "[REDACTED]"
}
RedactProcessor masks the values of fields whose key is in Deny. This is the minimal v1 redaction stage; the compiled path-DSL (*.password) lands later behind this same interface.
func NewRedactProcessor ¶
func NewRedactProcessor(keys ...string) *RedactProcessor
NewRedactProcessor builds a key-denylist redactor.
type RedactStrategy ¶
type RedactStrategy uint8
RedactStrategy is what happens to a matched field.
const ( // Mask replaces the value with the censor string. Mask RedactStrategy = iota // Hash replaces it with a stable sha256 prefix (preserves correlation // without exposing the value). Hash // Drop removes the field entirely. Drop )
type RingTransport ¶
type RingTransport struct {
// contains filtered or unexported fields
}
RingTransport is a bounded ring buffer drained by one worker goroutine. It is the high-throughput async engine: a fixed pre-allocated slice avoids the per-record channel-send overhead and makes DropOldest O(1).
NOTE: this is a correct mutex+cond bounded ring, not yet a lock-free Disruptor. The Transport interface is the contract; a lock-free MPSC variant can replace this implementation later without touching callers. We ship the correct version rather than a subtly-broken lock-free claim.
func NewRingTransport ¶
func NewRingTransport(s Sink, capacity int, policy OverflowPolicy) *RingTransport
NewRingTransport starts a worker draining a ring of the given capacity.
func (*RingTransport) Close ¶
func (t *RingTransport) Close() error
Close drains remaining records, stops the worker, closes the sink.
func (*RingTransport) Dispatch ¶
func (t *RingTransport) Dispatch(r *Record)
Dispatch implements Transport.
func (*RingTransport) Dropped ¶
func (t *RingTransport) Dropped() uint64
func (*RingTransport) Sync ¶
func (t *RingTransport) Sync() error
type RotatingFile ¶
type RotatingFile struct {
// Path is the active log file.
Path string
// MaxSizeBytes triggers rotation when exceeded (default 100 MiB).
MaxSizeBytes int64
// MaxBackups caps retained rotated files (0 = keep all).
MaxBackups int
// MaxAge prunes rotated files older than this (0 = no age limit).
MaxAge time.Duration
// Compress gzips rotated segments.
Compress bool
// contains filtered or unexported fields
}
RotatingFile is an io.WriteCloser with owned size-based rotation, age/count retention, and optional gzip compression of rotated segments — so a "last logger" doesn't punt rotation to lumberjack/logrotate. It also supports Reopen() for logrotate-style external rotation (SIGHUP).
func NewRotatingFile ¶
func NewRotatingFile(path string) (*RotatingFile, error)
NewRotatingFile opens (creating dirs as needed) the log file.
func (*RotatingFile) Reopen ¶
func (r *RotatingFile) Reopen() error
Reopen closes and reopens the active file — for SIGHUP / logrotate (copytruncate-free) external rotation.
type SampleProcessor ¶
type SampleProcessor struct {
First uint64
Thereafter uint64 // keep 1 in M after First; 0 disables sampling
NeverBelow Level
// contains filtered or unexported fields
}
SampleProcessor keeps the first N records per reset window then 1 in M, and NEVER samples records at or above NeverBelow (default LevelError) — you must not drop errors. Drops are counted (no silent loss).
func NewSampleProcessor ¶
func NewSampleProcessor(first, thereafter uint64) *SampleProcessor
NewSampleProcessor builds a leveled sampler.
func (*SampleProcessor) Dropped ¶
func (p *SampleProcessor) Dropped() uint64
Dropped reports how many records the sampler discarded.
type Sink ¶
type Sink interface {
// Emit writes r if r.Level >= the sink's level.
Emit(r *Record) error
// Sync flushes buffered data.
Sync() error
// Close flushes and releases resources.
Close() error
}
Sink is a terminal destination. Each sink owns its level + encoder so a fan-out can send the same record to console(debug,pretty) and file(info,json) simultaneously. A sink that errors must not block or kill sibling sinks (see Fanout).
type Snapshot ¶
type Snapshot struct {
Emitted uint64 `json:"emitted"`
Dropped uint64 `json:"dropped"`
SinkErrors uint64 `json:"sink_errors"`
ByLevel map[string]uint64 `json:"by_level"`
}
Snapshot is an immutable read of the counters.
type Source ¶
Source is a resolved caller location. Resolution is done lazily by encoders (only when a record is actually formatted) so PC capture stays cheap.
type Span ¶
type Span struct {
// contains filtered or unexported fields
}
Span is scoped structured context with an outcome — the tracing-rs "spans as context" + Eliot "causal action tree" idea, logging-native (no separate tracer). Every log made with the span's ctx inherits its fields and a hierarchical span path, so a flat log stream reconstructs the tree of what happened and why. End/Fail emit a single duration+outcome record.
func (*Span) End ¶
func (s *Span) End()
End emits the single span-completion record with duration + outcome. Safe to call once; subsequent calls are no-ops.
type SyncTransport ¶
type SyncTransport struct {
// contains filtered or unexported fields
}
SyncTransport delivers inline on the calling goroutine. Simplest, lossless, correct; the default until the caller opts into async.
func NewSyncTransport ¶
func NewSyncTransport(s Sink) *SyncTransport
NewSyncTransport wraps a sink for inline delivery.
func (*SyncTransport) Close ¶
func (t *SyncTransport) Close() error
func (*SyncTransport) Dispatch ¶
func (t *SyncTransport) Dispatch(r *Record)
func (*SyncTransport) Dropped ¶
func (t *SyncTransport) Dropped() uint64
func (*SyncTransport) Sync ¶
func (t *SyncTransport) Sync() error
type SyslogSink ¶
type SyslogSink struct {
// contains filtered or unexported fields
}
SyslogSink writes to a local or remote syslog daemon (RFC 3164/5424 via the stdlib). Severity is mapped from the record level so syslog filtering works. Unix-only (build-tagged); on Windows use NetSink or the event-log path.
func NewSyslogSink ¶
func NewSyslogSink(network, addr, tag string, enc Encoder, minLevel Level) (*SyslogSink, error)
NewSyslogSink dials syslog. network "" + addr "" uses the local daemon; otherwise e.g. ("tcp","logs.example:514"). tag is the syslog program tag.
func (*SyslogSink) Emit ¶
func (s *SyslogSink) Emit(r *Record) error
Emit implements Sink, routing to the matching syslog severity.
func (*SyslogSink) Sync ¶
func (s *SyslogSink) Sync() error
Sync is a no-op (syslog writer is unbuffered).
type TraceExtractor ¶
TraceExtractor pulls correlation IDs out of a context. The core stays zero-dependency: contrib/otel registers a real OTEL/W3C extractor; tests or custom propagation can register their own.
type Transport ¶
type Transport interface {
// Dispatch delivers r to the sink. Implementations that defer delivery
// MUST Clone r — the caller's *Record is pooled and reused on return.
Dispatch(r *Record)
// Dropped returns the total records lost to the overflow policy (never
// silent: callers/self-metrics surface this).
Dropped() uint64
Sync() error
Close() error
}
Transport moves a finished Record from the log call to the sink. It is the pluggable async engine seam: Sync (inline), Channel (bounded chan + worker), Ring (bounded ring + worker) are all interchangeable behind this interface.
type WriterSink ¶
type WriterSink struct {
W io.Writer
Enc Encoder
MinLvl Leveler
// contains filtered or unexported fields
}
WriterSink adapts any io.Writer + Encoder into a Sink with its own level.
func NewConsoleSink ¶
func NewConsoleSink(w io.Writer, minLevel Level) *WriterSink
NewConsoleSink builds a console WriterSink that auto-enables color only when w is a real terminal and NO_COLOR is unset — pretty in dev, plain in files/CI without any flag.
func NewFileSink ¶
func NewFileSink(rf *RotatingFile, enc Encoder, minLevel Level) *WriterSink
NewFileSink builds a Sink writing encoded records to a self-rotating file.
func NewWriterSink ¶
func NewWriterSink(w io.Writer, enc Encoder, minLevel Level) *WriterSink
NewWriterSink builds a WriterSink. minLevel may be a *LevelVar for runtime control or a constant Level.
func (*WriterSink) Sync ¶
func (s *WriterSink) Sync() error
Sync implements Sink (best-effort flush for Syncer writers).
Source Files
¶
- admin.go
- anyval.go
- atomic.go
- audit.go
- buffer.go
- caller.go
- config_reload.go
- dedup.go
- encoder.go
- encoder_console.go
- encoder_json.go
- encoder_logfmt.go
- enrich.go
- field.go
- fingerscrossed.go
- handler.go
- level.go
- logger.go
- math.go
- metrics.go
- presets.go
- processor.go
- race_off.go
- record.go
- recover.go
- redact_path.go
- shim_std.go
- signal.go
- sink.go
- sink_file.go
- sink_http.go
- sink_net.go
- sink_syslog_unix.go
- span.go
- template.go
- transport.go
- transport_disruptor.go
- transport_ring.go
- tty.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Demo of the ubgo/logger core: typed fields, fan-out with per-sink level+encoder, a redaction processor, sampling, async transport, and the slog bridge.
|
Demo of the ubgo/logger core: typed fields, fan-out with per-sink level+encoder, a redaction processor, sampling, async transport, and the slog bridge. |
|
Package logtest provides a capturing sink and assertion helpers so logging is testable: assert what was logged, at which level, with which fields — and optionally fail a test on any unexpected ERROR.
|
Package logtest provides a capturing sink and assertion helpers so logging is testable: assert what was logged, at which level, with which fields — and optionally fail a test on any unexpected ERROR. |