logger

package module
v0.0.0-...-ab38593 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 19, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

README

ubgo/logger — the last Go logging library you'll need

Go Reference Go Report Card test lint coverage tag license Go

ubgo/logger is a pluggable, adapter-based, log/slog-native structured logging library for Go — zero-allocation on the hot path, batteries included, and a drop-in upgrade path from zap, zerolog, logrus, slog, and logr.

It is the consolidation of the best ideas from the Go, JVM, .NET, Rust, JavaScript, and Python logging ecosystems into one coherent, benchmarked package: structured logging + debug-on-error buffering + secret redaction + sampling + OpenTelemetry trace correlation + log rotation + tamper-evident audit logs + spans + message templates, behind one small API.

If you've ever asked "which Go logging library should I use — zap, zerolog, logrus, or slog?", this is the answer that ends the question.


Table of contents


Why ubgo/logger

log/slog won the Go logging interface war — the whole ecosystem now writes slog.Handler backends. But slog is deliberately minimal: no sampling, no log rotation, no async/backpressure, no PII redaction, no dedup, no runtime level control, and writing a correct slog.Handler is a documented footgun. The community filled the gaps with 50+ tiny, single-purpose dependencies.

ubgo/logger is the slog backend that fills every gap — one dependency, one mental model, honest benchmarks:

  • slog-native — it is a correct slog.Handler (passes the standard library's testing/slogtest). The entire slog ecosystem composes on top.
  • Zero-allocation typed hot path (CI-enforced), competitive with zap and zerolog.
  • One extension seam — a processor pipeline. Redaction, sampling, enrichment, dedup are all the same concept.
  • Batteries included — rotation, redaction, sampling, OTEL correlation, FingersCrossed, audit, network/cloud sinks — built in, not 50 dependencies.
  • Drop-in migration from zap, zerolog, logrus, std log, and logr.

Feature highlights

Category What you get
API slog-native · type-safe generic fields (String, Int[T], …) · message templates · named events
Performance zero-allocation typed path (~295 ns/op, 0 B, 0 allocs, CI-gated) · object pooling
Transports sync · bounded-channel · lock-free Disruptor ring; explicit Block/DropNewest/DropOldest backpressure + dropped-count
Reliability per-sink level + encoder + failure isolation · honest drop accounting
Differentiators FingersCrossed debug-on-error buffering · compiled path-DSL redaction · spans-as-context causal trees · tamper-evident audit chain
Context context.Context propagation · OTEL trace_id/span_id correlation · MDC-equivalent bound fields
Sinks console (TTY-aware) · JSON · logfmt · file (rotation/retention/gzip) · syslog · TCP/UDP/TLS · Loki · Datadog · Elasticsearch · OTLP · Sentry
Ops runtime level via HTTP / signal / config file · self-metrics endpoint
DX Development()/Production() presets · logtest assertion kit · panic-recovery helpers

Install

Requires Go 1.24+.

go get github.com/ubgo/logger

Optional adapter modules (only pull the heavy dependency you use):

go get github.com/ubgo/logger/contrib/zap      # migrate from uber-go/zap
go get github.com/ubgo/logger/contrib/logrus   # migrate from sirupsen/logrus
go get github.com/ubgo/logger/contrib/zerolog  # migrate from rs/zerolog
go get github.com/ubgo/logger/contrib/phuslu   # migrate from phuslu/log
go get github.com/ubgo/logger/contrib/logr     # Kubernetes / controller-runtime
go get github.com/ubgo/logger/contrib/otel     # OpenTelemetry Logs bridge
go get github.com/ubgo/logger/contrib/sentry   # Sentry error events

Quick start (step by step)

1. The simplest possible logger
package main

import logger "github.com/ubgo/logger"

func main() {
	log := logger.New() // JSON to stderr at Info
	defer log.Close()

	log.Info("server started", logger.String("addr", ":8080"), logger.Int("pid", 4242))
}
{"time":"2026-05-19T12:00:00Z","level":"info","msg":"server started","addr":":8080","pid":4242}
2. Use a preset
log := logger.Development() // pretty, colored, Debug, caller — for local dev
// or
log := logger.Production()  // JSON, Info, async, sampled — for services
defer log.Close()
3. Add request context
reqLog := log.With(logger.String("request_id", "abc-123"))
reqLog.Info("handling request") // request_id on every line
4. Wire it as the standard slog logger (so all libraries benefit)
import "log/slog"

slog.SetDefault(log.NewSlog())
slog.Info("now every slog call in your deps flows through ubgo/logger")
5. Build a production pipeline
log := logger.New(
	logger.WithLevel(logger.LevelInfo),
	logger.WithProcessors(
		logger.NewPathRedactor(logger.Mask, "[REDACTED]", "*.password", "*.token"),
		logger.NewSampleProcessor(100, 100), // first 100, then 1/100 — never drops ERROR
	),
	logger.WithTransport(logger.NewDisruptorTransport(
		logger.NewWriterSink(os.Stderr, logger.NewJSONEncoder(), logger.LevelInfo),
		8192, logger.DropNewest,
	)),
)
defer log.Close() // drains the async ring

That's the whole setup. The sections below show each capability.

Core concepts

There are five nouns:

  • Logger — what you call (log.Info(...)). Immutable; With() returns a child.
  • Field — a type-safe key/value (logger.String, logger.Int[T], logger.Err, …). Scalars are unboxed → zero allocation.
  • Processor — the single extension seam: func(ctx, *Record) error. Enrichment, redaction, sampling, dedup are all processors. Returning logger.ErrDrop drops the record (this is how sampling works).
  • Transport — how a record gets from the call site to the sink: Sync (inline), Channel (bounded queue), or Disruptor (lock-free ring) — each with an explicit overflow policy.
  • Sink — the destination (console, file, network, cloud). Each sink owns its own level + encoder; a Fanout broadcasts to many with failure isolation.

Full design rationale: docs/architecture.md.

Recipes

Structured fields (zero-allocation)
log.Info("payment processed",
	logger.String("user", userID),
	logger.Int("amount_cents", 1999),
	logger.Bool("captured", true),
	logger.Dur("latency", elapsed),
	logger.Err(err), // nil-safe; emits "error":null
)

Use logger.Any(key, v) for arbitrary values (reflection, off the hot path).

Fan-out to multiple sinks
console := logger.NewConsoleSink(os.Stdout, logger.LevelDebug) // pretty, TTY-aware
jsonF, _ := logger.NewRotatingFile("/var/log/app.log")
file := logger.NewFileSink(jsonF, logger.NewJSONEncoder(), logger.LevelInfo)

log := logger.New(logger.WithSink(logger.NewFanout(console, file)))

Each sink keeps its own level and encoder; one failing sink never blocks the others.

Debug-on-error (FingersCrossed)

The killer feature. A successful request logs nothing below the activation level. The first error flushes the entire buffered debug trail — so you get full forensics exactly when something breaks, and silence when it doesn't.

fc := logger.NewFingersCrossed(
	logger.NewWriterSink(os.Stderr, logger.NewJSONEncoder(), logger.LevelTrace),
)
log := logger.New(logger.WithTransport(logger.NewSyncTransport(fc)), logger.WithLevel(logger.LevelTrace))

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := logger.FCScope(r.Context()) // one buffer per request
	log.DebugContext(ctx, "loaded config")
	log.DebugContext(ctx, "queried db")
	// if everything succeeds → nothing is emitted
	// if log.ErrorContext(ctx, "boom") fires → the two Debug lines + the error are all flushed
}
Secret/PII redaction

