zlog

package module
v0.3.4 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 17 Imported by: 0

README

zlog

Go Reference Go Version

English | 简体中文

An out-of-the-box, structured logging component for Go backend services. Built on Uber zap as its high-performance core, zlog adds an engineering-friendly layer on top: a type-safe Field-style API, dual global-singleton / standalone-instance modes, a self-contained lightweight file rotator, level-based file splitting, context-based trace propagation, and an optional gin middleware.

Features

  • Type-safe Field-style API: zlog.Info("msg", zlog.String("k", "v"), zlog.Int("n", 1)) — no format strings, no sentinels, no reflection boxing.
  • zap fully hidden: zero zap types exposed to callers, so the core can be swapped out later without breaking your code.
  • Dual modes: global singleton (Init + package-level functions) fits most services; New standalone instances suit multi-module isolation.
  • Self-contained lightweight rotation: size + time (hourly/daily/weekly/monthly) rotation combined, write-triggered with no background goroutine, supporting retention by count/age and gzip compression.
  • Level-based file splitting: DEBUG/INFO go to app.log, WARN and above go to app.log.wf, making alert collection easy.
  • Context trace propagation: once trace_id / request_id are injected into the context, they appear automatically on every log line.
  • Runtime level adjustment: SetLevel is thread-safe and takes effect without a restart.
  • gin middleware (optional subpackage): trace_id injection + standalone access log + panic recovery with logging; the core library has zero gin dependency.
  • Well-defined FATAL semantics: write → Sync → os.Exit(1), with no log loss before exit.

Installation

go get github.com/leeyzero/zlog

Requires Go 1.22 or later (see go.mod).

Quick Start

Initialize the global logger once at process startup, then call the package-level functions from anywhere:

package main

import "github.com/leeyzero/zlog"

func main() {
	if err := zlog.Init(zlog.DefaultConfig(), zlog.WithConsole(true)); err != nil {
		panic(err)
	}
	defer zlog.Close()

	zlog.Info("service started",
		zlog.String("version", "v1.0.0"),
		zlog.Int("port", 8080),
	)
}

The default output is JSON, for example:

{"level":"INFO","time":"2026-07-22T11:04:05+08:00","caller":"main/main.go:12","msg":"service started","version":"v1.0.0","port":8080}

Core Usage

Field-style structured logging

Every field constructor returns a zlog.Field — type-safe and reflection-free:

zlog.String("user", "alice")
zlog.Int("uid", 1001)
zlog.Int64("order_id", 202607220001)
zlog.Float64("amount", 99.9)
zlog.Bool("admin", false)
zlog.Duration("cost", time.Millisecond*320)
zlog.Time("ts", time.Now())
zlog.Err(err)                 // key is fixed as "error"
zlog.Any("payload", anyValue) // fallback, uses reflection
Global singleton vs. standalone instance
// Mode 1: global singleton (recommended for most services)
zlog.Init(cfg)
zlog.Info("...")
zlog.Default().Info("...") // grab the global Logger instance directly

// Mode 2: standalone instance (multi-module isolation / different outputs)
log, err := zlog.New(cfg)
defer log.Close()
log.Info("...")

When Init has not been called, the package-level functions fall back to a default logger writing to stderr, guaranteeing that calls never panic at any time.

Derived child loggers

With returns a derived logger carrying fixed fields; every subsequent log line automatically includes them:

reqLog := log.With(zlog.String("component", "order"))
reqLog.Info("created", zlog.Int64("id", 1))
// => ...,"component":"order","id":1

A derived instance shares the underlying file and level switch with its parent. Close should be called only once, on the root logger.

Context trace propagation
ctx = zlog.ContextWithTraceID(ctx, "trace-abc-123")
ctx = zlog.ContextWithRequestID(ctx, "req-0001")

// Recommended: derive once per request and reuse
reqLog := log.WithContext(ctx)
reqLog.Info("handling")   // automatically carries trace_id / request_id
reqLog.Warn("slow")
Dynamic level adjustment
zlog.SetLevel(zlog.WarnLevel) // thread-safe, effective immediately
Runtime caller toggle

Logger.WithCaller(enabled bool) Logger derives a logger that turns the automatic caller annotation on or off at runtime, sharing the same core/sink. This differs from the construction-time WithCaller(bool) Option: the Option fixes the setting when the logger is built, whereas the method derives a new instance on the fly.

Disabling it lets a caller write its own caller field — useful when the real call site sits behind a variable-depth wrapper (e.g. a GORM logger adapter):

