- Four log levels:
DEBUGINFOWARNERROR - Global default or custom instances:
logs.New(w)or package-level functions - Structured field chains:
With().Str("k","v").Int("n",1).Info() - Namespace (Trace):
Trace("api").Info()→trace=api - Distributed tracing:
TraceCtx/TraceId/Ctx - Auto-hijack stdlib
log:New()converts stdlog → logfmt automatically - Stdlib-compatible signatures:
Print/Printf/Println - File output: daily rotation, configurable max age/size, optional console mirroring
- High performance: zero-allocation fast path,
sync.Poolbuffer reuse
Version note: v0.12.x requires Go 1.24+.
Go 1.20 users should use v0.11.x (go get github.com/zxysilent/logs@v0.11).
package main
import (
"context"
stdlog "log"
"github.com/zxysilent/logs"
)
func main() {
// === Default global instance ===
logs.SetLevel(logs.LevelDebug) // LevelDebug for dev, LevelInfo for production
logs.SetCaller(true)
logs.Info("hello world")
// === Namespace (Trace) ===
apiLog := logs.Trace("api")
apiLog.Info("server started") // trace=api
ctx := logs.TraceCtx(context.Background(), "req-1")
apiLog.Ctx(ctx).Info("handle") // trace=api.req-1
// === Structured fields ===
logs.With().
Str("user", "alice").
Int("age", 30).
Info("user login")
// === Distributed tracing ===
ctx = logs.TraceCtx(context.Background())
logs.Ctx(ctx).Str("op", "query").Debug("trace")
// === Stdlib compatibility ===
logs.Print("stdlib", "message") // msg=stdlibmessage
logs.Printf("stdlib %s", "format") // msg=stdlib format
stdlog.Println("auto hijacked to logfmt") // hijacked by New()
// === Custom instance (file output) ===
// Custom instances have correct caller skip; add WithSkip(1) if wrapped in a helper
w, closeFn := logs.NewFile("./logs/app.log", logs.WithMaxAge(7), logs.WithMaxSize(64), logs.WithConsole(true))
defer closeFn()
applog := logs.New(w, logs.WithLevel(logs.LevelInfo))
applog.Info("app started")
}Numerically aligned with log/slog (higher = more severe).
| Constant | Value | Description |
|---|---|---|
logs.LevelDebug |
-4 | Debug |
logs.LevelInfo |
0 | Info |
logs.LevelWarn |
4 | Warning |
logs.LevelError |
8 | Error |
logs.LevelMute |
20241020 | Disables all output (sentinel) |
LDEBUG/LINFO/LWARN/LERROR/LNONEare deprecated and will be removed in a future major version.
ParseLevel converts a case-insensitive string to a Level:
logs.ParseLevel("debug") // LevelDebug
logs.ParseLevel("WARN") // LevelWarn
logs.ParseLevel("OFF") // LevelMute
// Accepts: D/DBG/DEBUG/-4, I/INF/INFO/0, W/WRN/WARN/WARNING/4, E/ERR/ERROR/8, OFF/NONE/MUTENote:
Set*functions should be configured once before logging starts. Runtime modification is NOT recommended — config writes are unsynchronized and may race with concurrent log output. Configure during initialization and use immutableNew()instances for runtime use.
logs.SetLevel(lv Level) // set log level
logs.SetCaller(b bool) // enable/disable caller line
logs.SetSep(sep ...string) // path separators, default "/" (right-most match wins)
logs.SetSkip(skip int) // extra caller skip frames
logs.SetOutput(out io.Writer) // set output writer
logs.SetFile(path string) // set file output
logs.SetMaxAge(ma int) // max retention days, default 64
logs.SetMaxSize(ms int64) // max file size (MiB), default 64
logs.SetConsole(b bool) // also print to stderr (recommended)
logs.SetTrace(trace string) // set namespace on default instance
logs.Close() error // close
// Output
logs.Debug(args ...any)
logs.Debugf(format string, args ...any)
logs.Info(args ...any)
logs.Infof(format string, args ...any)
logs.Warn(args ...any)
logs.Warnf(format string, args ...any)
logs.Error(args ...any)
logs.Errorf(format string, args ...any)
// Stdlib compatibility
logs.Print(args ...any)
logs.Println(args ...any)
logs.Printf(format string, args ...any)
// Field chain / tracing
logs.With(trace ...string) *fielder
logs.Ctx(ctx context.Context) *fielder
// Namespace / sub-Logger
logs.Trace(trace string) *Logger // replace namespace
logs.Clone(trace ...string) *Logger // copy (no args) or append tracePrefer the package-level default instance. It requires no initialization and caller skip is already correct.
A New logger is configured once via functional options and is immutable afterwards
(no Set* methods). For runtime-mutable config, use the package-level default instance.
// Construct with options (out=nil means Discard)
l := logs.New(w,
logs.WithLevel(logs.LevelDebug),
logs.WithCaller(true),
logs.WithSep("/internal", "/"),
logs.WithSkip(0),
logs.WithHijack(true), // default true; false to disable stdlib hijack
)
// If your custom instance is wrapped in a helper, add WithSkip(1) so caller
// points to the actual call site:
helper := func(msg string) {
l.Info(msg)
}
_ = logs.New(w, logs.WithCaller(true), logs.WithSkip(1))
_ = helper // caller(file:line) points to the caller of helper("msg")
// File output: NewFile returns the Writer + a close handle; optional WithMaxAge/WithMaxSize/WithConsole
w, closeFn := logs.NewFile("app.log", logs.WithMaxAge(7), logs.WithMaxSize(64), logs.WithConsole(true))
defer closeFn()
fl := logs.New(w)
l.Debug(...) l.Debugf(...) l.Info(...) l.Infof(...)
l.Warn(...) l.Warnf(...) l.Error(...) l.Errorf(...)
l.Print(...) l.Println(...) l.Printf(...)
l.With(trace ...string) *fielder l.Ctx(ctx) *fielder
l.Trace(trace string) *Logger // namespaced sub-logger (shares root config)
l.Clone(trace ...string) *Logger // copy (no args) or append traceTrace/Clone derive a sub-Logger that shares the parent's root Config.
api := logs.Trace("api") // *Logger, trace=api (replace)
pay := api.Clone("pay") // *Logger, trace=api.pay (append)
api.Debug(...) api.Info(...) api.Warn(...) api.Error(...) api.Print(...)
api.With() *fielder // derive a one-shot fielder, inherits attr+trace
api.Ctx(ctx context.Context) *fielder
// trace = ns (no ctx) or ns.trace (with ctx)
// Freeze a field chain into a persistent, reusable, concurrency-safe *Logger:
base := logs.With().Str("svc", "api").Int("pid", 1).Group() // *Logger
base.Info("started") // svc=api pid=1, not released, reusable
base.With().Int("uid", 9).Info("login")// Fields
fl.Str(key, val string) fl.Stringer(key string, val fmt.Stringer)
fl.Bytes(key string, val []byte) fl.Err(err error) fl.IfErr(err error)
fl.Bool(key string, b bool)
fl.Int(key string, i int) fl.Int8(key, i int8) fl.Int16(key, i int16)
fl.Int32(key, i int32) fl.Int64(key, i int64)
fl.Uint(key, i uint) fl.Uint8(key, i uint8) fl.Uint16(key, i uint16)
fl.Uint32(key, i uint32) fl.Uint64(key, i uint64)
fl.Float32(key string, f float32) fl.Float64(key string, f float64)
fl.Time(key string, t time.Time) fl.Dur(key string, d time.Duration)
fl.Any(key string, i any) fl.Raw(key string, b []byte)
// Control
fl.If(b bool) // conditional output
fl.Caller(b bool) // per-entry caller control
// Freeze into a reusable *Logger
fl.Group() *Logger // persist field chain (no manual release)
// Terminal methods (fielder is recycled after call)
fl.Debug(args ...any) fl.Debugf(format string, args ...any)
fl.Info(args ...any) fl.Infof(format string, args ...any)
fl.Warn(args ...any) fl.Warnf(format string, args ...any)
fl.Error(args ...any) fl.Errorf(format string, args ...any)ctx := logs.TraceCtx(context.Background()) // generate new trace
ctx := logs.TraceCtx(context.Background(), "myid") // use specified id
ctx = logs.TraceCtx(ctx, "child") // append → myid.child
ctx = logs.TraceCtx(ctx) // reuse existing trace
traceId := logs.TraceOf(ctx) // read trace
id := logs.TraceId() // generate standalone id// Auto-hijack — New() calls hijackstd(), converting stdlog → logfmt
// prefix is captured as log namespace
stdlog.SetPrefix("myprefix")
_ = logs.New(nil) // hijack reads prefix → trace=myprefix
stdlog.Println("hello") // output: trace=myprefix level=INF msg=hello
// Print compat — package-level Print/Printf/Println → logfmt
logs.Print("a", "b") // msg=ab
logs.Printf("%s:%d", "k", 1) // msg=k:1time=2026-01-01T12:00:00.000 level=INF msg="hello world"
time=2026-01-01T12:00:00.000 level=INF trace=api.req-1 caller=/main.go:42 user=alice msg=login
time=2026-01-01T12:00:00.000 level=ERR trace=api error="something failed" msg="request failed"
time/levelalways presenttrace— present when tracing/namespace is usedcaller— present whenSetCaller(true)is set (file:line)error— present whenErr/IfErris called
db.AddHook(&repoHook{showSql: true})
type repoHook struct { showSql bool }
func (rh *repoHook) BeforeProcess(ctx *contexts.ContextHook) (context.Context, error) {
return ctx.Ctx, nil
}
func (rh *repoHook) AfterProcess(ctx *contexts.ContextHook) error {
if ctx.Err != nil {
logs.Ctx(ctx.Ctx).Err(ctx.Err).Str("SQL", ctx.SQL).
Any("args", ctx.Args).Dur("dur", ctx.ExecuteTime).Error()
} else if ctx.ExecuteTime > 200*time.Millisecond {
logs.Ctx(ctx.Ctx).Str("SlowSQL", ctx.SQL).
Any("args", ctx.Args).Dur("dur", ctx.ExecuteTime).Warn()
} else if rh.showSql {
logs.Ctx(ctx.Ctx).Str("SQL", ctx.SQL).
Any("args", ctx.Args).Dur("dur", ctx.ExecuteTime).Debug()
}
return ctx.Err
}pkg: github.com/zxysilent/logs
cpu: 12th Gen Intel(R) Core(TM) i5-12500H
count: average of 3 runs
BenchmarkDisabled 1.0 ns/op, 0 B/op, 0 allocs // level filter fast path
BenchmarkParallelSimple 12 ns/op, 0 B/op, 0 allocs // parallel bare output
BenchmarkParallelSpan 62 ns/op, 0 B/op, 0 allocs // parallel Trace + output
BenchmarkParallel 60 ns/op, 0 B/op, 0 allocs // parallel With 7 fields
BenchmarkSimple 50 ns/op, 0 B/op, 0 allocs // basic Info()
BenchmarkError 106 ns/op, 0 B/op, 0 allocs // Error log
BenchmarkInfof 100 ns/op, 16 B/op, 1 allocs // formatted output
BenchmarkWith5Fields 187 ns/op, 0 B/op, 0 allocs // 5 structured fields
BenchmarkWith10Fields 289 ns/op, 0 B/op, 0 allocs // 10 structured fields
BenchmarkSimpleCaller 284 ns/op, 0 B/op, 0 allocs // Info + caller
BenchmarkParallelFile 309 ns/op, 0 B/op, 0 allocs // parallel file write