Redaction happens in-process, before bytes reach any sink — the only place raw values and structure coexist.

pr := logger.NewPathRedactor(logger.Mask, "[REDACTED]",
	"*.password",                  // any password field at any depth
	"req.headers.authorization",   // exact dotted path
	"user.**",                     // everything under user
)
log := logger.New(logger.WithProcessors(pr))

Strategies: logger.Mask (replace), logger.Hash (sha256 prefix — keeps correlation), logger.Drop (remove).

Sampling under load
// keep the first 100, then 1 in every 100 — but NEVER sample ERROR and above
log := logger.New(logger.WithProcessors(logger.NewSampleProcessor(100, 100)))

DedupProcessor collapses identical repeated lines and annotates the survivor with deduped_count.

Context, tracing, and request scoping
ctx = logger.ContextWith(ctx, logger.String("tenant", "acme")) // MDC-style bound field
log.InfoContext(ctx, "doing work")                              // tenant included automatically

For OpenTelemetry trace correlation, add the enricher with the OTEL extractor (see contrib/otel):

log := logger.New(logger.WithProcessors(
	logger.NewEnrichProcessor(otellogger.TraceExtractor()), // adds trace_id/span_id from the active span
))
Spans (causal log trees)
ctx, span := log.StartSpan(ctx, "checkout", logger.String("order", id))
defer span.End() // emits span.end with duration + ok