log.WithCaller(false).Info("gorm sql", zlog.String("caller", realCallSite))

Like With/WithContext, the derived instance shares the underlying sink; call Close only on the root logger.

Configuration

Use DefaultConfig() to get a config with sensible defaults, then override via struct fields or functional options.

Config
Field Type Default Description
Level Level InfoLevel Minimum output level
Encoding string "json" "json" or "console"
Console bool false Whether to also write to stdout
File FileConfig File output; no file is written when Path is empty
SplitLevel bool true Whether to split into .log / .log.wf
Caller bool true Whether to print file:line
TimeFormat string RFC3339 Time format
ConsoleSeparator string "" (tab) Field separator for the console encoding; only effective when Encoding="console"; empty keeps zap's default tab
WriteErrorHook func(error) nil Invoked when a log file write fails (disk full, I/O error), for metrics/alerting/fallback; nil disables it (zap still prints write errors to stderr). Must be goroutine-safe and must not log to this logger
FileConfig
Field Type Default Description
Path string e.g. "logs/app.log"; the wf file is derived automatically as logs/app.log.wf
MaxSize int 100 Per-file size limit (MB); 0 means no size-based rotation
MaxBackups int 10 Number of archives to keep; 0 means unlimited
MaxAge int 30 Days to retain archives; 0 means unlimited
Compress bool false gzip-compress old files
Interval Interval None Time-based rotation granularity
BufferSize int 0 Write-buffer size in KB; 0 (default) disables buffering (every log hits the file immediately). When > 0, logs are buffered and flushed on buffer-full or FlushInterval, cutting write syscalls under high throughput
FlushInterval time.Duration 0 Timed flush interval, only effective when BufferSize > 0; 0 uses zap's default (30s)
Options
zlog.New(zlog.DefaultConfig(),
	zlog.WithLevel(zlog.DebugLevel),
	zlog.WithEncoding("console"),
	zlog.WithConsole(true),
	zlog.WithFile(fc),
	zlog.WithSplitLevel(false),
	zlog.WithCaller(false),
	zlog.WithTimeFormat(time.RFC3339Nano),
	zlog.WithConsoleSeparator(" "), // console only, defaults to tab
	zlog.WithWriteErrorHook(func(err error) { /* metric / alert */ }),
	zlog.WithBuffer(256, 30*time.Second), // enable a 256 KB write buffer (off by default)
)

Level strings can be parsed with ParseLevel (case-insensitive, handy for reading from a config file):

lv, err := zlog.ParseLevel("warn") // => zlog.WarnLevel

File Rotation and Level Splitting

Rotation is write-triggered: there is no background goroutine; the current file is archived only when a write crosses a size or time boundary.

  • Size-based: archives when MaxSize > 0 and the write would exceed the limit.
  • Time-based: Interval sets the granularity, with archive suffixes as follows:
Interval Example archive suffix Description
Hourly app.log.2026072211 Down to the hour
Daily app.log.20260722 Down to the day
Weekly app.log.20260720 The Monday date of that week
Monthly app.log.202607 Down to the month

When size forces multiple splits within the same period, a sequence number is appended: app.log.20260722.1, .2, and so on.

With SplitLevel enabled (the default), WARN and above are additionally written to the .wf file, following the same rotation rules (e.g. app.log.wf.20260722), so an alerting system can collect only the .wf files.

Optional write buffering

Set FileConfig.BufferSize > 0 (or use WithBuffer) to buffer file writes and flush on buffer-full or a timer, reducing write syscalls under high throughput. Buffering is off by default to preserve the "every log hits the file immediately" semantics.

Trade-offs when enabled:

  • A background timed-flush goroutine is started per buffered file sink and stopped on Close (with the default SplitLevel, that means one for .log and one for .log.wf). These are the only goroutines zlog ever runs — rotation itself remains goroutine-free.
  • Fatal, Sync, and Close all flush, so normal exit paths never lose logs. An abnormal exit (unrecovered panic, kill -9) may lose logs still sitting in the buffer.
  • WriteErrorHook fires at flush time rather than at the log call.

gin Middleware (optional)

