xlog

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: May 21, 2026 License: MIT Imports: 14 Imported by: 0

README

xlog

CI

Structured logger facade for Go with a zap-like field API and a pluggable backend contract.

The root package is user-facing. Core contracts and field extension points live in lower-level packages so backends and adapters never import the root package.

xlog/                user-facing logger and re-exports (single xlog.go)
pkg/core/            Core, Event, Level, Encoder contracts
pkg/field/           concrete Field type, built-in fields, custom helpers
pkg/sink/            io.Writer targets (file, multi, locked, std…)
pkg/http/            net/http middleware (package http, alias as xloghttp)
internal/consolecore dev-oriented human-readable backend
internal/jsoncore    JSON backend (github.com/go-faster/jx)
contrib/loggers/<name>/  opt-in logging-library adapters, each its own go.mod
contrib/libs/<name>/     opt-in non-logger library integrations (e.g. pgx)

Install

go get github.com/gopherex/xlog

Minimum Go: 1.25.0.

Quick start

import "github.com/gopherex/xlog"

logger := xlog.NewJSON(
    xlog.WithLevel(xlog.InfoLevel),
    xlog.WithFields(xlog.String("service", "api")),
)

logger.Info("user created",
    xlog.String("user_id", id),
    xlog.Int("attempt", attempt),
    xlog.Err(err),
)

xlog.Default() returns a JSON logger at info level writing to stdout. xlog.NewConsole(...) gives a human-readable dev formatter.

Core contract

Backends implement:

type Core interface {
    Enabled(Level) bool
    Write(Event) error
    With([]field.Field) Core
    Sync() error
}

Anything that implements core.Core from github.com/gopherex/xlog/pkg/core can be plugged in via xlog.New(core) or xlog.WithCore(core).

Sinks

file, err := sink.OpenFile("logs/app.log",
    sink.WithMaxSize(100*1024*1024),
    sink.WithMaxBackups(7),
    sink.WithCompress(true),
)
if err != nil { return err }
defer file.Close()

logger := xlog.NewJSON(xlog.WithSink(sink.NewMulti(os.Stdout, file)))

Available: sink.NewWriter, sink.NewMulti, sink.OpenFile, sink.NewDiscard, sink.NewLocked, sink.Stdout, sink.Stderr.

Core wrappers

level := xlog.NewAtomicLevel(xlog.InfoLevel)

logger := xlog.NewJSON(
    xlog.WithAtomicLevel(level),
    xlog.WithSampling(5, 10),
    xlog.WithAsync(1024, xlog.AsyncDropOldest),
    xlog.WithObserver(myObserver),
)

xlog.NewTeeCore, xlog.NewFilterCore, xlog.NewSamplerCore, xlog.NewHookCore, xlog.NewAsyncCore are exposed at the root.

Checked logging

if ce := logger.Check(xlog.DebugLevel, "request payload"); ce != nil {
    ce.Write(xlog.Any("payload", payload))
}

Disabled checked logs do not build the variadic field slice.

Context

ctx = xlog.IntoContext(ctx, logger)
ctx = xlog.ContextWithFields(ctx, xlog.String("request_id", id))

xlog.FromContext(ctx).Info("handled")

HTTP middleware

import xloghttp "github.com/gopherex/xlog/pkg/http"

handler := xloghttp.Middleware(logger)(mux)

Propagates X-Request-Id, stores logger in request context, logs method, path, status, duration, bytes, user agent, remote IP.

Pretty output

Two paths to human-readable logs without a separate binary (zap-pretty style):

1. WithPretty() — auto-switching encoder. For the root logger. JSON in prod, console in dev — same code:

logger := xlog.NewJSON(xlog.WithPretty()) // forces console layout

PrettyAuto (default) honors XLOG_PRETTY env first, then falls back to TTY detection on the writer:

XLOG_PRETTY Result
1 / true / yes / on pretty
0 / false / no / off raw JSON
unset pretty if stdout is a TTY

Explicit xlog.WithPretty() / xlog.WithoutPretty() override the env.