log.InfoContext(ctx, "charging card") // inherits span identity + fields
_, child := log.StartSpan(ctx, "charge_gateway")
// ... span_path "1.1" lets you reconstruct the tree from a flat log stream
child.Fail(err) // span.end becomes level=error, ok=false
child.End()
Message templates

Serilog-style: one call gives you readable text and structured fields and a stable grouping key.

log.Infot("processed {count} files for {user}", 12, "ada")
// msg="processed 12 files for ada"
// msg_template="processed {count} files for {user}"  ← stable for alerting/grouping
// count=12, user="ada"                               ← structured
Events, not messages
log.Event("user.signup", logger.String("plan", "pro"), logger.Int("uid", 7))
// no prose — the event name is the primary index (great for analytics/AI)
Log file rotation

Built in. No lumberjack dependency.

rf, _ := logger.NewRotatingFile("/var/log/app.log")
rf.MaxSizeBytes = 100 << 20 // 100 MiB
rf.MaxBackups = 7
rf.MaxAge = 14 * 24 * time.Hour
rf.Compress = true // gzip rotated segments
log := logger.New(logger.WithSink(logger.NewFileSink(rf, logger.NewJSONEncoder(), logger.LevelInfo)))

// logrotate-friendly: reopen on SIGHUP
stop := logger.OnSIGHUP(func() { _ = rf.Reopen() })
defer stop()
Async delivery & backpressure
sink := logger.NewWriterSink(os.Stderr, logger.NewJSONEncoder(), logger.LevelInfo)

// bounded channel + worker
t := logger.NewChannelTransport(sink, 4096, logger.DropNewest)
// or lock-free Disruptor ring for max throughput
t := logger.NewDisruptorTransport(sink, 8192, logger.Block)

log := logger.New(logger.WithTransport(t))
defer log.Close() // drains the queue

// dropped records are counted, never silent:
n := t.Dropped()
Tamper-evident audit logs
f, _ := os.Create("/var/log/audit.log")
audit := logger.NewAuditSink(f, logger.NewJSONEncoder())
log := logger.New(logger.WithTransport(logger.NewSyncTransport(audit)))

log.Info("user deleted record", logger.String("actor", "admin"), logger.Int("id", 42))

Each line is hash-chained (sha256(prev || record)). Verify integrity later:

res := logger.VerifyAudit(file)
if !res.OK {
	fmt.Printf("tampered at seq %d: %s\n", res.BrokenAtSeq, res.Reason)
}

Detects edits, deletions, and reordering.

Runtime log level (HTTP / signal / file)
lv := logger.NewLevelVar(logger.LevelInfo)
log := logger.New(logger.WithLeveler(lv))

// 1. HTTP: GET/PUT /loglevel?level=debug
http.Handle("/loglevel", logger.NewLevelHandler(lv))

// 2. Signal: flip to debug on SIGUSR2, back on next
stop := logger.CycleLevelOnSignal(lv, syscall.SIGUSR2, logger.LevelInfo, logger.LevelDebug)
defer stop()

// 3. Config file: {"level":"warn"} hot-reloaded
_, stopW := logger.WatchConfigFile("/etc/app/log.json", lv, 5*time.Second)
defer stopW()