gin-related capabilities live in the standalone subpackage middleware/ginlog; the core library itself does not depend on gin. Three middlewares are provided:

  • TraceID: resolves a trace_id for each request and injects it into the request context (the producer that Middleware/Recovery/WithContext consume). An inbound header (default X-Trace-Id) is reused for cross-service correlation; otherwise a new id is generated. The default generator emits a W3C Trace Context / OpenTelemetry-compatible 128-bit random id (32 hex chars) and deliberately encodes no internal info. Options: WithTraceIDGenerator (custom id scheme) and WithTraceHeader (custom header name). Register it as the outermost middleware.
  • Middleware: writes a standalone access log (separate file, timestamp k=v text format, not wrapped in JSON), with fields such as trace_id, client_ip, method, path, status, cost_time (microseconds), plus optional business fields.
  • Recovery: catches panics, logs them to the application log (with trace_id, method, path, and stack), and returns 500. It detects broken-pipe/connection-reset (logs without a stack and skips writing a response), and accepts options WithRecoveryHandler (custom response) and WithRecoveryExtraFields (extra log fields).
access := ginlog.Middleware(ginlog.DefaultAccessConfig(),
	ginlog.WithExtraFields(func(c *gin.Context) []ginlog.KV {
		return []ginlog.KV{{Key: "app", Val: "demo"}}
	}),
)

r := gin.New()
r.Use(ginlog.TraceID()) // outermost: inject trace_id into the request context
r.Use(ginlog.Recovery(log))
r.Use(access)

Example access log line:

2026/07/22 11:04:05.123456 trace_id=trace-abc client_ip=127.0.0.1 method=GET path=/api/user status=200 cost_time=1832 app=demo

The timestamp defaults to microsecond precision (2006/01/02 15:04:05.000000), cost_time is in microseconds, and values containing spaces are automatically quoted and escaped. Customize the timestamp via AccessConfig.TimeLayout (any Go time layout; empty falls back to the default).

Output Format

Fixed field keys under the JSON encoding: time, level, msg, caller (when Caller is enabled), stacktrace (for Error and above, as needed). Levels are uppercase (INFO, ERROR, ...).

Performance

Benchmarks with io.Discard as the sink and Caller enabled (go test -bench=. -benchmem, Xeon Gold 6271C):

Scenario ns/op allocs/op
Info (no fields) ~1300 2
Info (5 fields) ~2300 3
WithContext (derive per log line) ~2900 10

Notes:

  • The 2 allocations with no fields come from zap's encoding buffer and Caller stack capture, unrelated to this library's wrapper.
  • The extra 1 allocation with fields is the Field → zap field conversion (the wrapper cost, quantified).
  • The 10 allocations for WithContext are the worst case of deriving a logger on every log line. Real usage should derive once per request with reqLog := log.WithContext(ctx) and reuse it, dropping back to 2–3 allocations.
  • Ultra-high-throughput paths can disable Caller (WithCaller(false)) to reduce overhead further.
Rotator vs. lumberjack

The self-contained rotator is benchmarked against lumberjack under an identical config (MaxSize large enough that no rotation happens during the run, so this measures the pure write path), writing a ~230-byte JSON line to a real file (go test -bench 'Write' -benchmem, Xeon Gold 6271C, GOMAXPROCS=32):

Benchmark ns/op throughput allocs/op
Rotator — serial 1263 181 MB/s 0
lumberjack — serial 1256 182 MB/s 0
Rotator — parallel (32 goroutines) 1462 157 MB/s 0
lumberjack — parallel (32 goroutines) 1449 158 MB/s 0

The write path is on par with lumberjack — same mutex-guarded append model, zero allocations per write — while additionally supporting combined size + time rotation, custom archive naming, and .wf level splitting. Reproduce with:

go test -run '^$' -bench 'Write' -benchmem ./

Examples

The examples/ directory contains complete, independently runnable examples:

Directory Demonstrates
examples/basic Global singleton, package-level functions, dynamic level
examples/instance New standalone instance, With derived child logger
examples/file-rotation File rotation, level splitting, retention and compression
examples/buffered Optional write buffering (BufferSize / WithBuffer)
examples/context trace_id / request_id trace propagation
examples/gin ginlog access log + Recovery

Run any example:

go run ./examples/basic

FAQ

Do the logging methods return an error or panic? No. Only New / Init return an error; Debug/Info/... never return an error and never panic.

Does FATAL lose logs? No. Fatal writes and Syncs first, then calls os.Exit(1).

Do derived loggers need to be closed separately? No. Instances derived via With / WithContext share the underlying file with the root; call Close once, on the root logger.

Why not use lumberjack? To keep size + time combined rotation, custom archive naming, and .wf splitting cohesive in one place, a self-contained lightweight rotator is used.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Close

func Close() error

func ContextWithRequestID

func ContextWithRequestID(ctx context.Context, id string) context.Context

ContextWithRequestID 向 ctx 注入 request_id。

func ContextWithTraceID

func ContextWithTraceID(ctx context.Context, id string) context.Context