2. sink.NewPretty(w) — NDJSON reformatter. Wraps any io.Writer. Parses each JSON line, prints a colored single-line record, falls back to passthrough for non-JSON. Backend-agnostic — works with jsoncore, the zap/zerolog/slog contribs, or any external JSON logger:

out := sink.NewPretty(os.Stdout)

// our facade
logger := xlog.NewJSON(xlog.WithWriter(out))

// or any contrib that writes NDJSON
zl := zerolog.New(out)
xlog.New(zerologadapter.New(zl)).Info("hi")

Helper fields

xlog.Secret("token", token)
xlog.Email("email", email)
xlog.ErrorCause(err)
xlog.ErrorChain(err)
xlog.Errors("errs", errs)

Custom fields

type UserID string

func (id UserID) AppendXLog(enc field.Encoder, key string) {
    enc.String(key, string(id))
}

logger.Info("created", xlog.ValueOf("user_id", UserID("u1")))

One-off typed field:

logger.Info("created", xlog.Generic("user_id", id,
    func(enc field.Encoder, key string, id UserID) {
        enc.String(key, string(id))
    },
))

Fully manual encoding: xlog.CustomFn.


Contrib adapters

Adapters live under contrib/ as separate Go modules, so the root module stays free of third-party dependencies — you pull in only what you use. They are grouped by kind:

  • contrib/loggers/<name>/ — adapters for other logging libraries (slog, zap, zerolog, …), wired in both directions.
  • contrib/libs/<name>/ — integrations for non-logger libraries that emit their own logs (e.g. pgx), routed into xlog.
Installing an adapter

Each adapter has its own import path and go get:

go get github.com/gopherex/xlog/contrib/loggers/slog

Use it like any other Core:

import (
    "log/slog"
    "os"

    "github.com/gopherex/xlog"
    slogadapter "github.com/gopherex/xlog/contrib/loggers/slog"
)

handler := slog.NewJSONHandler(os.Stdout, nil)
logger  := xlog.New(slogadapter.New(handler))

logger.Info("started", xlog.String("service", "api"))
Available contribs

Each contrib is its own Go module — go get pulls only what you use. Both directions are supported: use xlog through a native backend (New), or expose xlog where the native logger's API is expected (NewSink…).

Path Forward (xlog uses native) Reverse (native uses xlog)
…/contrib/loggers/slog (log/slog) slog.New(handler) slog.NewSink(l) slog.Handler
…/contrib/loggers/zap (go.uber.org/zap) zap.New(zl) zap.NewSink(l) zapcore.Core
…/contrib/loggers/zerolog (rs/zerolog) zerolog.New(zl) zerolog.NewSinkWriter(l) io.Writer
…/contrib/loggers/logrus (sirupsen/logrus) logrus.New(lr) logrus.NewSinkHook(l) logrus.Hook
…/contrib/loggers/hclog (hashicorp/go-hclog) hclog.New(hc) hclog.NewSinkWriter(l) io.Writer
…/contrib/loggers/gokit (go-kit/log) gokit.New(kl) gokit.NewSink(l) kitlog.Logger
…/contrib/loggers/apex (apex/log) apex.New(al) apex.NewSinkHandler(l) apexlog.Handler
…/contrib/loggers/phuslu (phuslu/log) phuslu.New(pl) phuslu.NewSinkWriter(l) io.Writer
…/contrib/loggers/charm (charmbracelet/log) charm.New(cl) charm.NewSinkWriter(l) io.Writer
…/contrib/loggers/log15 (inconshreveable/log15.v2) log15.New(l15) log15.NewSinkHandler(l) l15.Handler

Non-logger library integrations live under contrib/libs/:

Path Integration
…/contrib/libs/pgx (jackc/pgx/v5) pgx.NewTracer(l) (*tracelog.TraceLog, error) — routes pgx query logs into xlog (ctx-aware).
…/contrib/libs/aws (aws-sdk-go-v2 / smithy-go) aws.New(l) logging.Logger — routes AWS SDK logs into xlog (ctx-aware).
…/contrib/libs/otel (go.opentelemetry.io/otel) otel.TraceFields (trace_id/span_id enrichment), otel.New(otellog.Logger) (OTLP logs bridge), otel.SpanObserver() (record errors on the active span).
OpenTelemetry