Self-metrics (emitted/dropped/by-level) are exposed too:

http.Handle("/logmetrics", log.Metrics())
The slog bridge
slog.SetDefault(log.NewSlog())
// every slog.Handler middleware (samber/slog-*, otelslog) composes on top of ubgo/logger
Testing your logs
import "github.com/ubgo/logger/logtest"

func TestSignup(t *testing.T) {
	log, cap := logtest.New()
	svc := NewService(log)
	svc.Signup("ada")

	cap.AssertLogged(t, logger.LevelInfo, "signup complete")
	cap.AssertField(t, "user", "ada")
	cap.AssertNoErrors(t)
}

Migrating from zap / zerolog / logrus / slog

Migration is mechanical — keep your existing call sites, swap the engine.

From How Module
log/slog slog.SetDefault(log.NewSlog()) core (no extra dep)
std log logger.RedirectStdLog(log, logger.LevelInfo) core
uber-go/zap zaplogger.New(core, zapcore.InfoLevel) contrib/zap
sirupsen/logrus logruslogger.Attach(logrusLogger, core) contrib/logrus
rs/zerolog zerologlogger.New(zl, logger.LevelInfo) contrib/zerolog
phuslu/log phulogger.New(pl, logger.LevelInfo) contrib/phuslu
go-logr/logr logrlogger.New(core) contrib/logr

Full guide: docs/migration.md.

Contrib modules

Heavy third-party dependencies are isolated in separate, independently-versioned submodules so the core stays dependency-free:

Module Purpose
contrib/zap Forward zap call sites through ubgo/logger
contrib/logrus logrus.Hook + Attach() drop-in
contrib/zerolog Ship through a zerolog.Logger
contrib/phuslu Ship through a phuslu/log writer
contrib/logr logr.Logger for Kubernetes / controller-runtime
contrib/otel OpenTelemetry Logs bridge + W3C trace extractor
contrib/sentry WARN+ records as Sentry events

Performance

Measured on Apple M-series, Go 1.24, output to io.Discard. Allocation count is enforced by a CI gate (TestZeroAlloc*).

Path ns/op B/op allocs/op
Typed hot path ~295 0 0
Disabled level (gated out) ~7 0 0
Through the slog bridge ~698 320 1
stdlib slog JSON (reference) ~704 0 0

