yaklog

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Mar 12, 2026 License: MIT Imports: 23 Imported by: 0

README

yaklog

Go Version Go Reference Go Report Card Lint Test Fuzz

English | 中文

High-performance JSON logging library for Go.

  • Zero-allocation hot path: Uses bufpool pre-allocated buffers + sync.Pool; heap allocations on the normal log path are zero.
  • Sync / Async dual paths: Send() writes synchronously in the caller's goroutine; Post() dispatches to a package-level worker for asynchronous batched writes.
  • Fluent API: Two-level fluent chain (Logger + Event), no reflection, type-safe.
  • JSON / Text dual format: Automatically switches to human-readable text when the target is Console; all other targets default to JSON.
  • Pluggable samplers: RateSampler (token bucket) / HashSampler (deterministic hash sampling).
  • adapter sub-package: Adapts *Logger to slog.Handler, taking over slog.Default() and stdlib log.
  • Hot-path safe: The library never uses panic for error handling internally; write failures are only counted.

Installation

go get github.com/uniyakcom/yaklog

Requirements: Go 1.25+.


Quick Start

import "github.com/uniyakcom/yaklog"

func main() {
    // Create a Logger with default config (output to os.Stderr, level Info, JSON format)
    l := yaklog.New()
    l.Info().Str("app", "demo").Msg("service starting").Send()

    // Custom Logger
    logger := yaklog.New(yaklog.Options{
        Level: yaklog.Debug,
        Out:   yaklog.Console(),   // Text format to os.Stderr
    })
    logger.Debug().Str("env", "dev").Send()

    // Wait for all async log writes before process exit
    yaklog.Wait()
}

Global Configuration

yaklog.Config sets package-level defaults and (lazily) initializes the global worker goroutine:

yaklog.Config(yaklog.Options{
    Level:          yaklog.Info,
    Out:            yaklog.Save("./logs/app.log"),
    FileMaxSize:    128,         // MB, rotate after exceeding
    FileMaxAge:     7,           // days
    FileMaxBackups: 5,
    FileCompress:   true,
    QueueLen:       8192,
    FlushInterval:  50 * time.Millisecond,
})

Config may be called multiple times, but QueueLen and FlushInterval only take effect on the first call (or the first New() call) — the internal worker is started exactly once via sync.Once. All other fields (Level, Out, Source, etc.) apply on every call and affect all subsequent zero-argument New() calls. Call it once at the very beginning of main for a clean initialization.


Options Fields

Field Type Default Description
Level Level Info Minimum output level
Out io.Writer os.Stderr Output target (Console() / Save(...) / Discard() / any io.Writer)
Source bool false Whether to append file name and line number to each log entry
CallerFunc func(file string, line int) string nil Custom formatter for the source field; receives the raw file path and line number, returns the string written to the log. Return "" to suppress the field entirely. Useful for filename-only (path.Base(file)), path sanitization, or relative-path trimming. Ignored when Source is false.
TimeFormat TimeFormat TimeRFC3339Milli Time format (see section below)
Sampler Sampler nil (all) Pluggable sampler
QueueLen int 4096 Async queue depth (only used by Post())
FlushInterval time.Duration 100ms Worker periodic flush interval
FilePath string "" Log file path; if Out is nil, automatically calls Save(FilePath). Accepts absolute or relative paths — relative paths are resolved to absolute at configuration time using the process working directory, so they will not drift even if os.Chdir is called later. Empty string falls back to os.Stderr.
FileMaxSize int 100 (MB) Maximum single-file size in MB
FileMaxAge int 0 (no limit) Backup file retention in days; 0 = never expire
FileMaxBackups int 0 (no limit) Maximum number of old files to keep; 0 = unlimited
FileCompress bool false Whether to gzip-compress rotated archives
FileLocalTime bool false Use local time in rotated file names (otherwise UTC)
ConsoleTimeFormat string "15:04:05.000" Time string for Console text format (time.Format layout)
ConsoleNoColor bool false true = disable ANSI color output
ConsoleLevelFull bool false true = full level name (INFO/WARN…); default single letter (I/W…)
ColorScheme ColorScheme zero value Custom ANSI color scheme; zero-value fields use built-in defaults; ignored when ConsoleNoColor: true

Console visual scheme