Three composable integrations:

import (
    "github.com/gopherex/xlog"
    otel "github.com/gopherex/xlog/contrib/libs/otel"
)

// A) Correlate every ctx log with the active trace.
log := xlog.NewJSON(
    xlog.WithContextFieldExtractor(otel.TraceFields), // adds trace_id/span_id
    xlog.WithObserver(otel.SpanObserver()),           // C) error logs -> span event + status
)
log.Ctx().Info(ctx, "handled")   // {... "trace_id":"…","span_id":"…"}

// B) Emit logs as an OTLP signal through the OTel Logs SDK.
xl := xlog.New(otel.New(loggerProvider.Logger("xlog")))

WithContextFieldExtractor is OTel-agnostic — it takes any func(context.Context) []xlog.Field, so the root package keeps no OTel dependency.

Forward example — xlog facade, zap backend:

zl := uzap.NewExample()
logger := xlog.New(zapcontrib.New(zl))
logger.Info("hi", xlog.String("k", "v"))

Reverse example — zap callers, xlog backend:

xl := xlog.NewJSON()
zl := uzap.New(zapcontrib.NewSink(xl))
zl.Info("hi", uzap.String("k", "v")) // xl receives it

Libraries without a structured hook surface (zerolog, phuslu, charm, hclog) expose NewSinkWriter — plug it into the native logger's io.Writer configured for JSON; the writer reparses each line into an xlog event.

Writing your own adapter

An adapter is a type implementing core.Core. Steps:

  1. Create a directory contrib/loggers/<name>/ (logging libraries) or contrib/libs/<name>/ (other libraries).

  2. Add a go.mod:

    cd contrib/loggers/<name>
    go mod init github.com/gopherex/xlog/contrib/loggers/<name>
    go get github.com/gopherex/xlog
    
  3. Implement core.Core. Skeleton:

    package <name>
    
    import (
        "github.com/gopherex/xlog/pkg/core"
        "github.com/gopherex/xlog/pkg/field"
    )
    
    type Core struct {
        inner   *external.Logger
        context []field.Field
    }
    
    func New(l *external.Logger) *Core { return &Core{inner: l} }
    
    func (c *Core) Enabled(level core.Level) bool {
        return c.inner.IsLevelEnabled(toExternalLevel(level))
    }
    
    func (c *Core) Write(e core.Event) error {
        // map e.Level, e.Message, e.Context + e.Fields → external API
        return nil
    }
    
    func (c *Core) With(fields []field.Field) core.Core {
        if len(fields) == 0 { return c }
        next := *c
        next.context = append([]field.Field(nil), c.context...)
        next.context = append(next.context, fields...)
        return &next
    }
    
    func (c *Core) Sync() error { return c.inner.Sync() }
    
  4. Map xlog fields to the external API. Switch on field.Kind (StringKind, BoolKind, Int64Kind, Uint64Kind, Float64Kind, DurationKind, TimeKind, ErrorKind, AnyKind, CustomKind) and call the matching accessor (StringValue(), Int64Value(), …). See contrib/loggers/slog/slog.go for a complete example.

  5. Add tests writing through xlog.New(yourcore.New(...)) and asserting on the external sink's output.

Local development of multiple modules

The repo ships a go.work so go build/go test see both the root module and every contrib module without publishing. To run everything:

go test ./... ./contrib/...

When adding a new contrib, append it to go.work:

use (
    .
    ./contrib/loggers/slog
    ./contrib/loggers/<new>
    ./contrib/libs/<new>
)

