Documentation
¶
Overview ¶
Package zerowrap provides a reusable wrapper around zerolog for context-based logging.
It simplifies common logging patterns by providing:
- Context-based logger storage and retrieval
- A Logger type with error wrapping helpers
- Field enrichment (single, multiple, or from structs)
- Configurable logger creation with sensible defaults
- File-based logging with rotation support
- OpenTelemetry integration (optional sub-package)
Logger Type ¶
The Logger type wraps zerolog.Logger and provides additional convenience methods. All zerolog.Logger methods are available via embedding:
type Logger struct {
zerolog.Logger
}
Logger methods:
log.WrapErr(err, msg) error // Log and wrap error log.WrapErrWithFields(err, msg, fields) error // Log with fields and wrap log.WrapErrf(err, format, args...) error // Log and wrap with formatted message log.WithField(key, value) Logger // Return logger with added field log.WithFields(fields) Logger // Return logger with added fields log.WithStruct(s) Logger // Return logger with fields from struct
Quick Start ¶
// Create and attach logger to context
logger := zerowrap.New(zerowrap.Config{
Level: "debug",
Format: "console",
})
ctx := zerowrap.WithCtx(context.Background(), logger)
// Use throughout your application
log := zerowrap.FromCtx(ctx)
log.Info().Msg("hello world")
Context Functions ¶
Store and retrieve loggers from context:
FromCtx(ctx) Logger // Get logger from context (no-op if none) Ctx(ctx) *zerolog.Logger // Get pointer to underlying zerolog.Logger WithCtx(ctx, log) context.Context // Attach logger to context WithCtxZerolog(ctx, log) context.Context // Attach zerolog.Logger to context
Field Helpers ¶
Get logger with additional fields:
FromCtxWithField(ctx, key, value) Logger // One field FromCtxWithFields(ctx, fields) Logger // Multiple fields FromCtxWithStruct(ctx, s) Logger // Fields from struct tags
Get new context with enriched logger:
CtxWithField(ctx, key, value) context.Context CtxWithFields(ctx, fields) context.Context CtxWithStruct(ctx, s) context.Context
Struct Tags ¶
Extract fields from structs using the `log` tag (falls back to `json`, then field name):
type Request struct {
UserID int `log:"user_id"`
RequestID string `log:"request_id"`
IP string `json:"ip_address"`
Internal string `log:"-"` // skipped
}
log := zerowrap.FromCtxWithStruct(ctx, Request{UserID: 123, RequestID: "abc"})
Logger Creation ¶
Create loggers with configuration:
New(cfg Config) Logger // Create with config NewFromEnv(prefix string) Logger // Create from env vars NewWithFile(cfg, fileCfg) (Logger, func(), error) // Create with file output Default() Logger // Default logger (info, console) WithHook(log, hook) Logger // Add hook to logger
Config ¶
Configuration for logger creation:
type Config struct {
Level string // trace, debug, info, warn, error, fatal, panic
Format string // json or console
TimeFormat string // time format (default: time.RFC3339)
Output io.Writer // output writer (default: os.Stderr)
Caller bool // include caller info (file:line)
}
FileConfig ¶
Configuration for file-based logging with rotation and app-managed paths. Use keyed fields when constructing FileConfig values; unkeyed composite literals are not supported as this exported configuration struct may grow:
type FileConfig struct {
Enabled bool // toggle file logging
Path string // full explicit file path override (takes priority when set)
AppName string // enables app-managed log paths when Path is empty
BaseDir string // overrides root directory before AppName when using app-managed paths
Name string // log file name (defaults to "app" with .log extension if missing)
Mode FileMode // app-managed file layout: "single" or "session" (default: "single")
FileFormat FileFormat // file output format: "json" or "console" (default: "json")
MaxSize int // max size in MB before rotation (default: 100)
MaxBackups int // max old files to retain (default: 3)
MaxAge int // max days to retain (default: 28)
Compress bool // compress rotated files
}
Error Helpers ¶
Log and return wrapped errors in one line:
func doSomething(ctx context.Context) error {
log := zerowrap.FromCtx(ctx)
// Simple wrap
if err != nil {
return log.WrapErr(err, "failed to connect")
}
// With fields
if err != nil {
return log.WrapErrWithFields(err, "query failed", map[string]any{
"table": tableName,
})
}
// With formatted message
if err != nil {
return log.WrapErrf(err, "failed to connect to %s", host)
}
}
Field Constants ¶
Common field names for consistency:
// Identity & Tracing FieldComponent, FieldRequestID, FieldTraceID, FieldSpanID FieldCorrelationID, FieldSessionID, FieldUserID // HTTP/API FieldMethod, FieldPath, FieldStatus, FieldClientIP // Service/Infra FieldService, FieldVersion, FieldHost, FieldEnv // Operations FieldAction, FieldOperation, FieldError, FieldDuration // Data FieldCount, FieldSize // Clean Architecture - Layers FieldLayer, FieldUseCase // Clean Architecture - Adapters FieldAdapter, FieldAdapterType, FieldHandler, FieldRepository, FieldGateway // Database/Storage FieldTable, FieldQuery, FieldDatabase // Messaging/Events FieldEvent, FieldTopic, FieldQueue, FieldPayload // Entity/Resource FieldEntity, FieldEntityID, FieldEntityType
Usage:
ctx = zerowrap.CtxWithField(ctx, zerowrap.FieldComponent, "database")
ctx = zerowrap.CtxWithFields(ctx, map[string]any{
zerowrap.FieldRequestID: requestID,
zerowrap.FieldUserID: userID,
})
Environment Variables ¶
Create logger from environment variables:
// Reads MYAPP_LOG_LEVEL and MYAPP_LOG_FORMAT
log := zerowrap.NewFromEnv("MYAPP")
File Logging ¶
Create logger with file output and rotation:
log, cleanup, err := zerowrap.NewWithFile(
zerowrap.Config{Level: "info", Format: "console"},
zerowrap.FileConfig{
Enabled: true,
Path: "/var/log/app.log",
MaxSize: 100, // MB
MaxBackups: 3,
MaxAge: 28, // days
Compress: true,
},
)
if err != nil {
panic(err)
}
defer cleanup()
App-Managed File Paths ¶
When Path is empty and AppName is set, the library automatically resolves log paths using OS-specific conventions:
Linux: $XDG_STATE_HOME/<app>/logs/<name>.log (falls back to ~/.local/state/<app>/logs/<name>.log) macOS: ~/Library/Logs/<app>/<name>.log Windows: %LOCALAPPDATA%\<app>\Logs\<name>.log
The Path field always takes priority as an explicit full-path override. Directories are created automatically for app-managed paths.
FileModeSingle writes <root>/<name>.log.
FileModeSession writes <root>/sessions/<session-id>/<name>.log and
shares the session directory across loggers in the same process.
FileFormatConsole writes human-readable file output without ANSI colors.
cfg := zerowrap.Config{Level: "info", Format: "console"}
fileCfg := zerowrap.FileConfig{
Enabled: true,
AppName: "MyApp",
Mode: zerowrap.FileModeSession,
FileFormat: zerowrap.FileFormatConsole,
}
path, err := zerowrap.ResolveLogPath(fileCfg)
if err != nil {
panic(err)
}
_ = path
log, cleanup, err := zerowrap.NewWithFile(cfg, fileCfg)
if err != nil {
panic(err)
}
defer cleanup()
_ = log
OpenTelemetry Integration ¶
For OpenTelemetry log bridging, use the optional otel sub-package:
import "github.com/bnema/zerowrap/otel"
// Using global provider
log := zerowrap.New(cfg).Hook(otel.NewHook("my-service"))
// Using custom provider
log := zerowrap.New(cfg).Hook(otel.NewHookWithProvider(provider, "my-service"))
Field Propagation Pattern ¶
The key pattern is to enrich the context with fields EARLY (at request entry points), so all downstream code automatically includes those fields in logs:
// Middleware: add request-scoped fields once
func RequestMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Define fields early - they propagate to ALL downstream logs
ctx = zerowrap.CtxWithFields(ctx, map[string]any{
zerowrap.FieldRequestID: uuid.New().String(),
zerowrap.FieldMethod: r.Method,
zerowrap.FieldPath: r.URL.Path,
zerowrap.FieldClientIP: r.RemoteAddr,
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Handler: add user-specific fields after auth
func UserHandler(ctx context.Context, userID string) error {
// Add user field - now ALL downstream logs include user_id
ctx = zerowrap.CtxWithField(ctx, zerowrap.FieldUserID, userID)
// This log has: request_id, method, path, client_ip, user_id
zerowrap.FromCtx(ctx).Info().Msg("user authenticated")
return processUser(ctx) // ctx carries all fields downstream
}
// Service layer: just use the context, fields are already there
func processUser(ctx context.Context) error {
log := zerowrap.FromCtx(ctx)
// This log automatically has ALL parent fields
log.Debug().Msg("processing user")
if err := db.Query(ctx); err != nil {
// Error log includes all fields: request_id, method, path, client_ip, user_id
return log.WrapErr(err, "database query failed")
}
return nil
}
Complete Example ¶
func main() {
// Create logger with service-level fields
logger := zerowrap.New(zerowrap.Config{
Level: "debug",
Format: "console",
})
ctx := zerowrap.WithCtx(context.Background(), logger)
// Service-level fields: present in ALL logs
ctx = zerowrap.CtxWithFields(ctx, map[string]any{
zerowrap.FieldService: "my-api",
zerowrap.FieldVersion: "1.0.0",
zerowrap.FieldEnv: "production",
})
// Start server with enriched context
server.Start(ctx)
}
func HandleRequest(ctx context.Context, req *Request) error {
// Request-level fields: added at entry point
ctx = zerowrap.CtxWithFields(ctx, map[string]any{
zerowrap.FieldRequestID: req.ID,
zerowrap.FieldUserID: req.UserID,
zerowrap.FieldPath: req.Path,
})
log := zerowrap.FromCtx(ctx)
log.Info().Msg("request started")
// All downstream calls use same enriched context
if err := validateRequest(ctx, req); err != nil {
return log.WrapErr(err, "validation failed")
}
if err := processRequest(ctx, req); err != nil {
return log.WrapErr(err, "processing failed")
}
log.Info().Msg("request completed")
return nil
}
func validateRequest(ctx context.Context, req *Request) error {
log := zerowrap.FromCtx(ctx)
// Log automatically includes: service, version, env, request_id, user_id, path
log.Debug().Msg("validating request")
return nil
}
func processRequest(ctx context.Context, req *Request) error {
log := zerowrap.FromCtx(ctx)
// Same fields propagated here too
log.Debug().Msg("processing request")
if err := callExternalAPI(ctx); err != nil {
// Error includes all context fields for easy debugging
return log.WrapErr(err, "external API call failed")
}
return nil
}
Index ¶
- Constants
- func Ctx(ctx context.Context) *zerolog.Logger
- func CtxWithField(ctx context.Context, key string, value any) context.Context
- func CtxWithFields(ctx context.Context, fields map[string]any) context.Context
- func CtxWithStruct(ctx context.Context, s any) context.Context
- func ResolveLogPath(fileCfg FileConfig) (string, error)
- func WithCtx(ctx context.Context, log Logger) context.Context
- func WithCtxZerolog(ctx context.Context, log zerolog.Logger) context.Context
- type Config
- type FileConfig
- type FileFormat
- type FileMode
- type Logger
- func Default() Logger
- func FromCtx(ctx context.Context) Logger
- func FromCtxWithField(ctx context.Context, key string, value any) Logger
- func FromCtxWithFields(ctx context.Context, fields map[string]any) Logger
- func FromCtxWithStruct(ctx context.Context, s any) Logger
- func New(cfg Config) Logger
- func NewFromEnv(prefix string) Logger
- func NewWithFile(cfg Config, fileCfg FileConfig) (Logger, func(), error)
- func WithHook(log Logger, hook zerolog.Hook) Logger
- func (l Logger) WithField(key string, value any) Logger
- func (l Logger) WithFields(fields map[string]any) Logger
- func (l Logger) WithStruct(s any) Logger
- func (l Logger) WrapErr(err error, msg string) error
- func (l Logger) WrapErrWithFields(err error, msg string, fields map[string]any) error
- func (l Logger) WrapErrf(err error, format string, args ...any) error
Constants ¶
const ( // Identity & Tracing FieldComponent = "component" FieldRequestID = "request_id" FieldTraceID = "trace_id" FieldSpanID = "span_id" FieldCorrelationID = "correlation_id" FieldSessionID = "session_id" FieldUserID = "user_id" // HTTP/API FieldMethod = "method" FieldPath = "path" FieldStatus = "status" FieldClientIP = "client_ip" // Service/Infra FieldService = "service" FieldVersion = "version" FieldHost = "host" FieldEnv = "env" // Operations FieldAction = "action" FieldOperation = "operation" FieldError = "error" FieldDuration = "duration_ms" // Data FieldCount = "count" FieldSize = "size_bytes" // Clean Architecture - Layers FieldLayer = "layer" // domain, usecase, adapter FieldUseCase = "usecase" // e.g., "CreateUser", "GetOrder" // Clean Architecture - Adapters FieldAdapter = "adapter" // e.g., "http", "grpc", "postgres" FieldAdapterType = "adapter_type" // "in" (driving) or "out" (driven) FieldHandler = "handler" // HTTP/gRPC handler name FieldRepository = "repository" // repository name FieldGateway = "gateway" // external service gateway // Database/Storage FieldTable = "table" FieldQuery = "query" FieldDatabase = "database" // Messaging/Events FieldEvent = "event" FieldTopic = "topic" FieldQueue = "queue" FieldPayload = "payload" // Entity/Resource FieldEntity = "entity" // e.g., "user", "order" FieldEntityID = "entity_id" // ID of the entity being operated on FieldEntityType = "entity_type" // type of entity )
Common field name constants for consistent logging across applications.
Variables ¶
This section is empty.
Functions ¶
func Ctx ¶
Ctx returns a pointer to the underlying zerolog.Logger in context. This is for compatibility with zerolog's Ctx pattern. If no logger is found, returns a pointer to a disabled logger.
func CtxWithField ¶
CtxWithField returns a new context with an enriched logger containing the field.
func CtxWithFields ¶
CtxWithFields returns a new context with an enriched logger containing the fields.
func CtxWithStruct ¶
CtxWithStruct returns a new context with an enriched logger containing fields from struct.
func ResolveLogPath ¶ added in v1.4.0
func ResolveLogPath(fileCfg FileConfig) (string, error)
ResolveLogPath resolves the log file path from the given FileConfig.
If Path is set, it is returned directly and no other fields are validated. If Path is empty and AppName is empty, an empty path is returned with no error. Otherwise, an app-managed path is built from AppName, defaulting Name to "app", Mode to FileModeSingle, and FileFormat to FileFormatJSON.
Types ¶
type Config ¶
type Config struct {
// Level is the minimum log level (trace, debug, info, warn, error, fatal, panic).
// Defaults to "info" if empty or invalid.
Level string
// Format is the output format: "json" or "console".
// Defaults to "console" if empty or invalid.
Format string
// TimeFormat is the time format string.
// Defaults to time.RFC3339 if empty.
TimeFormat string
// Output is the writer for log output.
// Defaults to os.Stderr if nil.
Output io.Writer
// Caller adds caller information (file:line) to log entries.
Caller bool
}
Config holds logger configuration options.
type FileConfig ¶
type FileConfig struct {
// Enabled toggles file logging on/off.
Enabled bool
// Path is the full log file path. If set, it takes priority over app-managed path fields.
Path string
// AppName enables app-managed log paths when Path is empty.
AppName string
// BaseDir overrides the app-managed root directory before AppName.
BaseDir string
// Name is the log file name. Defaults to "app". The .log extension is added if missing.
Name string
// Mode controls app-managed file layout. Defaults to FileModeSingle.
Mode FileMode
// FileFormat controls the file output format. Defaults to FileFormatJSON.
FileFormat FileFormat
// MaxSize is the maximum size in megabytes before rotation.
// Defaults to 100 MB if 0.
MaxSize int
// MaxBackups is the maximum number of old log files to retain.
// Defaults to 3 if 0.
MaxBackups int
// MaxAge is the maximum number of days to retain old log files.
// Defaults to 28 if 0.
MaxAge int
// Compress determines if rotated files should be compressed.
Compress bool
}
FileConfig holds configuration for file-based logging. Use keyed fields when constructing FileConfig values; unkeyed composite literals are not supported as this exported configuration struct may grow.
type FileFormat ¶ added in v1.4.0
type FileFormat string
FileFormat controls the format used for the file writer.
const ( // FileFormatJSON writes JSON lines to the log file. FileFormatJSON FileFormat = "json" // FileFormatConsole writes human-readable console output to the log file. FileFormatConsole FileFormat = "console" )
type FileMode ¶ added in v1.4.0
type FileMode string
FileMode controls how app-managed log file paths are laid out.
type Logger ¶ added in v1.1.0
Logger wraps zerolog.Logger with additional convenience methods. All zerolog.Logger methods are available via embedding.
func Default ¶
func Default() Logger
Default returns a sensible default logger writing to stderr with console format.
func FromCtx ¶
FromCtx extracts the logger from context. If no logger is found, returns a disabled (no-op) logger.
func FromCtxWithField ¶
FromCtxWithField returns a logger with one additional field.
func FromCtxWithFields ¶
FromCtxWithFields returns a logger with multiple additional fields.
func FromCtxWithStruct ¶
FromCtxWithStruct returns a logger with fields extracted from struct tags. Uses `log` struct tag for field names, falls back to `json` tag, then field name. Fields tagged with "-" are skipped.
func NewFromEnv ¶
NewFromEnv creates a logger configured from environment variables. Uses {prefix}_LOG_LEVEL and {prefix}_LOG_FORMAT. Example: with prefix "MYAPP", reads MYAPP_LOG_LEVEL and MYAPP_LOG_FORMAT.
func NewWithFile ¶
func NewWithFile(cfg Config, fileCfg FileConfig) (Logger, func(), error)
NewWithFile creates a logger that writes to both stderr and a file. Returns the logger, a cleanup function that must be called to close the file, and any error encountered.
func (Logger) WithFields ¶ added in v1.1.0
WithFields returns a new Logger with the fields added.
func (Logger) WithStruct ¶ added in v1.1.0
WithStruct returns a new Logger with fields extracted from struct tags.
func (Logger) WrapErr ¶ added in v1.1.0
WrapErr logs the error and returns a wrapped error with the message. Uses fmt.Errorf with %w for unwrapping support.
log := zerowrap.FromCtx(ctx)
if err != nil {
return log.WrapErr(err, "failed to connect")
}
func (Logger) WrapErrWithFields ¶ added in v1.1.0
WrapErrWithFields logs with fields and returns a wrapped error.
if err != nil {
return log.WrapErrWithFields(err, "query failed", map[string]any{"id": id})
}