ContextWithTraceID 向 ctx 注入 trace_id。

func Debug

func Debug(msg string, fields ...Field)

func Error

func Error(msg string, fields ...Field)

func Fatal

func Fatal(msg string, fields ...Field)

func FileCallerEncoder added in v0.3.0

func FileCallerEncoder(file string, line int) string

FileCallerEncoder 仅输出文件名:行号,如 "global.go:72"。

func FullCallerEncoder added in v0.3.0

func FullCallerEncoder(file string, line int) string

FullCallerEncoder 输出完整路径:行号,如 "/home/user/app/main.go:42"。

func Info

func Info(msg string, fields ...Field)

func Init

func Init(cfg Config, opts ...Option) error

Init 初始化/替换全局 Logger。

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) (string, bool)

RequestIDFromContext 提取 request_id。

func SetLevel

func SetLevel(lv Level)

func Sync

func Sync() error

func TraceIDFromContext

func TraceIDFromContext(ctx context.Context) (string, bool)

TraceIDFromContext 提取 trace_id。

func Warn

func Warn(msg string, fields ...Field)

Types

type CallerEncoder added in v0.3.0

type CallerEncoder func(file string, line int) string

CallerEncoder 定义 caller 字段格式化方式。传入文件完整路径和行号,返回格式化字符串。

type Config

type Config struct {
	Level         Level         // 最低输出级别,默认 INFO
	Encoding      string        // "json" | "console",默认 json
	Console       bool          // 是否同时输出控制台,默认 false
	File          FileConfig    // 文件输出,Path 为空则不写文件
	SplitLevel    bool          // 是否拆 .log/.log.wf,默认 true
	Caller        bool          // 是否打印 file:line,默认 true
	CallerEncoder CallerEncoder // nil 时默认:package/file.go:line(zap ShortCallerEncoder)
	TimeFormat    string        // 时间格式,默认 RFC3339
	// ConsoleSeparator 仅在 Encoding="console" 时生效,为空则沿用 zap 默认的制表符。
	ConsoleSeparator string
	// WriteErrorHook 在底层日志文件写入失败时被回调(如磁盘写满、IO 错误),
	// 便于应用埋点/告警/降级。为 nil 时不启用(zap 仍会把写错误输出到 stderr)。
	// 回调会在写入路径上同步触发、可能被多 goroutine 并发调用,因此必须并发安全,
	// 且不要在其中再写本 logger,以免递归。
	WriteErrorHook func(error)
}

Config 日志器配置。

func DefaultConfig

func DefaultConfig() Config

DefaultConfig 返回带默认值的配置。

type Field

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

Field 结构化日志字段。内部持有 zap 字段,对外完全隐藏 zap。

func Any

func Any(k string, v any) Field

func Bool

func Bool(k string, v bool) Field

func Duration

func Duration(k string, v time.Duration) Field

func Err

func Err(err error) Field

func Float64

func Float64(k string, v float64) Field

func Int

func Int(k string, v int) Field

func Int64

func Int64(k string, v int64) Field

func String

func String(k, v string) Field

func Time

func Time(k string, v time.Time) Field

type FileConfig

type FileConfig struct {
	Path       string   // 如 "logs/app.log";wf 文件自动派生 "logs/app.log.wf"
	MaxSize    int      // MB,默认 100;0 不按大小轮转
	MaxBackups int      // 保留个数,默认 10;0 不限
	MaxAge     int      // 保留天数,默认 30;0 不限
	Compress   bool     // gzip 旧文件,默认 false
	Interval   Interval // 时间轮转粒度,默认 None
	// BufferSize 写缓冲大小(KB)。为 0(默认)表示不缓冲,每条日志立即写入底层文件;
	// 大于 0 时启用带缓冲的刷盘:日志先写入内存缓冲,缓冲写满或到达 FlushInterval 时才落盘,
	// 可显著降低高并发下的 write 系统调用次数。
	// 权衡:开启缓冲后会有一个后台定时 flush goroutine(Close 时停止);进程异常退出
	// (panic 未 recover、被 kill -9 等)可能丢失尚未 flush 的缓冲日志。Fatal、Sync、Close
	// 都会主动 flush,因此正常退出路径不丢日志。此外,写失败时 WriteErrorHook 会在 flush
	// 时刻(而非日志调用时刻)被回调。
	BufferSize int
	// FlushInterval 缓冲定时 flush 间隔,仅在 BufferSize>0 时生效。
	// 为 0 时启用缓冲则采用 zap 默认值(30s)。
	FlushInterval time.Duration
}