Logger options

  • xlog.WithLevel(level) / xlog.WithAtomicLevel(level) / xlog.WithLevelEnabler(leveler)
  • xlog.WithWriter(writer) / xlog.WithSink(writer)
  • xlog.WithFields(fields...)
  • xlog.WithClock(func() time.Time) / xlog.WithTimeLayout(layout)
  • xlog.WithEncoder(encoder) / xlog.WithCore(core)
  • xlog.WithObserver(observer)
  • xlog.WithCaller(true) / xlog.WithCallerSkip(skip)
  • xlog.WithStacktrace(level)
  • xlog.WithSampling(first, thereafter)
  • xlog.WithAsync(buffer, policy)
  • xlog.WithPretty() / xlog.WithoutPretty() (see Pretty output; respects XLOG_PRETTY env)

Documentation

Overview

Package xlog is a small structured logger facade over a pluggable Core.

The package-level API intentionally does not know about JSON, slog, zap, or any other concrete backend. Backends implement Core; adapters live in separate packages.

Index

Constants

View Source
const (
	TraceLevel    = xcore.TraceLevel
	DebugLevel    = xcore.DebugLevel
	InfoLevel     = xcore.InfoLevel
	WarnLevel     = xcore.WarnLevel
	ErrorLevel    = xcore.ErrorLevel
	CriticalLevel = xcore.CriticalLevel

	AsyncBlock      = xcore.AsyncBlock
	AsyncDropNewest = xcore.AsyncDropNewest
	AsyncDropOldest = xcore.AsyncDropOldest

	FieldTime    = xfield.TimeKey
	FieldLevel   = xfield.LevelKey
	FieldLogger  = xfield.LoggerKey
	FieldMessage = xfield.MessageKey
	FieldError   = xfield.ErrorKey
)

Variables

View Source
var (
	String   = xfield.String
	Bool     = xfield.Bool
	Int      = xfield.Int
	Int64    = xfield.Int64
	Uint     = xfield.Uint
	Uint64   = xfield.Uint64
	Float64  = xfield.Float64
	Duration = xfield.Duration
	Time     = xfield.Time
	Err      = xfield.Err
	Error    = xfield.Error
	Any      = xfield.Any
	Custom   = xfield.Custom
	CustomFn = xfield.CustomFunc
)

Functions

func ContextWithFields

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

func IntoContext

func IntoContext(ctx context.Context, logger *Logger) context.Context

Types

type AsyncPolicy

type AsyncPolicy = xcore.AsyncPolicy

type AtomicLevel

type AtomicLevel = xcore.AtomicLevel

func NewAtomicLevel

func NewAtomicLevel(level Level) *AtomicLevel

type CheckedEntry

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

func (*CheckedEntry) Write

func (e *CheckedEntry) Write(fields ...Field)

type Config

type Config struct {
	Level            Level
	Leveler          LevelEnabler
	Writer           io.Writer
	Fields           []Field
	Clock            func() time.Time
	TimeLayout       string
	Encoder          Encoder
	Core             Core
	Observer         Observer
	ContextFields    func(context.Context) []Field
	Caller           bool
	CallerSkip       int
	Stacktrace       *Level
	SampleFirst      uint64
	SampleThereafter uint64
	AsyncBuffer      int
	AsyncPolicy      AsyncPolicy
	UseAsync         bool
	Pretty           PrettyMode
}

func DefaultConfig

func DefaultConfig() Config

type ContextLogger

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

ContextLogger mirrors Logger but takes a context.Context as the first argument on every logging method. Each call attaches fields carried in the context (ContextWithFields) and passes the context through to the Event so context-aware backends (e.g. the slog adapter) and OTel-style integrations can read request-scoped values. The underlying Logger is bound once.

func (*ContextLogger) Critical

func (c *ContextLogger) Critical(ctx context.Context, msg string, fields ...Field)

func (*ContextLogger) Debug

func (c *ContextLogger) Debug(ctx context.Context, msg string, fields ...Field)

func (*ContextLogger) Enabled

func (c *ContextLogger) Enabled(level Level) bool

func (*ContextLogger) Error

func (c *ContextLogger) Error(ctx context.Context, msg string, fields ...Field)

func (*ContextLogger) Info

func (c *ContextLogger) Info(ctx context.Context, msg string, fields ...Field)

func (*ContextLogger) Level

func (c *ContextLogger) Level() Level

func (*ContextLogger) Log

func (c *ContextLogger) Log(ctx context.Context, level Level, msg string, fields ...Field)

Log writes at the given level. Entry point for adapters mapping an external level enum onto xlog at runtime.

func (*ContextLogger) Logger

func (c *ContextLogger) Logger() *Logger

Logger returns the underlying Logger.

func (*ContextLogger) Sync

func (c *ContextLogger) Sync() error

func (*ContextLogger) Trace

func (c *ContextLogger) Trace(ctx context.Context, msg string, fields ...Field)

func (*ContextLogger) Warn

func (c *ContextLogger) Warn(ctx context.Context, msg string, fields ...Field)

func (*ContextLogger) With

func (c *ContextLogger) With(fields ...Field) *ContextLogger

With returns a ContextLogger whose underlying Logger carries the extra fields.

type Core

type Core = xcore.Core

func NewAsyncCore

func NewAsyncCore(next Core, buffer int, policy AsyncPolicy, observer Observer) Core

func NewFilterCore

func NewFilterCore(next Core, leveler LevelEnabler) Core

func NewHookCore

func NewHookCore(next Core, observer Observer) Core

func NewSamplerCore

func NewSamplerCore(next Core, first, thereafter uint64) Core

func NewTeeCore

func NewTeeCore(cores ...Core) Core

type Encoder

type Encoder = xcore.Encoder

type Event

type Event = xcore.Event

type Field

type Field = xfield.Field

func Email

func Email(key, value string) Field

func ErrorCause

func ErrorCause(err error) Field

func ErrorChain

func ErrorChain(err error) Field

func Errors

func Errors(key string, errs []error) Field

func FieldsFromContext

func FieldsFromContext(ctx context.Context) []Field

func Generic

func Generic[T any](key string, value T, append xfield.AppendFunc[T]) Field

func Secret

func Secret(key, value string) Field

func ValueOf

func ValueOf[T xfield.Value](key string, value T) Field

type FieldEncoder

type FieldEncoder = xfield.Encoder

type Level

type Level = xcore.Level

func ParseLevel

func ParseLevel(s string) (Level, error)

ParseLevel parses "trace" | "debug" | "info" | "warn" | "warning" | "error" | "err" | "critical" | "crit" | "fatal" (case-insensitive).

type LevelEnabler

type LevelEnabler = xcore.LevelEnabler

type LevelReader

type LevelReader = xcore.LevelReader

type Logger

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

func Default

func Default() *Logger

Default returns a JSON logger at info level writing to stdout.

func FromContext

func FromContext(ctx context.Context) *Logger

func New

func New(core Core) *Logger

New constructs a Logger. The name is optional and can be set or extended later with Named, zap-style.

func NewConsole

func NewConsole(opts ...Option) *Logger

NewConsole constructs a human-readable Logger. Use Named to set/extend the name.

func NewJSON

func NewJSON(opts ...Option) *Logger

NewJSON constructs a JSON-backed Logger. Use Named to set/extend the name.

func (*Logger) AppendName

func (l *Logger) AppendName(sub string) *Logger

AppendName returns a child whose name is parent.name + "." + sub. Example: logger("api").AppendName("db") → "api.db". Empty sub returns the receiver unchanged.

func (*Logger) Check

func (l *Logger) Check(level Level, msg string) *CheckedEntry

func (*Logger) CheckCritical

func (l *Logger) CheckCritical(msg string) *CheckedEntry

func (*Logger) CheckDebug

func (l *Logger) CheckDebug(msg string) *CheckedEntry

func (*Logger) CheckError

func (l *Logger) CheckError(msg string) *CheckedEntry

func (*Logger) CheckInfo

func (l *Logger) CheckInfo(msg string) *CheckedEntry

func (*Logger) CheckTrace

func (l *Logger) CheckTrace(msg string) *CheckedEntry

func (*Logger) CheckWarn

func (l *Logger) CheckWarn(msg string) *CheckedEntry

func (*Logger) Core

func (l *Logger) Core() Core

func (*Logger) Critical

func (l *Logger) Critical(msg string, fields ...Field)

func (*Logger) Ctx

func (l *Logger) Ctx() *ContextLogger

Ctx returns a ContextLogger bound to l.

func (*Logger) Debug

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

func (*Logger) Enabled

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

func (*Logger) Error

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

func (*Logger) Info

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

func (*Logger) Level

func (l *Logger) Level() Level

Level reports the current minimum level. A dynamic leveler that implements LevelReader (e.g. *AtomicLevel) is read live, so it reflects Set; otherwise the configured static level is returned.

func (*Logger) Log

func (l *Logger) Log(level Level, msg string, fields ...Field)

Log writes at the given level. Primary entry point for adapters that map an external level enum onto xlog at runtime.

func (*Logger) Name

func (l *Logger) Name() string

Name returns the dotted logger name, or "" if unnamed.

func (*Logger) PrependName

func (l *Logger) PrependName(prefix string) *Logger

PrependName returns a child whose name is prefix + "." + parent.name. Useful for stamping an app-level root on top of a component-named logger (e.g. component owns "api.db", app prepends "main" → "main.api.db"). Empty prefix returns the receiver unchanged.

func (*Logger) ReplaceName

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

ReplaceName returns a child with name set to name, discarding any existing chain. Pass "" to clear the name entirely.

func (*Logger) Sync

func (l *Logger) Sync() error

func (*Logger) Trace

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

func (*Logger) Warn

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

func (*Logger) With

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

func (*Logger) WithContext

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

type NopCore

type NopCore = xcore.NopCore

type Observer

type Observer = xcore.Observer

type Option

type Option func(*Config)

func WithAsync

func WithAsync(buffer int, policy AsyncPolicy) Option

func WithAtomicLevel

func WithAtomicLevel(level *AtomicLevel) Option

func WithCaller

func WithCaller(enabled bool) Option

func WithCallerSkip

func WithCallerSkip(skip int) Option

func WithClock

func WithClock(clock func() time.Time) Option

func WithContextFieldExtractor

func WithContextFieldExtractor(fn func(context.Context) []Field) Option

WithContextFieldExtractor registers a function that derives extra fields from the context on every ContextLogger call (Info(ctx, ...) etc.). Use it to attach request-scoped data such as OTel trace_id/span_id to each log without coupling the core to any specific library. The plain Logger path has no context and is unaffected.

func WithCore

func WithCore(core Core) Option

func WithEncoder

func WithEncoder(encoder Encoder) Option

func WithFields

func WithFields(fields ...Field) Option

func WithLevel

func WithLevel(level Level) Option

func WithLevelEnabler

func WithLevelEnabler(leveler LevelEnabler) Option

func WithObserver

func WithObserver(observer Observer) Option

func WithPretty

func WithPretty() Option

func WithSampling

func WithSampling(first, thereafter uint64) Option

func WithSink

func WithSink(writer io.Writer) Option

func WithStacktrace

func WithStacktrace(level Level) Option

func WithTimeLayout

func WithTimeLayout(layout string) Option

func WithWriter

func WithWriter(writer io.Writer) Option

func WithoutPretty

func WithoutPretty() Option

type PrettyMode

type PrettyMode int

PrettyMode controls human-readable output for JSON loggers.

const (
	// PrettyAuto enables pretty output when XLOG_PRETTY env is truthy or, if
	// the env is unset, when the writer is a TTY. This is the default.
	PrettyAuto PrettyMode = iota
	// PrettyOn forces pretty output regardless of env or TTY.
	PrettyOn
	// PrettyOff forces raw JSON output regardless of env or TTY.
	PrettyOff
)

Directories

Path Synopsis
contrib
libs/aws module
libs/otel module
libs/pgx module
loggers/zap module
internal
pkg

Jump to

Keyboard shortcuts

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