Documentation
¶
Overview ¶
Package loggerj provides an ultra-high-performance, asynchronous, and lock-free logging facility designed for high-throughput Go services. It offers zero heap allocations in the hot path, atomic rate limiting, log rotation, and structured fields in both text and JSON formats.
Architecture: Pre-Compiled Execution Profiles ¶
Unlike traditional loggers that use mutexes and maps for rate limiting and sampling in the hot path, loggerj uses a "Pre-compiled Execution Profile" architecture.
- Cold Path (Init-time): You register log types using RegisterSub(). This pre-bakes JSON/Text prefixes into []byte and initializes lock-free atomic counters for rate limiting and sampling. Profiles are stored in an immutable copy-on-write registry accessed via atomic.Pointer, ensuring zero interface boxing and zero map lookups in the hot path.
- Hot Path (Log-time): The Log() method performs ZERO map lookups, ZERO mutex locks, and ZERO heap allocations. It uses atomic.CompareAndSwap (CAS) for rate limiting and atomic.Add for sampling. String-to-byte conversion uses unsafe zero-copy (Go 1.21+). Timestamps are deferred to the worker goroutine, removing a vDSO syscall from the hot path.
- Worker: A dedicated goroutine formats entries, injects pre-baked []byte prefixes, and writes to the underlying io.Writer via bufio. Flush() drains the channel before writing, guaranteeing no log loss on explicit flush.
This design ensures that logging never blocks the caller, eliminates GC pressure in the hot path, and scales linearly with CPU cores without lock contention.
Quick Start ¶
logger := loggerj.NewLogger(loggerj.Config{
JSONOutput: true,
FlushTimeout: 50 * time.Millisecond,
})
// COLD PATH: Register profiles once at startup
logger.RegisterSub("HTTP",
loggerj.WithRateLimit(1000, time.Second),
loggerj.WithFields("env", "prod", "service", "gateway"),
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go logger.Start(ctx)
defer logger.Close()
// HOT PATH: Ultra-fast, zero-allocation logging
logger.InfoString("HTTP", "request received", "method", "GET", "path", "/api")
logger.ErrorString("DB", "connection failed", "host", "localhost", "err", "timeout")
// Context-aware logging (opt-in, zero cost when unused)
ctx = context.WithValue(ctx, loggerj.TraceIDKey, "abc-123")
logger.InfoCtx(ctx, "HTTP", "traced request", "method", "POST")
# Performance See BENCH.md for detailed benchmark results and methodology.
Index ¶
- Constants
- type Config
- type DurabilityTier
- type Entry
- type Field
- func Bool(key string, val bool) Field
- func Dur(key string, val time.Duration) Field
- func Err(err error) Field
- func ErrWithKey(key string, err error) Field
- func Float64(key string, val float64) Field
- func Int(key string, val int) Field
- func Int64(key string, val int64) Field
- func Str(key, val string) Field
- func Uint64(key string, val uint64) Field
- type FieldType
- type Level
- type Logger
- func (l *Logger) AsWriter(level Level, logType string) io.Writer
- func (l *Logger) Close() error
- func (l *Logger) Debug(logType string, msg []byte, fields ...string)
- func (l *Logger) DebugCtx(ctx context.Context, logType string, msg string, fields ...string)
- func (l *Logger) DebugFields(logType string, msg []byte, fields ...Field)
- func (l *Logger) DebugFieldsString(logType string, msg string, fields ...Field)
- func (l *Logger) DebugString(logType string, msg string, fields ...string)
- func (l *Logger) Drops() uint64
- func (l *Logger) Error(logType string, msg []byte, fields ...string)
- func (l *Logger) ErrorCtx(ctx context.Context, logType string, msg string, fields ...string)
- func (l *Logger) ErrorFields(logType string, msg []byte, fields ...Field)
- func (l *Logger) ErrorFieldsString(logType string, msg string, fields ...Field)
- func (l *Logger) ErrorString(logType string, msg string, fields ...string)
- func (l *Logger) Flush()
- func (l *Logger) GetLevel() Level
- func (l *Logger) Info(logType string, msg []byte, fields ...string)
- func (l *Logger) InfoCtx(ctx context.Context, logType string, msg string, fields ...string)
- func (l *Logger) InfoFields(logType string, msg []byte, fields ...Field)
- func (l *Logger) InfoFieldsString(logType string, msg string, fields ...Field)
- func (l *Logger) InfoString(logType string, msg string, fields ...string)
- func (l *Logger) Log(level Level, logType string, msg []byte, fields ...string)
- func (l *Logger) LogFields(level Level, logType string, msg []byte, fields ...Field)
- func (l *Logger) RegisterSub(logType string, opts ...SubOption)
- func (l *Logger) ResetDrops()
- func (l *Logger) SetLevelValue(level Level)
- func (l *Logger) SetOnDrop(fn func(dropped uint64))
- func (l *Logger) Start(ctx context.Context)
- func (l *Logger) StartWithWriter(ctx context.Context, w io.Writer)
- func (l *Logger) Stats() Stats
- func (l *Logger) Warn(logType string, msg []byte, fields ...string)
- func (l *Logger) WarnCtx(ctx context.Context, logType string, msg string, fields ...string)
- func (l *Logger) WarnFields(logType string, msg []byte, fields ...Field)
- func (l *Logger) WarnFieldsString(logType string, msg string, fields ...Field)
- func (l *Logger) WarnString(logType string, msg string, fields ...string)
- type SlogHandler
- type Stats
- type StdLogWriter
- type SubOption
- type SubProfile
Constants ¶
const ( // TraceIDKey is the context key for distributed trace identifiers. TraceIDKey contextKey = iota // RequestIDKey is the context key for request identifiers. RequestIDKey // SpanIDKey is the context key for span identifiers. SpanIDKey )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// JSONOutput controls the output format. If true, logs are formatted as JSON.
// If false, logs are formatted as human-readable text. Default: false
JSONOutput bool
// FlushTimeout is the interval at which the worker flushes buffered logs.
// Shorter timeouts reduce latency but increase I/O operations. Default: 50ms
FlushTimeout time.Duration
// ChannelSize is the capacity of the internal log channel. Larger values
// provide more buffering for burst traffic. If full, entries are dropped.
// Default: 4096
ChannelSize int
// WorkerBufferSize is the initial capacity of the worker's format buffer.
// Minimum: 256. Default: 4096
WorkerBufferSize int
// FlushThreshold is the byte count at which the worker flushes the
// format buffer to the underlying writer. Should be <= WorkerBufferSize.
// Minimum: 256. Default: 4096
FlushThreshold int
// WriterBufferSize is the size of the bufio.Writer buffer used for I/O.
// Minimum: 512. Default: 8192
WriterBufferSize int
// RateLimitWindow is the default time window for rate limiting, in seconds.
// Used if a SubProfile doesn't specify its own window via WithRateLimit.
// Minimum: 1. Default: 1
RateLimitWindow int64
// IncludeCaller adds file:line information to each log entry.
// WARNING: Adds ~460ns overhead and 2 allocations per log entry.
// Should be disabled in production for maximum performance. Default: false
IncludeCaller bool
// OutputFile is the path to the log file. If empty, logs are written to stderr.
OutputFile string
// MaxFileSize is the maximum size of the log file before rotation.
// If 0, rotation is disabled. Default: 0
MaxFileSize int64
// MaxBackupFiles is the maximum number of rotated log files to keep.
// Only effective if MaxFileSize > 0. Default: 0
MaxBackupFiles int
// SyncMode bypasses the async channel/worker pipeline and writes each log
// entry directly to the underlying writer with a single atomic write()
// syscall. When SyncMode is true:
//
// - The log channel and worker goroutine are NOT created.
// - Start()/StartWithWriter() become no-ops.
// - Flush() becomes a no-op (writes are already synchronous).
// - Each log call performs: format → write() → return.
//
// The file is opened with O_APPEND, which the POSIX kernel guarantees
// to be atomic for writes up to PIPE_BUF (typically 4096-65536 bytes).
// This means concurrent goroutines writing to the same file will NOT
// interleave their log lines — no mutex is needed.
//
// This mode is FIXED at creation time and cannot be toggled at runtime.
// Use for audit trails, financial logs, or any scenario requiring
// per-log write guarantees. Target: <400ns/op, ≤1 alloc/op.
//
// Default: false (async mode)
SyncMode bool
// DurabilityTier controls the sync-mode durability guarantee.
// Only effective when SyncMode is true. Default: OSBuffered
DurabilityTier DurabilityTier
// FsyncEveryNCount is the number of writes before calling fsync(2).
// Only effective when DurabilityTier is FsyncEveryN. Default: 100
FsyncEveryNCount int
}
Config holds the configuration for a Logger instance. All fields have sensible defaults and can be left at their zero values.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a Config with sensible defaults for production use.
type DurabilityTier ¶ added in v1.4.0
type DurabilityTier uint8
DurabilityTier controls what guarantee SyncMode provides beyond the write(2) syscall itself. Higher tiers trade latency for a stronger promise about what survives a crash.
const ( // OSBuffered (default): uses bufio.Writer with periodic flush (every // 10ms or 100 logs, whichever comes first). Each write is a buffer // copy (~5ns), not a syscall. Survives process crash. Does NOT survive // OS crash or power loss — data may still be in the page cache. // // This is the guarantee zap and zerolog provide, though neither states // it explicitly; loggerj states it here on purpose. // // Throughput: ~300ns/op (competitive with zerolog/zap sync mode). OSBuffered DurabilityTier = iota // Direct: one write(2) syscall per log entry, no buffering. Relies on // O_APPEND atomicity for concurrent safety. Survives process crash. // Does NOT survive OS crash or power loss. // // Throughput: ~1550ns/op (syscall overhead dominates). Direct // FsyncEveryN: calls fsync(2) after every N writes (configured via // Config.FsyncEveryNCount). Survives OS crash / power loss for // committed entries, at the cost of fsync latency (typically 1-10ms // on spinning disk, less on SSD/NVMe). // // Throughput: ~5000ns/op (fsync latency amortized over N logs). FsyncEveryN // FsyncEveryWrite: calls fsync(2) after every single write. Maximum // durability, minimum throughput. Intended for audit trails, not // high-volume application logs. // // Throughput: ~5000-10000ns/op (fsync on every log). FsyncEveryWrite )
func (DurabilityTier) String ¶ added in v1.4.0
func (d DurabilityTier) String() string
String returns the human-readable name of the durability tier.
type Entry ¶
type Entry struct {
Level Level
Type string
Msg []byte
File string
Line int
Fields []string // legacy string-field API (Log, InfoString, ...)
// FieldsV holds typed fields from the Field API (LogFields,
// InfoFields, ...). An Entry uses exactly one of Fields or FieldsV
// per log call, never both — the formatter checks FieldsV first.
FieldsV []Field
Profile *SubProfile
}
Entry represents a single log record. Entries are pooled using sync.Pool to minimize allocations. The Reset method clears all fields for reuse.
Timestamps are not stored in the Entry; they are captured by the worker goroutine at format time, removing a vDSO syscall from the hot path.
type Field ¶ added in v1.4.0
type Field struct {
Key string
Type FieldType
Num uint64 // holds Int64/Uint64/Float64(bits)/Duration(ns)/Bool(0-1)
Str string // holds String value, or Error.Error() text
}
Field is a single structured log attribute. It carries its value in one of the untyped union members below instead of interface{}, so building a Field never allocates — the same guarantee zap.Field provides.
Size: 48 bytes (string key + uint8 type + uint64 num + string val). Passed by value — no pointer indirection, no heap escape.
func Dur ¶ added in v1.4.0
Dur constructs a duration field. Zero allocation — stored as nanoseconds.
func Err ¶ added in v1.4.0
Err constructs an error field with key "error". Returns a zero-value Field (skipped by the encoder) if err is nil — mirrors zap.Error's nil-safety. Zero allocation when err is nil.
func ErrWithKey ¶ added in v1.4.0
ErrWithKey constructs an error field with a custom key. Returns a zero-value Field (skipped by the encoder) if err is nil.
func Float64 ¶ added in v1.4.0
Float64 constructs a float64 field. Zero allocation — bits are stored in Num via math.Float64bits.
func Int ¶ added in v1.4.0
Int constructs an int field. Zero allocation — the value is stored directly in the Num union member, no boxing.
type FieldType ¶ added in v1.4.0
type FieldType uint8
FieldType identifies which union member of Field is populated, avoiding interface{} boxing on the hot path.
const ( StringType FieldType = iota // Str field: value in Str Int64Type // Int/Int64: value in Num (as int64 bits) Uint64Type // Uint64: value in Num Float64Type // Float64: value in Num (as float64 bits) BoolType // Bool: value in Num (0 or 1) DurationType // Dur: value in Num (nanoseconds) ErrorType // Err: value in Str (err.Error() text) )
type Level ¶
type Level uint8
Level represents the severity of a log entry. Lower values indicate more verbose logging. The logger filters entries below the configured threshold.
const ( // LevelDebug is the most verbose level, used for detailed debugging information. LevelDebug Level = 0 // LevelInfo is the default level, used for general operational messages. LevelInfo Level = 1 // LevelWarn indicates potential issues that should be monitored. LevelWarn Level = 2 // LevelError indicates serious problems that require immediate attention. LevelError Level = 3 )
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger is the main logging instance. It provides asynchronous, high-throughput, lock-free logging with zero heap allocations in the hot path.
func NewLogger ¶
NewLogger creates a new Logger instance. The logger is not started; call Start or StartWithWriter to begin processing.
func (*Logger) AsWriter ¶
AsWriter returns an io.Writer that routes all writes to the logger at the specified level and logType.
Usage with the standard library log package:
log.SetFlags(0) // Disable std log timestamps; loggerj adds its own log.SetOutput(logger.AsWriter(loggerj.LevelInfo, "STDLIB"))
Zero-allocation guarantee: The Write method uses bytes.TrimRight (not strings.TrimRight) to remove trailing newlines without allocation. The underlying Log() method immediately copies msg via append(e.Msg[:0], msg...), satisfying the io.Writer contract (not retaining p after Write returns).
This adapter is suitable for intercepting legacy or third-party logs that use the standard library log package. For the application's primary high-throughput path, prefer the native logger.InfoString or logger.Info methods to avoid the function call overhead of the io.Writer interface.
func (*Logger) Close ¶
Close releases resources held by the logger. In async mode: flushes the buffered writer and closes the log file. In sync mode: flushes the shared buffered writer and closes the sync file.
func (*Logger) DebugCtx ¶ added in v1.1.0
DebugCtx logs a message at LevelDebug with optional context extraction. If ctx is nil or contains no known keys, it behaves exactly like DebugString with zero additional cost.
func (*Logger) DebugFields ¶ added in v1.4.0
DebugFields logs a message at LevelDebug with typed fields. Caller skip is 2.
func (*Logger) DebugFieldsString ¶ added in v1.4.0
DebugFieldsString logs a string message at LevelDebug with typed fields.
func (*Logger) DebugString ¶
DebugString logs a string message at LevelDebug with zero-copy string-to-byte conversion. Caller skip is 2.
func (*Logger) Drops ¶
Drops returns the total number of log entries dropped because the internal channel was full.
func (*Logger) ErrorCtx ¶ added in v1.1.0
ErrorCtx logs a message at LevelError with optional context extraction. If ctx is nil or contains no known keys, it behaves exactly like ErrorString with zero additional cost.
func (*Logger) ErrorFields ¶ added in v1.4.0
ErrorFields logs a message at LevelError with typed fields. Caller skip is 2.
func (*Logger) ErrorFieldsString ¶ added in v1.4.0
ErrorFieldsString logs a string message at LevelError with typed fields.
func (*Logger) ErrorString ¶
ErrorString logs a string message at LevelError with zero-copy string-to-byte conversion. Caller skip is 2.
func (*Logger) Flush ¶
func (l *Logger) Flush()
Flush forces an immediate flush of all pending log entries. It signals the worker to drain the channel and write the buffer, then blocks until the worker confirms completion (or times out after 1 second).
If no worker is running, Flush drains and discards all pending entries to prevent channel blockage. This also fixes the 1-second block that occurred when OutputFile was set but Start() hadn't been called yet.
func (*Logger) InfoCtx ¶ added in v1.1.0
InfoCtx logs a message at LevelInfo with optional context extraction. If ctx is nil or contains no known keys, it behaves exactly like InfoString with zero additional cost.
func (*Logger) InfoFields ¶ added in v1.4.0
InfoFields logs a message at LevelInfo with typed fields. Caller skip is 2.
func (*Logger) InfoFieldsString ¶ added in v1.4.0
InfoFieldsString logs a string message at LevelInfo with typed fields.
func (*Logger) InfoString ¶
InfoString logs a string message at LevelInfo with zero-copy string-to-byte conversion. Caller skip is 2.
func (*Logger) LogFields ¶ added in v1.4.0
LogFields logs a message with typed fields at the given level. This is the zero-allocation alternative to Log() with string fields. Caller skip is 1.
func (*Logger) RegisterSub ¶
RegisterSub registers a SubProfile for a specific logType.
COLD PATH: Call this during application initialization, NOT inside HTTP handlers or hot loops. The registry uses a copy-on-write strategy: a new immutable snapshot is created and swapped atomically, so concurrent hot-path reads are never blocked.
func (*Logger) ResetDrops ¶
func (l *Logger) ResetDrops()
ResetDrops resets the drop counter to zero.
func (*Logger) SetLevelValue ¶
SetLevelValue sets the current log level threshold atomically. Entries below this level are discarded in ~2ns with zero allocations.
func (*Logger) SetOnDrop ¶ added in v1.1.0
SetOnDrop registers a callback invoked whenever a log entry is dropped due to a full channel. Thread-safe; can be called concurrently with logging. Pass nil to unregister the callback.
func (*Logger) Start ¶
Start begins the worker goroutine that processes log entries. It writes to the configured OutputFile or stderr.
func (*Logger) StartWithWriter ¶
StartWithWriter begins the worker goroutine with a custom io.Writer.
Lifecycle synchronization:
- The started channel is closed (via sync.Once) when the worker begins, allowing callers to wait for readiness without polling or sleeping.
- The workerDone channel is closed when the worker exits (after draining remaining entries on context cancellation).
The Flush() method drains all pending channel entries before writing, guaranteeing no log loss on explicit flush.
func (*Logger) WarnCtx ¶ added in v1.1.0
WarnCtx logs a message at LevelWarn with optional context extraction. If ctx is nil or contains no known keys, it behaves exactly like WarnString with zero additional cost.
func (*Logger) WarnFields ¶ added in v1.4.0
WarnFields logs a message at LevelWarn with typed fields. Caller skip is 2.
func (*Logger) WarnFieldsString ¶ added in v1.4.0
WarnFieldsString logs a string message at LevelWarn with typed fields.
type SlogHandler ¶ added in v1.4.0
type SlogHandler struct {
// contains filtered or unexported fields
}
----------------------------------------------------------------------------- slog.Handler Adapter (Go 1.21+ Ecosystem Integration) -----------------------------------------------------------------------------
SlogHandler implements slog.Handler, routing all slog calls through loggerj's zero-allocation typed-field pipeline.
Group handling follows the loggerj Field model: nested groups are flattened to dotted keys. Example:
slog.Group("http", "method", "GET")
becomes:
"http.method":"GET"
This keeps the Field API allocation-free and works well with flat log pipelines such as Loki, Elasticsearch, and Datadog.
func NewSlogHandler ¶ added in v1.4.0
func NewSlogHandler(l *Logger, logType string) *SlogHandler
NewSlogHandler returns a slog.Handler that routes all records into the given Logger under the specified logType.
type Stats ¶ added in v1.2.0
type Stats struct {
Drops uint64
ChannelSize uint64
ChannelCap uint64
SyncWriteErrors uint64 // Non-zero means at least one sync-mode write failed
RotationErrors uint64 // Non-zero means at least one rotation step failed
}
Stats represents a snapshot of logger statistics. Using a struct instead of a map avoids heap allocations on every call, which is important for observability loops (e.g., Prometheus exporters) that poll Stats() frequently.
type StdLogWriter ¶
type StdLogWriter struct {
// contains filtered or unexported fields
}
StdLogWriter wraps a Logger to implement the io.Writer interface. This allows the standard library "log" package (and third-party libraries that rely on it) to route their output through loggerj's async pipeline.
func (*StdLogWriter) Write ¶
func (w *StdLogWriter) Write(p []byte) (int, error)
Write implements the io.Writer interface. Trailing newlines from std log are trimmed for cleaner loggerj output. Uses bytes.TrimRight to avoid the string(p) allocation that occurred with strings.TrimRight.
Zero-allocation guarantee: The Log() method immediately copies msg via append(e.Msg[:0], msg...), so the io.Writer contract (not retaining p after Write returns) is satisfied. This eliminates the only documented allocation in the AsWriter adapter path.
type SubOption ¶
type SubOption func(*SubProfile)
SubOption configures a SubProfile during RegisterSub.
func WithFields ¶
WithFields adds static key-value pairs that will be pre-baked into the JSON/Text prefixes. This avoids formatting these fields in the hot path.
func WithRateLimit ¶
WithRateLimit sets a lock-free rate limit for this specific logType.
limit is the max logs per window. window is the duration, for example time.Second or 500 * time.Millisecond. Sub-second windows are supported.
GATE ORDER: Rate limiting is applied AFTER sampling. If you configure both WithSampleRate(10) and WithRateLimit(100), the rate limit applies to the *sampled* stream, not the raw input stream.
Example: 500 logs/s input, WithSampleRate(10), WithRateLimit(100):
- Sampling: 500/10 = 50 logs pass
- Rate limit: 50 < 100, so all 50 pass Result: ~50 logs/s output (not 100)
This design preserves hot-path performance by avoiding time.Now() syscalls (~34ns) on entries that would be discarded by sampling anyway.
If you need "max N raw attempts per second", do not combine sampling with rate limiting. Use rate limiting alone.
Exact counting supports limits up to rlMaxExactLimit (16,777,215). Larger limits are capped to that value.
func WithSampleRate ¶
WithSampleRate enables statistical sampling for this logType.
Only 1 out of every `rate` logs will be emitted. For example, WithSampleRate(10) means approximately 10% of logs pass through. Useful for high-volume debug traces where statistical representation is sufficient.
GATE ORDER: Sampling is applied BEFORE rate limiting. This is a performance optimization: sampling uses atomic.Add (~1ns), while rate limiting uses time.Now() (~34ns) + CAS (~2.5ns). By sampling first, we avoid the expensive syscall on the majority of logs that would be discarded anyway.
If you need rate limiting on the raw input stream (not the sampled subset), do not combine sampling with rate limiting. Use rate limiting alone.
The sampling counter is atomic and lock-free, supporting concurrent logging.
type SubProfile ¶
type SubProfile struct {
Name string
// contains filtered or unexported fields
}
SubProfile represents a pre-compiled execution profile for a specific logType. Unlike traditional loggers that use mutexes and maps in the hot path, SubProfile holds lock-free atomic counters for rate limiting/sampling and pre-baked []byte prefixes for zero-CPU formatting.