FileConfig 文件输出与轮转配置。

type Interval

type Interval int

Interval 时间轮转粒度。

const (
	None Interval = iota
	Hourly
	Daily
	Weekly
	Monthly
)

type Level

type Level int8

Level 日志级别。

const (
	DebugLevel Level = iota - 1 // -1
	InfoLevel                   // 0
	WarnLevel                   // 1
	ErrorLevel                  // 2
	FatalLevel                  // 3
)

func ParseLevel

func ParseLevel(s string) (Level, error)

ParseLevel 解析级别字符串(大小写不敏感)。

func (Level) String

func (l Level) String() string

type Logger

type Logger interface {
	Debug(msg string, fields ...Field)
	Info(msg string, fields ...Field)
	Warn(msg string, fields ...Field)
	Error(msg string, fields ...Field)
	Fatal(msg string, fields ...Field) // 写完 Sync 后 os.Exit(1)
	With(fields ...Field) Logger
	WithContext(ctx context.Context) Logger
	// WithCaller 返回派生 Logger,动态开启/关闭自动 caller 注解。
	// enabled=false 时不再自动输出 caller 字段,便于调用方按自身逻辑
	// (如动态回溯栈帧)手动写入一个名为 "caller" 的字段。
	// 派生实例复用同一底层 core/sink,Close 仍只应在根 logger 上调用。
	WithCaller(enabled bool) Logger
	SetLevel(l Level)
	Sync() error
	Close() error
}

Logger 日志器接口。日志方法绝不 panic、绝不返回 error。

func Default added in v0.3.3

func Default() Logger

Default 返回当前全局 Logger 实例;未 Init 时惰性装配默认 stderr logger。

func New

func New(cfg Config, opts ...Option) (Logger, error)

New 构造一个独立 Logger 实例。

func With

func With(fields ...Field) Logger

func WithContext

func WithContext(ctx context.Context) Logger

type Option

type Option func(*Config)

Option 以 functional options 方式修改 Config。

func WithBuffer added in v0.2.0

func WithBuffer(sizeKB int, flushInterval time.Duration) Option

WithBuffer 启用文件写缓冲:sizeKB 为缓冲大小(KB),<=0 表示关闭缓冲(默认)。 flushInterval 为定时 flush 间隔,为 0 时启用缓冲则采用 zap 默认值(30s)。 开启后会有一个后台定时 flush goroutine,Close 时停止;详见 FileConfig.BufferSize。

func WithCaller

func WithCaller(on bool) Option

func WithCallerEncoder added in v0.3.0

func WithCallerEncoder(enc CallerEncoder) Option

func WithConsole

func WithConsole(on bool) Option

func WithConsoleSeparator

func WithConsoleSeparator(sep string) Option

WithConsoleSeparator 定制 console 编码的字段分隔符(仅 Encoding="console" 生效), 为空则沿用 zap 默认的制表符。

func WithEncoding

func WithEncoding(e string) Option

func WithFile

func WithFile(f FileConfig) Option

func WithLevel

func WithLevel(l Level) Option

func WithSplitLevel

func WithSplitLevel(on bool) Option

func WithTimeFormat

func WithTimeFormat(f string) Option

func WithWriteErrorHook added in v0.2.0

func WithWriteErrorHook(hook func(error)) Option

WithWriteErrorHook 注册日志文件写入失败时的回调(如磁盘写满、IO 错误)。 回调会在写入路径上同步触发、可能被并发调用,必须并发安全, 且不要在其中再写本 logger,以免递归。

type Rotator

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

Rotator 自研文件轮转写入器,支持大小+时间叠加轮转。

func NewRotator

func NewRotator(cfg RotatorConfig) *Rotator

func (*Rotator) Close

func (r *Rotator) Close() error

func (*Rotator) Sync

func (r *Rotator) Sync() error

func (*Rotator) Write

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

type RotatorConfig

type RotatorConfig struct {
	Filename   string
	MaxSize    int // MB,0 表示不按大小轮转
	MaxBackups int // 保留个数,0 表示不限
	MaxAge     int // 保留天数,0 表示不限
	Compress   bool
	Interval   Interval
}

RotatorConfig 轮转配置。

Directories

Path Synopsis
examples
basic command
Example: basic
Example: basic
buffered command
Example: buffered
Example: buffered
context command
Example: context
Example: context
file-rotation command
Example: file-rotation
Example: file-rotation
gin command
Example: gin
Example: gin
instance command
Example: instance
Example: instance
middleware

Jump to

Keyboard shortcuts

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