Element Color Notes
Timestamp dim gray (\x1b[2m) Low visual weight; recedes to background
Level letter / name Per-level color (see table above) Highest visual priority
Tag [name] Orange-256 (\x1b[38;5;166m) Immediately after level; set via logger.Tag("name"); omitted when not set
Field key= Dark blue (\x1b[34m) Includes =; muted, never conflicts with level colors
Field value Default (white) Stands out against the dim key
Err() value Inherits current level color Red for Error, yellow for Warn, etc.

Spacing rules: single-letter level → 1 space before first field; full-level names are padded to 5 chars (e.g. INFO ) for column alignment, then 1 space; 1 space between every field; 1 space before message. Disable all color with ConsoleNoColor: true.

Console output screenshot


ColorScheme

Per-logger ANSI color customization. Pass a ColorScheme value in Options.ColorScheme; any zero-value field falls back to the built-in default.

type ColorScheme struct {
    Trace  string // default: "\x1b[90m"  (dark gray)
    Debug  string // default: "\x1b[36m"  (cyan)
    Info   string // default: "\x1b[32m"  (green)
    Warn   string // default: "\x1b[33m"  (yellow)
    Error  string // default: "\x1b[31m"  (red)
    Panic  string // default: "\x1b[1;35m" (bold magenta)
    Fatal  string // default: "\x1b[1;31m" (bold red)
    Time   string // default: "\x1b[2m"   (dim)
    Key    string // default: "\x1b[34m"  (blue)
    Tag    string // default: "\x1b[38;5;166m"  (orange, 256-color)
    Source string // default: "\x1b[93m"  (bright yellow)
}

Solarized-dark palette example:

logger := yaklog.New(yaklog.Options{
    Out:   yaklog.Console(os.Stdout),
    Level: yaklog.Trace,
    ColorScheme: yaklog.ColorScheme{
        Trace:  "\x1b[38;5;244m",
        Debug:  "\x1b[38;5;37m",
        Info:   "\x1b[38;5;64m",
        Warn:   "\x1b[38;5;136m",
        Error:  "\x1b[38;5;160m",
        Panic:  "\x1b[38;5;125m",
        Fatal:  "\x1b[1;38;5;160m",
        Time:   "\x1b[38;5;240m",
        Key:    "\x1b[38;5;33m",
        Tag:    "\x1b[38;5;166m",
        Source: "\x1b[38;5;142m",
    },
})

ColorScheme has no effect when ConsoleNoColor: true or when using JSON output (Save/File*).


Time Formats

const (
    TimeRFC3339Milli TimeFormat = iota // "2006-01-02T15:04:05.000Z07:00" (default)
    TimeUnixSec                        // Unix second integer
    TimeUnixMilli                      // Unix millisecond integer
    TimeUnixNano                       // Unix nanosecond integer
    TimeOff                            // Omit time field
)

Log Levels

const (
    Trace Level = -2  // Lowest trace level
    Debug Level = -1
    Info  Level =  0  // Zero value, default minimum output level
    Warn  Level =  1
    Error Level =  2
    Panic Level =  3  // Writes log then calls PanicFunc (default: built-in panic); can be caught by defer/recover
    Fatal Level =  4  // Writes log + drains queue, then calls FatalFunc (default: os.Exit(1))
)
Level Value Behavior
Trace −2 Normal output
Debug −1 Normal output
Info 0 Normal output (default minimum level)
Warn 1 Normal output
Error 2 Normal output
Panic 3 Writes log synchronously then executes PanicFunc (default: built-in panic); recoverable via defer/recover
Fatal 4 Writes log synchronously + drains queue, then calls FatalFunc (default: os.Exit(1))

FatalFunc and PanicFunc Hooks

yaklog abstracts the Fatal exit action and Panic action into replaceable functions, primarily for capturing these behaviors in tests.

Hook Reference
Hook Default behavior Setter
FatalFunc os.Exit(1) yaklog.SetFatalFunc(fn)
PanicFunc panic(msg) yaklog.SetPanicFunc(fn)
Scenario 1: Capture Fatal in tests (without actually exiting)

Note: use Send() rather than Post() in tests. See the Notes section below for the reason.

func TestFatalLogsAndExits(t *testing.T) {
    var exitCode int
    var exitCalled bool

    // Save and restore
    old := yaklog.GetFatalFunc()
    defer yaklog.SetFatalFunc(old)
    yaklog.SetFatalFunc(func(code int) {
        exitCode = code
        exitCalled = true
        // Do NOT call os.Exit: prevents the test process from exiting
    })

    var buf bytes.Buffer
    l := yaklog.New(yaklog.Options{Out: &buf, Level: yaklog.Info})
    l.Fatal().Str("reason", "disk full").Msg("service crashed").Send()

    if !exitCalled {
        t.Fatal("expected Fatal to invoke FatalFunc")
    }
    if exitCode != 1 {
        t.Errorf("expected exit code 1, got %d", exitCode)
    }
    m := decodeJSON(t, strings.TrimSpace(buf.String()))
    if m["level"] != "FATAL" || m["msg"] != "service crashed" {
        t.Errorf("unexpected log content: %v", m)
    }
}
Scenario 2: Capture Panic in tests (without actually panicking)
func TestPanicLogsAndPanics(t *testing.T) {
    var panicMsg string

    old := yaklog.GetPanicFunc()
    defer yaklog.SetPanicFunc(old)
    yaklog.SetPanicFunc(func(msg string) {
        panicMsg = msg
        // Do NOT call panic: silently captures the message in test path
    })

    var buf bytes.Buffer
    l := yaklog.New(yaklog.Options{Out: &buf, Level: yaklog.Info})
    l.Panic().Str("op", "write").Msg("nil pointer").Send()

    if panicMsg != "nil pointer" {
        t.Errorf("expected panic message 'nil pointer', got %q", panicMsg)
    }
    // Log must be written before PanicFunc is called
    if !strings.Contains(buf.String(), "PANIC") {
        t.Error("log not written")
    }
}
Scenario 3: Custom exit cleanup in production
func main() {
    // Prepare metrics client
    metrics := initMetrics()

    // Replace FatalFunc: flush metrics before exiting
    yaklog.SetFatalFunc(func(code int) {
        // On Fatal crash, flush metrics to monitoring platform first
        metrics.Flush()
        os.Exit(code)
    })

    // ... business logic ...
}

Notes

  1. Queue drain for Post() + Fatal only happens when FatalFunc is os.Exit: Once a custom FatalFunc is set, Fatal.Post() does not wait for the queue to drain. If you need all async logs flushed before a custom exit, call yaklog.Wait() first, then trigger Fatal.
  2. Prefer Send() for Fatal-level logs: Guarantees the log content is written synchronously before FatalFunc is called.
  3. PanicFunc is expected not to return: In production, a custom PanicFunc should call built-in panic(msg) or runtime.Goexit(); if it returns normally, execution continues past the log call rather than crashing.

Output Targets

File safety checks — When the target path already exists, Save / FilePath performs three checks before opening the file:

  1. Must be a regular file (not a directory, device, socket, etc.); otherwise ErrNotLogFile is returned.
  2. Must not have any executable permission bits (mode & 0111); otherwise ErrNotLogFile is returned.
  3. The first 4 bytes must not match a known binary magic number (ELF \x7fELF, Mach-O, PE MZ); otherwise ErrNotLogFile is returned.

These checks prevent accidentally pointing a log path at an executable or binary, which would silently corrupt it.

// Text format, output to os.Stderr (no args = os.Stderr)
out := yaklog.Console()

// Text format, output to a custom io.Writer
out := yaklog.Console(myWriter)

// Multi-target output: combine with io.MultiWriter
out := yaklog.Console(io.MultiWriter(os.Stderr, myFile))

// File with rotation — accepts absolute or relative path.
// Relative paths are resolved to absolute at call time (process cwd).
out := yaklog.Save("./logs/app.log")

// Discard all output (testing / benchmarking scenarios)
out := yaklog.Discard()

Logger Fluent Modifications

All modification methods return a new *Logger; the original Logger is unaffected.

Label — Attach fixed fields
reqLog := logger.Label("service", "api").Label("version", 2)
reqLog.Info().Msg("request received").Send()
// Output: {"level":"INFO","service":"api","version":2,"msg":"request received"}

Label accepts values of type string / int / int64 / uint64 / float64 / bool / any.

Tag — Component label
// Attach a component label; rendered as [name] after the level indicator in Console
cacheLog := logger.Tag("cache")
cacheLog.Info().Str("op", "lookup").Msg("cache hit").Send()
// Console: 10:30:00.123 I [cache] op=lookup cache hit
// JSON:    {"level":"INFO","tag":"cache","op":"lookup","msg":"cache hit"}

Tag returns a new *Logger (shares level and wait group with the parent). In Console mode the label is rendered in orange-256 (\x1b[38;5;166m) immediately after the level indicator; in JSON mode it emits "tag":"name" as the field directly after level. Calling Tag with an empty string removes the label.

Fork — Independent level and wait group
// Derive a child Logger with independent level control (does not affect parent's level)
auditLog := logger.Fork()
auditLog.SetLevel(yaklog.Warn)
Context — Bind a context
// All subsequent events automatically read trace_id from ctx
l := logger.Context(ctx)
To — Switch output target
// Same Logger logic, output redirected to another io.Writer
fileLog := logger.To(yaklog.Save("./debug.log"))

To(nil) silently discards all writes and reports ErrWriterClosed via the OnWriteError callback.

SetLevel / GetLevel
logger.SetLevel(yaklog.Debug)
current := logger.GetLevel()
Wait — Wait for Logger-level async writes

logger.Wait() waits only for that Logger (and Label-derived child Loggers sharing the same wait group) to finish all pending Post() tasks. Unlike yaklog.Wait() (package-level global wait), it does not affect other Logger instances.

// Wait only for this Logger (and its Label children) Post() tasks
logger.Wait()

If you created independent child Loggers via Fork(), you need to call Wait() on each of them separately.


Event Builder Chain

Each log level method returns *Event; Msg() sets the message body and returns the same *Event; finally call Send() or Post() to terminate.

l.Info().
    Str("method", "GET").
    Str("path", "/api/users").
    Int("status", 200).
    Dur("latency", 3*time.Millisecond).
    Msg("HTTP request completed").
    Send()

Important: Msg() is NOT a terminator — you must follow it with Send() or Post() to trigger the write.

Supported Field Methods
Method Output Example
Str(key, val string) "key":"val"
Int(key string, val int) "key":42
Int64(key string, val int64) "key":-1
Uint64(key string, val uint64) "key":18446...
Float64(key string, val float64) "key":3.14
Bool(key string, val bool) "key":true
Time(key string, val time.Time) "key":"2026-01-02T..."
Dur(key string, val time.Duration) "key":"3m7s"
Err(err error) "error":"..." (skipped if nil)
AnErr(key string, err error) "key":"..." (skipped if nil)
Bytes(key string, val []byte) "key":"..." (zero-copy)
Any(key string, val any) "key":<JSON> (serialized via yakjson)
JSON(key string, raw []byte) "key":<raw> (raw bytes embedded as-is; zero-alloc, no reflect)
Stringer(key string, val fmt.Stringer) "key":"..."
Ctx(ctx context.Context) Overrides the Logger-bound ctx (for trace_id extraction)
Caller() Appends source=file:line; per-event alternative to global Source: true
Msg(msg string) Sets message body, returns *Event, not a terminator
Send vs Post
// Send: writes synchronously in the caller's goroutine; data is flushed when it returns
e.Send()

// Post: enqueues into the global worker queue for async write; returns immediately
// If the queue is full, the entry is dropped (counted in Dropped()); never blocks
e.Post()

Sync / Async Wait

Why you must call Wait()

Post() enqueues log buffers into the package-level worker queue and returns immediately. If the process exits before the worker finishes writing, any entries still in the queue are silently lost with no warning.

Consequences of exiting without calling Wait() (Post() path):

  • Remaining entries in the queue are permanently lost
  • This includes critical-level logs such as Error and Warn
  • Especially noticeable in load-testing or batch-processing scenarios

Recommended pattern:

func main() {
    yaklog.Config(yaklog.Options{...})
    defer yaklog.Wait() // Ensure all Post logs are flushed before process exit

    // ... business logic ...
    l.Info().Msg("service started").Post()
}
// Package-level Wait: waits for all async tasks submitted via Post() to finish writing
// Covers Post tasks from all Logger instances
yaklog.Wait()

// Logger-level Wait: waits only for this Logger instance (and Label children) Post() tasks
// Fork()-derived child Loggers have independent wait groups; wait on them separately
logger.Wait()

// Check total Post tasks dropped due to a full queue (for monitoring)
dropped := yaklog.Dropped()

// Check total write failure count (Send + Post paths, I/O error counter)
errs := yaklog.ErrCount()

Send() path does not need Wait(): Send() completes the write synchronously in the caller's goroutine; data is flushed by the time it returns and never enters the queue.

Concurrent Post() during Wait(): Any Post() call that races against Wait() is not guaranteed to be covered by that Wait() invocation. Ensure all producers have stopped submitting before calling Wait(); violating this is safe (no crash, no data corruption) but may leave some entries unflushed.

Wait vs Shutdown
Wait() Shutdown()
Drains queue
Accepts new Post() after call ❌ (returns ErrWriterClosed)
Suitable for Mid-program checkpoints, logger-level sync Process exit, final teardown
Reversible

Typical pattern: use defer yaklog.Wait() during normal operation; call yaklog.Shutdown() only if you want to permanently stop the async writer (e.g. in a graceful-shutdown hook where no further logging is expected).


Context Injection

TraceID
import "github.com/uniyakcom/yaklog"

// Inject a [16]byte TraceID into context
var traceID [16]byte
copy(traceID[:], someTraceBytes)
ctx = yaklog.WithTrace(ctx, traceID)

// Logger bound to ctx automatically appends "trace_id":"<32-hex>" to each entry
l := logger.Context(ctx)
l.Info().Msg("handling request").Send()
// Output: {"level":"INFO","trace_id":"0102...1f20","msg":"handling request"}
Attach arbitrary fields to context
ctx = yaklog.WithField(ctx, "request_id", "req-abc-123")
Store Logger in context
// Store Logger in ctx for cross-call-chain propagation
ctx = yaklog.WithLogger(ctx, logger)

// Retrieve Logger from ctx (returns global default Logger if not set)
l := yaklog.FromCtx(ctx)
l.Info().Msg("Logger retrieved from ctx").Send()
EventSink
// Register an event listener in ctx (for testing, metrics collection, etc.)
type mySink struct{}
func (s *mySink) Emit(level yaklog.Level, msg string, raw []byte) {
    // raw is the complete JSON / Text encoded bytes
}
ctx = yaklog.WithEventSink(ctx, &mySink{})

Samplers

RateSampler (token bucket)
// At most 100 entries per second, allowing 10 burst
sampler := yaklog.NewRateSampler(100, 10)
l := yaklog.New(yaklog.Options{Sampler: sampler})
HashSampler (deterministic hash sampling)
// Fixed 30% sampling (stable result for the same level+msg combination).
// Note: when used as Logger.Sampler, sampling is evaluated before Msg() is called,
// so msg is always "". Sampling is therefore effectively level-based in that context.
// For content-based sampling, call sampler.Sample() directly in application code.
sampler := yaklog.NewHashSampler(0.3)
l := yaklog.New(yaklog.Options{Sampler: sampler})

// Hot-update the sampling rate at runtime — atomic write, immediately visible to all goroutines.
sampler.SetRate(0.1) // change to 10% for all levels without rebuilding the Logger

// Per-level independent rates: e.g. keep 100% of Error logs but sample 5% of Debug.
sampler.SetRateForLevel(yaklog.Error, 1.0)
sampler.SetRateForLevel(yaklog.Warn,  1.0)
sampler.SetRateForLevel(yaklog.Info,  0.2)
sampler.SetRateForLevel(yaklog.Debug, 0.05)

SetRateForLevel allows each log level to carry its own sampling threshold — useful for high-traffic services that need full fidelity on errors while aggressively sampling debug output. Level-to-slot mapping is deterministic: uint8(level) & 7.

Implement the Sampler interface for custom samplers:

type Sampler interface {
    Sample(level Level, msg string) bool
}

adapter Sub-Package

Take over slog.Default()
import "github.com/uniyakcom/yaklog/adapter"

l := yaklog.New(yaklog.Options{Level: yaklog.Info})
adapter.SetDefault(l)

// All slog.Info / slog.Warn / slog.Error calls are now routed to l
slog.Info("via slog", "key", "val")

After calling l.SetLevel(yaklog.Debug) at runtime, call adapter.RefreshDefault() to propagate the new level into the default slog.Logger's Enabled check without rebuilding the handler:

l.SetLevel(yaklog.Debug)
adapter.RefreshDefault() // slog.Debug(...) now passes the Enabled gate
Take over stdlib log
import (
    "log"
    "log/slog"
    "github.com/uniyakcom/yaklog/adapter"
)

l := yaklog.New(yaklog.Options{Level: yaklog.Info})
log.SetOutput(adapter.ToStdLogWriter(l, slog.LevelWarn))
log.Print("legacy log message") // Routed to l at Warn level

Attribute handling notes:

  • slog.KindLogValuer attributes are resolved before encoding, preventing infinite-recursion risks from self-referencing valuers.
  • slog.Group attributes are flattened into "group.key" fields (up to 16 levels deep). Example: slog.Group("http", slog.String("method", "GET")) → field http.method.
  • WithGroup(name) is fully slog-spec compliant: fields emitted on a handler derived from WithGroup("req") carry the prefix req. (e.g. slog.String("id", "x")"req.id":"x"). Nested groups stack: WithGroup("a").WithGroup("b") prefixes all subsequent keys with a.b.. Passing an empty string is a no-op.

Rotation Sub-Package

Rotation is implemented by the rotation sub-package, automatically enabled via Save() or Options.FilePath:

yaklog.Config(yaklog.Options{
    Out:            yaklog.Save("./logs/app.log"),
    FileMaxSize:    64,    // Rotate after exceeding 64 MB
    FileMaxAge:     30,    // Retain for 30 days
    FileMaxBackups: 10,    // Keep at most 10 archives
    FileCompress:   true,  // gzip-compress old files
    FileLocalTime:  false, // Use UTC in file names
})

To use rotation directly (e.g., to guard against low disk space), construct a RotatingWriter with rotation.New:

import "github.com/uniyakcom/yaklog/rotation"

w, err := rotation.New(
    rotation.WithDir("/var/log/app"),
    rotation.WithFilename("app"),
    rotation.WithMaxSize(64 << 20),    // 64 MiB
    rotation.WithMinFreeBytes(200 << 20), // refuse open if < 200 MiB free
)
if err != nil {
    log.Fatal(err)
}
yaklog.Config(yaklog.Options{Out: w})

When available space drops below the threshold, rotation.New and RotatingWriter.Rotate return rotation.ErrInsufficientDiskSpace. Pair this with yaklog.SetOnWriteError to trigger alerts:

yaklog.SetOnWriteError(func(err error) {
    if errors.Is(err, rotation.ErrInsufficientDiskSpace) {
        metrics.IncrCounter("log.disk_full", 1)
    }
})

On non-Unix platforms (e.g., Windows) the disk-space check is skipped; WithMinFreeBytes has no effect.

RotatingWriter.Close is idempotent: calling it multiple times is safe and always returns nil after the first call.

When Write is called with a payload whose length is ≥ maxSize and the current file already contains data, the writer rotates the current file first, then writes the oversized payload into the fresh file. This prevents a large single message from being split across two backup archives.

When the underlying file write fails (e.g., disk full), RotatingWriter invokes the WithOnWriteError callback if one is set. Without a callback, it falls back to writing the payload to os.Stderr so log data is never silently discarded:

w, _ := rotation.New(
    rotation.WithDir("/var/log/app"),
    rotation.WithFilename("app"),
    rotation.WithOnWriteError(func(err error, p []byte) {
        // err: the underlying I/O error
        // p: the log data that could not be written
        metrics.IncrCounter("log.write_errors", 1)
        _, _ = os.Stderr.Write(p) // manual fallback
    }),
)
Total backup size limit

Use WithMaxTotalSize to cap the combined disk usage of all backup files. After each rotation cleanup pass RotatingWriter deletes the oldest backups until the total size of remaining backups falls below the threshold. WithMaxTotalSize can be combined with WithMaxBackups and WithMaxAge — the strictest constraint wins.

w, _ := rotation.New(
    rotation.WithDir("/var/log/app"),
    rotation.WithFilename("app"),
    rotation.WithMaxSize(64 << 20),        // 64 MiB per file
    rotation.WithMaxTotalSize(512 << 20),  // cap all backups at 512 MiB total
)
Health probe

RotatingWriter.Healthy() reports whether the writer is open and the last file write succeeded. It is concurrency-safe (pure atomic read, no lock) and designed for health checks and metrics probes.

if !w.Healthy() {
    metrics.IncrCounter("log.writer_unhealthy", 1)
}

Healthy() returns false when:

  • The writer has been Close()d.
  • The most recent file.Write call returned an error (e.g., disk full, fd invalidated).

It returns true again after the next successful Write call.


Performance Benchmarks

Benchmark data collected without PGO; median of 3 runs.
Methodology: all sub-benchmarks use RunParallel + io.Discard, consistent with zerolog/zap README numbers.
yaklog uses Send() (synchronous path) + TimeOff (no timestamp), matching zerolog's default no-timestamp behavior for equal-payload comparison.

Test Environment

Item Value
CPU Intel Xeon E-2186G @ 3.80GHz (6C/12T)
OS Linux amd64
Go 1.26 (GOMAXPROCS=12)
PGO Not used
Output io.Discard
Log a static string
Library ns/op B/op allocs/op
yaklog 13.7 0 0
zerolog 17.4 0 0
zap 46.9 0 0
zap (sugared) 48.2 0 0
slog (JSON) 134 0 0
logrus 3 217 1 585 28
Log a message and 10 fields
Library ns/op B/op allocs/op
yaklog 66.3 0 0
zerolog 69.8 0 0
zap 463 731 2
zap (sugared) 619 1 438 2
slog (JSON) 469 225 3
logrus 11 391 4 745 68
Logger with 10 context fields (pre-built)
Library ns/op B/op allocs/op
yaklog 14.1 0 0
zerolog 18.1 0 0
zap 47.9 0 0
zap (sugared) 50.4 0 0
slog (JSON) 142 0 0
logrus 9 565 3 291 59

Full multi-library benchmark source and reproducible results: _benchmarks/run.sh.


Performance Tips

  1. Prefer Post() — Async path with batched writes, zero allocations on hot path; only use Send() when you need to guarantee the write has completed.
  2. Avoid Any() on hot pathsAny() requires yakjson serialization and may produce small allocations; use typed methods like Str / Int / Bool for known types. If you already have serialized JSON bytes, use JSON(key, raw) instead — it embeds raw bytes directly with zero allocations.
  3. Call yaklog.Wait() at the end of main — Ensures all async logs are flushed before the program exits.
  4. Tune QueueLen appropriately — Default is 4096; under high traffic peaks, increase to 16384+ to avoid drops.
  5. Pre-build Label chains — Build the Label chain during initialization and reuse at runtime to avoid repeated prefix buffer allocations.
// ✅ Recommended: build a fixed-field child Logger at startup
reqLog := logger.Label("service", "api").Label("region", "us-east")

// Reuse in request handling
reqLog.Info().Str("path", r.URL.Path).Msg("request").Post()

Development Tools

lint.sh — Code Quality Checks
bash lint.sh              # Full check: gofmt -s + go vet + golangci-lint
bash lint.sh --vet        # Only gofmt + go vet (skip golangci-lint)
bash lint.sh --fix        # golangci-lint --fix auto-fix
bash lint.sh --fmt        # Format only (gofmt -s -w), no checks
bash lint.sh --test       # Quick test (go test ./... -race -count=1)

If golangci-lint is not installed, the script automatically installs it to $GOPATH/bin via the official install script.

bench.sh — Benchmarks (yaklog internal)
bash bench.sh             # Default: benchtime=3s, count=3
bash bench.sh 5s 5        # Custom: benchtime=5s, count=5

Output is saved to bench_<os>_<cores>c<threads>t.txt with a header containing CPU / Go version / kernel info.

_benchmarks/run.sh — Multi-library comparison
cd _benchmarks
./run.sh                        # All benchmarks (3s × 3 runs), saves results_*.txt
BENCHTIME=5s COUNT=5 ./run.sh   # Custom parameters

Results are saved to _benchmarks/results_<os>_<cores>c<threads>t_<timestamp>.txt
and serve as the canonical data source for benchmark tables in this README.

fuzz.sh — Fuzz Testing
bash fuzz.sh                        # All Fuzz targets, each runs for 5m
bash fuzz.sh 2m                     # All targets, custom duration 2m
bash fuzz.sh FuzzJSONEncoder        # Single target 5m
bash fuzz.sh FuzzJSONEncoder 2m     # Single target 2m
FUZZ_TIME=10m bash fuzz.sh         # Specify duration via environment variable

Crash records are automatically saved to fuzz_logs/fuzz_<timestamp>.log. Press Ctrl+C to skip to the next target; press again to exit.


License

MIT © 2026 uniyak.com

Documentation

Overview

Package yaklog 提供高性能零分配结构化日志库。

设计目标

  • 零分配热路径:级别未启用或采样丢弃全路径零分配; 启用路径通过 sync.Pool 复用 Event 对象 + bufpool 复用缓冲。
  • Send / Post 二元模型:Send 同步写入(无延迟),Post 异步投递包级 worker。
  • Shutdown:进程退出阶段可调用 Shutdown() 停止接受新的 Post 并排空队列。
  • JSON / Text 自动选择:Out 为 Console(...) 时自动切换 Text 编码器,其余使用 JSON。
  • 全局 Config:yaklog.Config(opts) 热更新全局参数;New() 零参使用全局配置。
  • 可插拔采样:内置 RateSampler(令牌桶)和 HashSampler(哈希固定采样)。
  • ctx 注入:通过 WithTrace / WithField / WithLogger / WithEventSink 将上下文信息注入日志。

快速开始

// 使用默认配置(JSON 输出到 stderr)
l := yaklog.New()
l.Info().Str("service", "gateway").Int("port", 8080).Msg("启动").Send()

自定义配置

l := yaklog.New(yaklog.Options{
	Out:   yaklog.Console(os.Stdout),
	Level: yaklog.Debug,
})
l.Debug().Str("key", "value").Send()

异步写入 + 等待排空

defer yaklog.Wait() // 进程退出前等待 Post 队列排空
l.Info().Msg("异步日志").Post()

派生子 Logger(Label 追加固定字段)

sub := l.Label("module", "auth")
sub.Info().Msg("鉴权通过").Send()

独立 Logger(Fork 独立级别)

dbLog := l.Fork()
dbLog.SetLevel(yaklog.Debug) // 不影响 l

Index

Constants

View Source
const (
	// ErrWriterClosed 包级 worker 已关闭或已执行 Shutdown,不再接受新 Post 任务。
	ErrWriterClosed = yakutil.ErrStr("yaklog: worker closed")

	// ErrInvalidPath 日志文件路径不合法(如空字符串或无法解析为绝对路径)。
	ErrInvalidPath = yakutil.ErrStr("yaklog: invalid file path")

	// ErrNotLogFile 目标路径指向的已有文件疑似二进制/可执行文件,拒绝覆盖写入。
	// 触发条件:文件已存在且(含可执行权限位 0111,或前缀匹配已知二进制 magic)。
	ErrNotLogFile = yakutil.ErrStr("yaklog: target file is not a log file")

	// ErrInvalidOpts 配置参数不合法(如队列容量超限)。
	ErrInvalidOpts = yakutil.ErrStr("yaklog: invalid options")
)
View Source
const ConsoleTimeDateMicro = "2006-01-02 15:04:05.000000"

ConsoleTimeDateMicro Console 输出时间格式,日期+时间,微秒精度。 示例:2026-03-08 16:38:43.152148

View Source
const ConsoleTimeDateMilli = "2006-01-02 15:04:05.000"

ConsoleTimeDateMilli Console 输出时间格式,日期+时间,毫秒精度。 示例:2026-03-08 16:38:43.152

View Source
const ConsoleTimeMicro = "15:04:05.000000"

ConsoleTimeMicro Console 输出时间格式,仅时间,微秒精度。 示例:16:38:43.152148

View Source
const ConsoleTimeMilli = "15:04:05.000"

ConsoleTimeMilli Console 输出时间格式,仅时间,毫秒精度(默认)。 示例:16:38:43.152

View Source
const ConsoleTimeRFC3339Milli = "2006-01-02T15:04:05.000Z07:00"

ConsoleTimeRFC3339Milli Console 输出时间格式,RFC3339 含毫秒与时区偏移。 示例:2026-03-08T16:38:43.152+08:00

Variables

This section is empty.

Functions

func Config

func Config(opts Options)

Config 设置全局默认 Options,影响后续所有 New()(零参)调用。并发安全(原子替换)。

可多次调用,但 QueueLen 和 FlushInterval 仅首次调用(或首次 New())时生效—— 包级 worker 由 sync.Once 保证只启动一次;其余字段每次调用均生效。 建议在 main() 最开始调用一次完成初始化。

func Console

func Console(w ...io.Writer) io.Writer

Console 创建彩色文本 Sink,输出到指定 writer(默认 os.Stderr)。

Logger 检测到此 Sink 后自动使用 textEncoder,热路径零分配无需 JSON 解析。 ConsoleTimeFormat 由 Options.ConsoleTimeFormat 控制(零值使用 ConsoleTimeMilli)。

func Discard

func Discard() io.Writer

Discard 返回丢弃所有输出的 Sink,通常用于测试场景。

func Dropped

func Dropped() int64

Dropped 返回因队列满而丢弃的 Post 任务总数(可用于监控)。

func ErrCount

func ErrCount() int64

ErrCount 返回全部写入错误次数(Post 异步路径 + Send 同步路径)。 可配合 Dropped() 监控日志流健康。

func GetFatalFunc

func GetFatalFunc() func(int)

GetFatalFunc 返回当前 Fatal 退出函数,用于保存和还原。

func GetPanicFunc

func GetPanicFunc() func(string)

GetPanicFunc 返回当前 Panic 函数,用于保存和还原。

func Save

func Save(path ...string) io.Writer

Save 创建基于文件的 JSON Sink,支持按大小自动轮转。

使用形式:

  • Save("./logs/app.log") — 显式指定路径
  • Save() — 使用 Options.FilePath(在 New 时解析)

轮转参数(MaxSize/MaxAge/MaxBackups/Compress/LocalTime)来自 Logger 的 Options。 返回的 io.Writer 同时实现 io.Closer;调用方可通过 Logger.Closer() 取回并在退出时关闭。

func SetFatalFunc

func SetFatalFunc(fn func(int))

SetFatalFunc 替换 Fatal 级别触发的退出函数,默认为 os.Exit。

典型用法:在 TestMain 或具体测试函数开头替换,测试结束后通过 defer 还原。

old := yaklog.GetFatalFunc()
defer yaklog.SetFatalFunc(old)
yaklog.SetFatalFunc(func(code int) { /* 捕获退出码,不真正退出 */ })

func SetOnDrop

func SetOnDrop(fn func())

SetOnDrop 注册 Post 队列满丢弃日志时的回调。 回调在 postTask 调用方 goroutine 中同步执行,须快速返回,不可阻塞。 传 nil 可清除回调。可搭配 yakevent 使用:

bus, _ := yakevent.ForAsync()
yaklog.SetOnDrop(func() { bus.Emit(&yakevent.Event{Type: "log.drop"}) })

func SetOnWriteError

func SetOnWriteError(fn func(error))

SetOnWriteError 注册异步写入失败时的回调,参数为底层 io.Writer 返回的 error。 回调在 worker goroutine 中同步执行,须快速返回,不可阻塞或 panic。 传 nil 可清除回调。

yaklog.SetOnWriteError(func(err error) { metrics.LogWriteErrors.Inc() })

func SetPanicFunc

func SetPanicFunc(fn func(string))

SetPanicFunc 替换 Panic 级别触发的 panic 函数,默认为内置 panic。

典型用法:在 TestMain 或具体测试函数开头替换,测试结束后通过 defer 还原。

old := yaklog.GetPanicFunc()
defer yaklog.SetPanicFunc(old)
yaklog.SetPanicFunc(func(msg string) { /* 捕获消息,不真正 panic */ })

func Shutdown

func Shutdown() error

Shutdown 停止接受新的 Post 任务,排空当前队列并关闭包级 worker。

它适合进程退出阶段使用:比 Wait 更强,会在排空后拒绝后续异步投递。 调用成功后,后续 Post 会被忽略,并通过 OnWriteError 回调报告 ErrWriterClosed。

func Wait

func Wait()

Wait 等待所有已提交的 Post 任务写入完成。通常在 main 函数最后调用。

原理:向 worker 发送屏障请求,worker 排空所有已入队条目后应答。 安全性:Wait 期间不应有并发的 Post 调用;若有,这些后续投递不保证被本次 Wait 覆盖,但不会引发崩溃或数据损坏。调用前应确保所有 Post 生产方已停止提交。

func WithEventSink

func WithEventSink(ctx context.Context, sink EventSink) context.Context

WithEventSink 将 EventSink 注入 context,返回新 context。

func WithField

func WithField(ctx context.Context, key string, val any) context.Context

WithField 将任意键值对注入 context,返回新 context。

同一 key 多次注入时,后注入的覆盖先注入的(context 链语义)。 Logger.Context(ctx) 绑定或 Event.Ctx(ctx) 时,这些字段会自动追加到每条日志。

func WithLogger

func WithLogger(ctx context.Context, l *Logger) context.Context

WithLogger 将 *Logger 注入 context,返回新 context。

FromCtx 可取出此 Logger;通常在 middleware 中绑定带请求信息的子 Logger。

func WithTrace

func WithTrace(ctx context.Context, id [16]byte) context.Context

WithTrace 将 [16]byte TraceID 注入 context,返回新 context。

Logger.Context(ctx) 绑定或 Event.Ctx(ctx) 时会自动提取并追加 "trace_id" 字段。

Types

type ColorScheme

type ColorScheme struct {
	Trace  string // Trace 级别色(含 level 标识及 Msg 文本);默认 "\x1b[90m"(暗灰)
	Debug  string // Debug 级别色;默认 "\x1b[36m"(青色)
	Info   string // Info 级别色;默认 "\x1b[32m"(绿色)
	Warn   string // Warn 级别色;默认 "\x1b[33m"(黄色)
	Error  string // Error 级别色;默认 "\x1b[31m"(红色)
	Panic  string // Panic 级别色;默认 "\x1b[1;35m"(加粗洋红)
	Fatal  string // Fatal 级别色;默认 "\x1b[1;31m"(加粗红色)
	Time   string // 时间戳色;默认 "\x1b[2m"(暗淡 dim)
	Key    string // 字段 key= 色;默认 "\x1b[34m"(暗蓝)
	Tag    string // Tag 标签色;默认 "\x1b[38;5;166m"(橙色 256色)
	Source string // source 字段值色;默认 "\x1b[93m"(亮黄)
}

ColorScheme 定义 Console 输出的 ANSI 颜色方案,允许用户替换任意元素的颜色。 零值(空字符串)字段使用内置默认色,无需填写全部字段。 仅在 Out = Console() 时生效;JSON 格式及 ConsoleNoColor=true 时忽略所有颜色配置。

ANSI 转义码格式:"\x1b[<code>m",例如 "\x1b[32m" 为绿色,"\x1b[1;33m" 为加粗黄色。 支持的标准颜色代码:30–37(暗色前景)、90–97(亮色前景)、1;3x(加粗前景)等。 终端 256 色格式:"\x1b[38;5;<n>m";真彩色格式:"\x1b[38;2;<r>;<g>;<b>m"。

type Event

type Event struct {
	// contains filtered or unexported fields
}

Event 代表一条正在构建的日志事件。

通过 Logger.Info() / Logger.Debug() 等方法获取;级别未启用时返回 nil。 所有链式方法均 nil-safe:(*Event)(nil).Str(...) 合法且返回 nil,零分配。

生命周期:Event 由 eventPool(sync.Pool)管理; 调用 Send() / Post() 后 buf 所有权转移,Event 归还 pool。 Send / Post 调用后不得再引用该 Event。

func (*Event) AnErr

func (e *Event) AnErr(key string, err error) *Event

AnErr 追加 error 字段,使用自定义键名。err 为 nil 时不追加。

func (*Event) Any

func (e *Event) Any(key string, val any) *Event

Any 追加任意类型字段(通过 yakjson 序列化;会分配)。

func (*Event) Bool

func (e *Event) Bool(key string, val bool) *Event

Bool 追加 bool 字段。

func (*Event) Bytes

func (e *Event) Bytes(key string, val []byte) *Event

Bytes 追加 []byte 字段(零拷贝 B2S,适合 UTF-8 数据)。

B2S 在 Bytes() 调用期间将 val 临时转为 string 并传入 appendJSONStr; appendJSONStr 在本次调用内同步将內容拷贝进 e.buf,之后不再持有 val 引用。 因此 val 只需在 Bytes() 调用期间保持有效,无需延续至 Send/Post。

func (*Event) Caller

func (e *Event) Caller() *Event

Caller 为此事件附加调用方 file:line(source 字段)。

适合在特定日志点按需附加定位信息,无需全局开启 Options.Source。 若 Options.Source 已全局启用,Caller() 捕获的 PC 更准确(在链式调用处采集,而非 Send/Post 处)。 调用后返回同一 *Event,可继续链式调用。

func (*Event) Ctx

func (e *Event) Ctx(ctx context.Context) *Event

Ctx 以 ctx 替换此事件的上下文(覆盖 Logger 的 boundCtx)。

可从 ctx 中提取 WithTrace / WithField 注入的字段。

func (*Event) Dur

func (e *Event) Dur(key string, val time.Duration) *Event

Dur 追加 time.Duration 字段(以毫秒浮点数表示)。

func (*Event) Err

func (e *Event) Err(err error) *Event

Err 追加 error 字段(key = "error")。err 为 nil 时不追加任何字段。 Console 模式下,error 字段的展示色与当前日志级别一致(如 Error 级别红色)。

func (*Event) Float64

func (e *Event) Float64(key string, val float64) *Event

Float64 追加 float64 字段(NaN/Inf 安全)。

func (*Event) Int

func (e *Event) Int(key string, val int) *Event

Int 追加 int 字段。

func (*Event) Int64

func (e *Event) Int64(key string, val int64) *Event

Int64 追加 int64 字段。

func (*Event) JSON

func (e *Event) JSON(key string, raw []byte) *Event

JSON 追加已序列化的原始 JSON bytes 作为字段值(零分配)。

JSON 输出模式下 raw 直接嵌入为嵌套对象,不做二次转义。 Console 输出模式下 raw 作为字段值原样输出。 调用方负责保证 raw 是合法 JSON;raw 为 nil 时跳过该字段。

Console 模式注意:若 raw 包含换行符(如美化格式的 JSON),输出行会被拆分, 可能导致结构化日志解析器丢失字段关联。建议 Console 模式下传入紧凑格式 JSON。

func (*Event) Msg

func (e *Event) Msg(msg string) *Event

Msg 设置日志消息,返回 *Event(未终结,可继续追加字段或直接调用 Send/Post)。

若不调用 Msg,日志记录中不含 msg 字段(零分配)。

func (*Event) Post

func (e *Event) Post()

Post 异步写入:将 buf 投递给包级 worker goroutine,不阻塞调用方。

适合大并发高吞吐场景。进程退出前应调用 yaklog.Wait() 或 logger.Wait() 排空。 调用后不得再引用该 Event。

func (*Event) Send

func (e *Event) Send()

Send 同步写入:在调用方 goroutine 中直接 io.Write,不经过 worker 队列。

写入完成后 goroutine 继续,适合对延迟敏感的场景或需要等待结果的场景。 调用后不得再引用该 Event。

func (*Event) Str

func (e *Event) Str(key, val string) *Event

Str 追加字符串字段。

func (*Event) Stringer

func (e *Event) Stringer(key string, val fmt.Stringer) *Event

Stringer 追加实现 fmt.Stringer 接口的对象(调用 .String())。

func (*Event) Time

func (e *Event) Time(key string, val time.Time) *Event

Time 追加 time.Time 字段(使用 Logger 配置的时间格式)。

func (*Event) Uint64

func (e *Event) Uint64(key string, val uint64) *Event

Uint64 追加 uint64 字段。

type EventSink

type EventSink interface {
	Emit(level Level, msg string, fields []byte)
}

EventSink 事件挂载接口。实现此接口可在每条日志写入时同步触发事件 (如向 yakevent 总线发布告警)。

Emit 在日志编码完成后、写入目标前被同步调用。 实现须并发安全,且不得持有 fields 引用(传入的是当前日志行的独立副本)。 若 Emit 内部发生 panic,yaklog 会隔离并吞掉该 panic,避免影响日志主流程。

type HashSampler

type HashSampler struct {
	// contains filtered or unexported fields
}

HashSampler 基于消息哈希的固定比例采样器。

对相同 level+msg 组合哈希后与阈值比较,保证同一来源的记录以稳定比例输出, 避免令牌桶的时间窗口倾斜问题。 并发安全:热路径纯整数运算,无共享可变状态,零分配。

种子范围:maphash.MakeSeed 在进程启动时随机化(AES-NI 加速,防哈希碰撞预测)。 相同消息在同一进程内的采样结果稳定一致;不同进程间由于种子不同结果将不同, 不应依赖跨进程的采样确定性。

各级别独立限额:limits[uint8(level)&7] 存储每个日志级别的采样阈值。 级别到索引的映射: Trace(-2)→6、Debug(-1)→7、Info(0)→0、Warn(1)→1、Error(2)→2、Panic(3)→3、Fatal(4)→4。 采样率支持在运行时通过 HashSampler.SetRateHashSampler.SetRateForLevel 更新; 新率平均在一次原子写后对所有 goroutine 可见。

注意:当作为 Logger.Sampler 使用时,采样发生在 Msg() 调用之前, msg 参数始终为空字符串。此时采样仅基于 level 粒度,而不是基于消息内容。 如需基于消息内容做稳定采样,应在应用层自行调用 Sample()。

func NewHashSampler

func NewHashSampler(rate float64) *HashSampler

NewHashSampler 创建固定比例采样器,所有级别使用相同初始采样率。

  • rate:采样率,范围 (0, 1.0];1.0 表示全量输出,0.01 表示 1%。

func (*HashSampler) Sample

func (s *HashSampler) Sample(level Level, msg string) bool

Sample 实现 Sampler 接口,对 level+msg 哈希后与该级别的阈值比较。 热路径:maphash.String 单次调用(AES-NI 加速),level 以黄金比例常数混入,零分配。

当 Logger.Sampler 调用时 msg 为空字符串(采样在 Msg() 之前执行), 此时哈希结果仅由 level 决定——即对同一级别按固定比例采样。

func (*HashSampler) SetRate

func (s *HashSampler) SetRate(rate float64)

SetRate 修改所有级别的采样率。原子写入,对所有 goroutine 单次内存屏障后可见。 rate 范围 (0, 1.0];超出范围自动酆位。

func (*HashSampler) SetRateForLevel

func (s *HashSampler) SetRateForLevel(level Level, rate float64)

SetRateForLevel 修改指定级别的采样率。原子写入,对所有 goroutine 单次内存屏障后可见。 让不同级别使用不同的采样率,例如 Error/Warn 全量输出而 Debug 只输出 10%。 rate 范围 (0, 1.0];超出范围自动酆位。

type LabelBuilder

type LabelBuilder struct {
	// contains filtered or unexported fields
}

LabelBuilder 批量追加固定字段的构建器。

相比多次调用 Label(每次 clone 一个 Logger),LabelBuilder 只在 Build 时 做一次 clone,减少中间 Logger 的堆分配(N 次 Label → N alloc → 1 alloc)。

使用方式:

sub := l.Labels().Str("module", "auth").Int("shard", 3).Build()

LabelBuilder 使用值接收器,典型内联用法下可栈分配(零堆 alloc)。

func (LabelBuilder) Any

func (b LabelBuilder) Any(key string, val any) LabelBuilder

Any 追加任意类型字段(通过 yakjson 序列化,有分配)。

func (LabelBuilder) Bool

func (b LabelBuilder) Bool(key string, val bool) LabelBuilder

Bool 追加 bool 字段。

func (LabelBuilder) Build

func (b LabelBuilder) Build() *Logger

Build 完成构建,返回新 Logger(仅此处做一次构造)。

返回的 Logger 与原 Logger 共享 level 和 wg(与 Label 行为一致)。

func (LabelBuilder) Float64

func (b LabelBuilder) Float64(key string, val float64) LabelBuilder

Float64 追加 float64 字段。

func (LabelBuilder) Int

func (b LabelBuilder) Int(key string, val int) LabelBuilder

Int 追加 int 字段。

func (LabelBuilder) Int64

func (b LabelBuilder) Int64(key string, val int64) LabelBuilder

Int64 追加 int64 字段。

func (LabelBuilder) JSON

func (b LabelBuilder) JSON(key string, raw []byte) LabelBuilder

JSON 追加已序列化的原始 JSON bytes 作为字段值(零分配)。 raw 为 nil 时跳过该字段。

func (LabelBuilder) Str

func (b LabelBuilder) Str(key, val string) LabelBuilder

Str 追加字符串字段。

func (LabelBuilder) Uint64

func (b LabelBuilder) Uint64(key string, val uint64) LabelBuilder

Uint64 追加 uint64 字段。

type Level

type Level int8

Level 日志输出级别,零值为 Info。

const (
	Trace Level = iota - 2 // -2,最低追踪级别
	Debug                  // -1
	Info                   // 0,零值,默认最低输出级别
	Warn                   // 1
	Error                  // 2
	Panic                  // 3,Send/Post 后调用 PanicFunc(默认为内置 panic),可被 defer/recover 捕获
	Fatal                  // 4,Send/Post 后调用 FatalFunc(默认为 os.Exit(1))
)

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger 高性能结构化日志器。

通过 New() 创建;通过 Label / Fork / Context / To 派生子 Logger。 Logger 对象可赋值传递(字段均为指针/切片),并发安全。

生命周期:Logger 持有的 closer(若 Out 是 io.Closer)在进程退出时 由调用方管理(应用级 defer closer.Close());yaklog 本身不注册 finalizer。

func FromCtx

func FromCtx(ctx context.Context) *Logger

FromCtx 从 context 中取出 *Logger。

若 context 中无 Logger,返回包级默认 Logger(全局 Config 效果)。

func New

func New(opts ...Options) *Logger

New 创建 Logger 实例。

  • 零参:完全使用全局 Config() 的 Options
  • 有参:完全使用传入的 Options(完整覆盖,不 merge 全局)

包级 worker 在第一次调用 New 或 Config 时惰性启动。

func (*Logger) Closer

func (l *Logger) Closer() io.Closer

Closer 返回底层写入器的 io.Closer(如 RotatingWriter)。 若 Out 不实现 io.Closer,返回 nil。调用方可在 defer 中关闭。

func (*Logger) Context

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

Context 将 ctx 绑定到 Logger,返回新 Logger。

绑定后该 Logger 的每条日志自动提取 ctx 中注入的字段(trace_id、WithField 字段)。 无需再在每条 Event 上调用 .Ctx(ctx),但 Event.Ctx(ctx) 仍可覆盖。

func (*Logger) Debug

func (l *Logger) Debug() *Event

Debug 创建 Debug 级别日志事件。nil 表示级别未启用(零分配)。

func (*Logger) Error

func (l *Logger) Error() *Event

Error 创建 Error 级别日志事件。nil 表示级别未启用(零分配)。

func (*Logger) Event

func (l *Logger) Event(lvl Level) *Event

Event 创建指定级别的日志事件。nil 表示级别未启用(零分配)。

适用于级别由运行时决定的场景(如 slog 适配器),避免 switch 分派开销。

func (*Logger) Fatal

func (l *Logger) Fatal() *Event

Fatal 创建 Fatal 级别日志事件。Send/Post 后排空队列并调用 FatalFunc(默认为 os.Exit(1))。

func (*Logger) Fork

func (l *Logger) Fork() *Logger

Fork 派生独立 Logger:拥有独立的 level 原子变量和 wg,不受父级 SetLevel 影响。

通常在需要对某个子模块独立控制日志级别时调用(如 db 模块 Debug 而全局 Info)。

func (*Logger) GetLevel

func (l *Logger) GetLevel() Level

GetLevel 返回当前最低输出级别。

func (*Logger) Info

func (l *Logger) Info() *Event

Info 创建 Info 级别日志事件。nil 表示级别未启用(零分配)。

func (*Logger) Label

func (l *Logger) Label(key string, val any) *Logger

Label 追加一个固定字段,返回新 Logger(共享 level 和 wg)。

线程安全:原 Logger 不受影响;派生后各自 prefix 独立。 val 支持 string / int / int64 / uint64 / float64 / bool,其他类型通过 any 序列化。

func (*Logger) Labels

func (l *Logger) Labels() LabelBuilder

Labels 返回预填充当前 prefix 的 LabelBuilder。

后续通过 Str / Int / Bool 等方法追加字段,最后调用 Build() 生成新 Logger。

func (*Logger) Panic

func (l *Logger) Panic() *Event

Panic 创建 Panic 级别日志事件。Send/Post 后同步写入日志,然后调用 PanicFunc(默认为内置 panic)。 PanicFunc 默认可被 defer/recover 捕获;可通过 SetPanicFunc 替换,与 Fatal 的差异在于可恢复性。

func (*Logger) SetLevel

func (l *Logger) SetLevel(lvl Level)

SetLevel 热更新最低输出级别。对所有共享此 level 指针的 Logger(Label 派生链)生效。 Fork 派生的子 Logger 有独立指针,互不影响。

func (*Logger) Tag

func (l *Logger) Tag(name string) *Logger

Tag 设置一个组件标签,返回新 Logger(共享 level 和 wg)。

Console 模式下标签紧跟级别标识后渲染为 [name](洋红色),格式示例:

10:30:00.123 I [cache] key=val message

JSON 模式下渲染为 "tag":"name" 字段,位于 level 字段之后。 标签不占用 Label 前缀空间,不参与 Label 字段编码,仅影响记录头部输出。

func (*Logger) To

func (l *Logger) To(out io.Writer) *Logger

To 替换输出目标,返回新 Logger。编码器根据新 Out 类型重新选择。

传入 nil 时,派生 Logger 的所有写入操作静默丢弃,并通过 OnWriteError 通知 ErrWriterClosed,同时计入 ErrCount。可用于彻底静默某个 Logger 分支。

派生后与原 Logger 共享 level/wg,但 out 和 enc 独立。

func (*Logger) Trace

func (l *Logger) Trace() *Event

Trace 创建 Trace 级别日志事件。nil 表示级别未启用(零分配)。

级别检查内联于此,disabled 路径无需调用 newEvent 函数,节省函数调用开销。

func (*Logger) Wait

func (l *Logger) Wait()

Wait 等待该 Logger(及共享 wg 的所有 Label 派生子 Logger)的所有 Post 写入完成。

安全性:与 sync.WaitGroup.Wait 语义一致,Wait 期间不应有并发的 Post 调用。

func (*Logger) Warn

func (l *Logger) Warn() *Event

Warn 创建 Warn 级别日志事件。nil 表示级别未启用(零分配)。

type Options

type Options struct {
	// ── 基础 ──────────────────────────────────────────────────────────────
	Level    Level     // 最低输出级别,零值为 Info
	Out      io.Writer // 输出目标;nil 且 FilePath 空 → JSON os.Stderr
	FilePath string    // 日志文件路径;Out 为 nil 时自动 Save(FilePath)
	Source   bool      // 是否附加调用方 file:line(有轻微性能开销)
	// CallerFunc 自定义 source 字段的显示值,接收原始 file 路径和行号,返回最终写入日志的字符串。
	// 返回空字符串则完全省略该字段。nil 表示使用默认行为(完整 file:line)。
	// 常用于:只保留文件名(filepath.Base(file))、路径脱敏、相对路径截取等场景。
	CallerFunc func(file string, line int) string
	TimeFormat TimeFormat // 时间戳格式,零值为 TimeRFC3339Milli
	Sampler    Sampler    // 采样器,nil 表示全量输出

	// ── 异步 Post 队列 ────────────────────────────────────────────────────
	QueueLen      int           // 包级 worker channel 容量;0 → defaultQueueLen
	FlushInterval time.Duration // 周期刷写间隔;0 → 100ms

	// ── 文件轮转(Out = Save(...) 时生效)────────────────────────────────
	FileMaxSize    int  // 单文件最大体积(MB);0 → 100
	FileMaxAge     int  // 备份文件最长保留天数;0 → 不限制
	FileMaxBackups int  // 最多保留旧文件数;0 → 不限
	FileCompress   bool // 是否压缩旧文件
	FileLocalTime  bool // 备份文件名时间戳使用本地时间(false = UTC)

	// ── Console 格式(Out = Console() 时生效)────────────────────────────
	// ANSI 颜色:Console() 默认启用,设 ConsoleNoColor=true 可关闭。
	// 启用后:级别有独立色彩,字段 key 以灰度(dim)弱化显示。
	ConsoleNoColor bool // true = 关闭 ANSI 颜色输出(默认 false,即颜色默认开启)
	// 级别显示:默认单字母简写(T/D/I/W/E/P/F),设 ConsoleLevelFull=true 使用完整名称(TRACE/DEBUG/INFO...)。
	ConsoleLevelFull  bool        // true = 完整级别名(TRACE/INFO/WARN...);false(默认)= 单字母简写(T/I/W...)
	ConsoleTimeFormat string      // Console 时间格式;空 → ConsoleTimeMilli
	ColorScheme       ColorScheme // Console ANSI 颜色方案;零值字段使用内置默认色;ConsoleNoColor=true 时忽略
}

Options 是 Logger 全量配置结构体,零值即可直接使用(零值 = 全部默认值)。

零值含义:

  • Level → Info(int8 零值恰好是 Info 的值 0)
  • Out → nil,New() 中 fallback 到 JSON → os.Stderr
  • FilePath → 空,Out 非 nil 时忽略;Out 为 nil 且 FilePath 非空时自动 Save
  • TimeFormat → TimeRFC3339Milli(iota=0)
  • QueueLen → 0,New() 中补 defaultQueueLen
  • 其余 bool → false

type RateSampler

type RateSampler struct {
	// contains filtered or unexported fields
}

RateSampler 基于令牌桶算法的速率采样器(复用 yakutil/ratelimit.Limiter)。

每秒最多允许 rate 条记录通过,突发上限为 burst 条。 超出速率的记录被静默丢弃。 热路径:单次原子 CAS,无锁,无 goroutine,零分配。

func NewRateSampler

func NewRateSampler(rate, burst int) *RateSampler

NewRateSampler 创建令牌桶采样器。

  • rate:每秒允许通过的最大记录数(> 0)
  • burst:突发令牌桶容量(> 0)

func (*RateSampler) Sample

func (s *RateSampler) Sample(_ Level, _ string) bool

Sample 实现 Sampler 接口,消耗一个令牌;令牌耗尽时返回 false。

type Sampler

type Sampler interface {
	Sample(level Level, msg string) bool
}

Sampler 采样器接口。

Sample 返回 true 表示允许输出本条记录,false 表示丢弃。 实现须并发安全(多 goroutine 同时调用)。

type TimeFormat

type TimeFormat uint8

TimeFormat JSON 输出时间戳格式,零值为 TimeRFC3339Milli。

const (
	TimeRFC3339Milli TimeFormat = iota // 默认,"2026-03-08T10:30:00.123Z"
	TimeUnixSec                        // 秒级 Unix 时间戳,如 1741426200
	TimeUnixMilli                      // 毫秒级,如 1741426200123
	TimeUnixNano                       // 纳秒级,如 1741426200123456789
	TimeOff                            // 不输出时间字段
)

Directories

Path Synopsis
Package adapter 提供 yaklog 与 Go 标准库日志系统的适配层。
Package adapter 提供 yaklog 与 Go 标准库日志系统的适配层。
Package rotation 提供基于大小的日志文件轮转写入器。
Package rotation 提供基于大小的日志文件轮转写入器。

Jump to

Keyboard shortcuts

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