The slog-bridge row is the honest through-bridge cost (slog's own Record/attrs allocation for >5 attrs) — published, not hidden. "Portable via slog" silently costing 10–40× is the ecosystem trap this library refuses to repeat.

See docs/performance.md for the methodology and how to reproduce.

FAQ

Is ubgo/logger a replacement for zap / zerolog / logrus? Yes — it's a zero-allocation, slog-native superset with batteries included, plus drop-in migration shims so switching is mechanical.

Should I use it instead of log/slog? Use slog's API; get ubgo/logger's engine. It implements slog.Handler (passing testing/slogtest) and adds sampling, rotation, redaction, async, FingersCrossed, audit, and trace correlation that slog deliberately omits.

Does it support OpenTelemetry? Yes — contrib/otel is an OTEL Logs bridge, and the core's level model is the OTEL SeverityNumber. Logs correlate with traces via trace_id/span_id.

Is it production-ready? The full feature set is implemented and race-tested with a CI matrix across all modules and an allocation-regression gate. APIs are stabilizing toward a v1.

Why not just import 50 samber/slog-* packages? You can — they compose on top, since ubgo/logger is a correct slog.Handler. But the things you actually need in production (rotation, redaction, sampling, backpressure, debug-on-error) are first-class here, in one dependency, benchmarked together.

Zero dependencies? The core module has no third-party dependencies. Heavy integrations live in opt-in contrib/* submodules.

Documentation

License

Apache-2.0 © the ubgo authors.


Keywords: Go logging library, golang structured logging, slog handler, zap alternative, zerolog alternative, logrus replacement, zero allocation logger, OpenTelemetry logging Go, log rotation, PII redaction, debug on error, tamper-evident audit log, Kubernetes logr.

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),
	)
}

Index

Examples

Constants

View Source
const GenesisHash = "0000000000000000000000000000000000000000000000000000000000000000"

GenesisHash is the chain anchor for the first record.

Variables

View Source
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

func ContextWith(ctx context.Context, fields ...Field) context.Context

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

func CycleLevelOnSignal(lv *LevelVar, sig os.Signal, normal, verbose Level) (stop func())

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

func FCScope(ctx context.Context) context.Context

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

func RedirectStdLog(l *Logger, level Level) func()

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

func NewAuditSink(w io.Writer, enc Encoder) *AuditSink

NewAuditSink wraps w. The encoder must be deterministic (the built-in JSONEncoder is — fields are emitted in slice order).

func (*AuditSink) Close

func (a *AuditSink) Close() error

Close implements Sink.

func (*AuditSink) Emit

func (a *AuditSink) Emit(r *Record) error

Emit implements Sink.

func (*AuditSink) Sync

func (a *AuditSink) Sync() error

Sync implements Sink.

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).

func (*ConfigWatcher) Stop

func (w *ConfigWatcher) Stop()

Stop ends the watcher goroutine.

type ConsoleEncoder

type ConsoleEncoder struct {
	Color      bool
	TimeFormat string // default "15:04:05.000"
}

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.

func (ConsoleEncoder) Name

func (ConsoleEncoder) Name() string

Name 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.

func (*DedupProcessor) Process

func (d *DedupProcessor) Process(_ context.Context, r *Record) error

Process implements Processor.

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.

func (*EnrichProcessor) Process

func (e *EnrichProcessor) Process(ctx context.Context, r *Record) error

Process implements Processor.

type Fanout

type Fanout struct {
	OnError func(Sink, error)
	// contains filtered or unexported fields
}

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.

func NewFanout

func NewFanout(sinks ...Sink) *Fanout

NewFanout groups sinks.

func (*Fanout) Close

func (f *Fanout) Close() error

Close implements Sink.

func (*Fanout) Emit

func (f *Fanout) Emit(r *Record) error

Emit implements Sink.

func (*Fanout) Sync

func (f *Fanout) Sync() error

Sync implements Sink.

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

func Any(key string, v any) Field

Any is the escape hatch: arbitrary value via reflection at encode time. Prefer a typed constructor on hot paths.

func Bool

func Bool(key string, v bool) Field

Bool adds a boolean field.

func Dur

func Dur(key string, d time.Duration) Field

Dur adds a time.Duration field.

func Err

func Err(err error) Field

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.

func Float

func Float[T float](key string, v T) Field

Float adds any float-typed value without boxing.

func Int

func Int[T integer](key string, v T) Field

Int adds any integer-typed value without boxing.

func NamedErr

func NamedErr(key string, err error) Field

NamedErr is Err with a caller-chosen key.

func String

func String(key, val string) Field

String adds a string field.

func Time

func Time(key string, t time.Time) Field

Time adds a time.Time field (stored as UnixNano).

func (Field) Value

func (f Field) Value() any

Value boxes the field value into an interface. This is the public accessor for adapter/sink authors outside this package (encoders use the unboxed accessors below on the hot path). nil-error fields return a nil any.

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
}

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).

func (*FingersCrossed) Emit

func (f *FingersCrossed) Emit(r *Record) error

Emit implements Sink.

func (*FingersCrossed) Sync

func (f *FingersCrossed) Sync() error

Sync implements Sink.

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.

func (JSONEncoder) Name

func (JSONEncoder) Name() string

Name 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

func (l Level) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler (lowercase band name).

func (Level) SeverityNumber

func (l Level) SeverityNumber() int

SeverityNumber returns the raw OTEL SeverityNumber.

func (Level) String

func (l Level) String() string

SeverityText returns the OTEL severity band name for the level.

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.

func NewLevelVar

func NewLevelVar(l Level) *LevelVar

NewLevelVar returns a LevelVar set to l.

func (*LevelVar) Level

func (lv *LevelVar) Level() Level

Level implements Leveler.

func (*LevelVar) Set

func (lv *LevelVar) Set(l Level)

Set atomically changes the minimum level.

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.

func (LogfmtEncoder) Name

func (LogfmtEncoder) Name() string

Name 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 New

func New(opts ...Option) *Logger

New builds a Logger. With no options it logs JSON at Info to stderr inline.

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

func Test(w interface{ Write([]byte) (int, error) }) *Logger

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) Close

func (l *Logger) Close() error

Close drains async transports and closes sinks.

func (*Logger) Debug

func (l *Logger) Debug(msg string, f ...Field)

func (*Logger) DebugContext

func (l *Logger) DebugContext(ctx context.Context, msg string, f ...Field)

func (*Logger) Debugt

func (l *Logger) Debugt(tmpl string, args ...any)

func (*Logger) Enabled

func (l *Logger) Enabled(level Level) bool

Enabled reports whether a record at level l would be emitted. Call this to guard expensive field construction.

func (*Logger) Error

func (l *Logger) Error(msg string, f ...Field)

func (*Logger) ErrorContext

func (l *Logger) ErrorContext(ctx context.Context, msg string, f ...Field)

func (*Logger) Errort

func (l *Logger) Errort(tmpl string, args ...any)

func (*Logger) Event

func (l *Logger) Event(name string, f ...Field)

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) EventAt

func (l *Logger) EventAt(ctx context.Context, level Level, name string, f ...Field)

EventAt logs a named event at an explicit level with context.

func (*Logger) Go

func (l *Logger) Go(ctx context.Context, fn func())

Go runs fn in a goroutine whose panics are logged (ERROR) instead of crashing the process — a safe-goroutine primitive.

func (*Logger) Handler

func (l *Logger) Handler() slog.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) Info

func (l *Logger) Info(msg string, f ...Field)

func (*Logger) InfoContext

func (l *Logger) InfoContext(ctx context.Context, msg string, f ...Field)

func (*Logger) Infot

func (l *Logger) Infot(tmpl string, args ...any)
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
}

func (*Logger) Log

func (l *Logger) Log(ctx context.Context, level Level, msg string, f ...Field)

Log emits at an arbitrary (possibly custom) level.

func (*Logger) Logt

func (l *Logger) Logt(ctx context.Context, level Level, tmpl string, args ...any)

Logt emits a templated record at an explicit level with context.

func (*Logger) Metrics

func (l *Logger) Metrics() *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

func (l *Logger) NewSlog() *slog.Logger

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")
}

func (*Logger) Recover

func (l *Logger) Recover(ctx context.Context)

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

func (l *Logger) RecoverAndContinue(ctx context.Context)

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
}

func (*Logger) StdLogWriter

func (l *Logger) StdLogWriter(level Level) *stdWriter

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

func (l *Logger) StdLogger(level Level) *log.Logger

StdLogger returns a *log.Logger that writes through this logger — a drop-in for code that takes a *log.Logger.

func (*Logger) Sync

func (l *Logger) Sync() error

Sync flushes buffered records (call before exit).

func (*Logger) Trace

func (l *Logger) Trace(msg string, f ...Field)

func (*Logger) TraceContext

func (l *Logger) TraceContext(ctx context.Context, msg string, f ...Field)

func (*Logger) Warn

func (l *Logger) Warn(msg string, f ...Field)

func (*Logger) WarnContext

func (l *Logger) WarnContext(ctx context.Context, msg string, f ...Field)

func (*Logger) Warnt

func (l *Logger) Warnt(tmpl string, args ...any)

func (*Logger) With

func (l *Logger) With(fields ...Field) *Logger

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
}

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() }

func (*Metrics) ServeHTTP

func (m *Metrics) ServeHTTP(w http.ResponseWriter, _ *http.Request)

ServeHTTP exposes the snapshot as JSON for an admin/metrics endpoint.

func (*Metrics) Snapshot

func (m *Metrics) Snapshot() Snapshot

Snapshot reads the current counters.

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

func NewTCPSink(addr string, enc Encoder, minLevel Level) *NetSink

NewTCPSink streams to a TCP endpoint.

func NewTLSSink

func NewTLSSink(addr string, cfg *tls.Config, enc Encoder, minLevel Level) *NetSink

NewTLSSink streams over TLS-wrapped TCP.

func NewUDPSink

func NewUDPSink(addr string, enc Encoder, minLevel Level) *NetSink

NewUDPSink streams to a UDP endpoint (fire-and-forget).

func (*NetSink) Close

func (s *NetSink) Close() error

Close closes the connection.

func (*NetSink) Dropped

func (s *NetSink) Dropped() uint64

Dropped reports records lost to network failures (never silent).

func (*NetSink) Emit

func (s *NetSink) Emit(r *Record) error

Emit implements Sink.

func (*NetSink) Sync

func (s *NetSink) Sync() error

Sync is a no-op (sockets are unbuffered here).

type Option

type Option func(*config)

Option configures a Logger at construction.

func WithCaller

func WithCaller(skip int) Option

WithCaller enables file:line capture (lazily resolved by encoders).

func WithLevel

func WithLevel(l Level) Option

WithLevel sets a constant minimum level.

func WithLeveler

func WithLeveler(lv Leveler) Option

WithLeveler sets a dynamic level source (e.g. *LevelVar) for runtime control.

func WithProcessors

func WithProcessors(ps ...Processor) Option

WithProcessors sets the pipeline (enrich/redact/sample/...). Order matters.

func WithSink

func WithSink(s Sink) Option

WithSink sets the terminal destination, delivered inline (SyncTransport). Use WithTransport for async.

func WithTransport

func WithTransport(t Transport) Option

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]
}

func (*PathRedactor) Process

func (pr *PathRedactor) Process(_ context.Context, r *Record) error

Process implements Processor.

type Processor

type Processor interface {
	Process(ctx context.Context, r *Record) error
}

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

type ProcessorFunc func(ctx context.Context, r *Record) error

ProcessorFunc adapts a function to Processor.

func (ProcessorFunc) Process

func (f ProcessorFunc) Process(ctx context.Context, r *Record) error

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).

func (*Record) AddField

func (r *Record) AddField(f Field)

AddField appends a field in place (used by enrich processors).

func (*Record) Clone

func (r *Record) Clone() *Record

Clone returns a heap copy safe to retain after the pipeline returns. The async transports call this before handing a record to a worker goroutine.

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.

func (*RedactProcessor) Process

func (p *RedactProcessor) Process(_ context.Context, r *Record) error

Process implements Processor.

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) Close

func (r *RotatingFile) Close() error

Close closes the active file.

func (*RotatingFile) Reopen

func (r *RotatingFile) Reopen() error

Reopen closes and reopens the active file — for SIGHUP / logrotate (copytruncate-free) external rotation.

func (*RotatingFile) Sync

func (r *RotatingFile) Sync() error

Sync flushes to disk.

func (*RotatingFile) Write

func (r *RotatingFile) Write(p []byte) (int, error)

Write implements io.Writer; rotates first if the line would exceed the cap.

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.

func (*SampleProcessor) Process

func (p *SampleProcessor) Process(_ context.Context, r *Record) error

Process implements Processor.

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

type Source struct {
	File string
	Line int
	Func string
}

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.

func (*Span) Fail

func (s *Span) Fail(err error)

Fail marks the span as failed; End will emit at ERROR with the error.

func (*Span) SetLevel

func (s *Span) SetLevel(l Level)

SetLevel overrides the level of the span.end record (default Info / Error if Fail was called).

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) Close

func (s *SyslogSink) Close() error

Close closes the syslog connection.

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

type TraceExtractor func(ctx context.Context) (traceID, spanID string, ok bool)

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) Close

func (s *WriterSink) Close() error

Close implements Sink.

func (*WriterSink) Emit

func (s *WriterSink) Emit(r *Record) error

Emit implements Sink.

func (*WriterSink) Sync

func (s *WriterSink) Sync() error

Sync implements Sink (best-effort flush for Syncer writers).

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL