logx

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

logx

Opinionated zerolog wrapper providing context-first structured logging, request logging middleware for echo, and consistent field naming across services. Extracted from core/pkg/logx so any repo can consume the same logging conventions without importing core.

go get github.com/theopenlane/logx

The model: loggers travel on the context

The package is built around one rule: log through the context, not through a global logger. A request-scoped (or job-scoped) logger is attached to the context.Context once — by the HTTP middleware, a job dispatcher, or your own setup code — and every log line downstream retrieves it from there. Fields attached along the way (request ID, organization, subject) then appear on every subsequent log line automatically, which is what makes production logs correlatable.

logx.FromContext(ctx).Info().Str("standard_id", id).Msg("standard updated")

FromContext never returns nil: if the context carries no logger (or a disabled one) it falls back to zerolog's global logger, so it is always safe to call. This is the method you will use most — prefer it over zerolog.Ctx, log.Ctx, or the global log.Info() in any code that has a context.

Two things ride on the context and it helps to keep them distinct:

  • The logger itself — a zerolog.Logger whose accumulated fields appear on every event it logs.
  • Durable fields (LogFields) — a plain map[string]any mirror of those fields that can be read back with FieldsFromContext. This exists so field state can cross process boundaries: a job queue can serialize the fields when a task is enqueued and restore them with WithFields on the worker side, keeping correlation intact across the hop.

Everyday usage

Adding fields
// one field
ctx = logx.WithField(ctx, logx.FieldOrganizationID, orgID)

// several at once
ctx = logx.WithFields(ctx, map[string]any{
	logx.FieldSubjectID: caller.SubjectID,
	logx.FieldOperation: "standard_update",
})

Both update the context logger and the durable field store, and both copy the underlying map so sibling contexts never share state. Use these for fields that should appear on every subsequent log line in the request or job; use plain zerolog chaining (.Str(...), .Int(...)) for fields that belong to a single event.

Canonical field names

Use the exported constants instead of string literals when a concept has one — this is what keeps organization_id from also appearing as org_id, owner_id, and org across services:

FieldRequestID, FieldOrganizationID, FieldSubjectID, FieldSubjectEmail, FieldCapabilities, FieldOperation, FieldRemoteIP, FieldUserAgent, FieldRequestProtocol, FieldTrueClientIP, FieldForwardedFor, FieldRealIP

Errors

Always attach errors with .Err(err) (never .Str("error", err.Error())) so error marshaling and stack traces work. For picking the level, the package classifies expected request-lifecycle failures — client disconnects and canceled work are not server faults and should not page anyone:

// logs at warn for benign errors (context.Canceled, closed/reset connections,
// sql.ErrTxDone, http.ErrAbortHandler), error for everything else
logx.ErrorEvent(ctx, err).Msg("rollback failed")

// same idea when an HTTP status is in hand: benign errors and 4xx land at warn
logx.FromContext(ctx).WithLevel(logx.RequestErrorLevel(err, status)).Err(err).Msg("request failed")

// the predicate, for your own branching
if logx.BenignError(err) { ... }
Seeding and bridging
// guarantee a context carries a logger (start of a job, background goroutine, etc.)
ctx = logx.SeedContext(ctx)

// hand a *slog.Logger to code that requires the standard library interface (e.g. echo v5's e.Logger);
// events flow through zerolog, which keeps level filtering and output unified
e.Logger = logx.SlogLogger(logger.Unwrap())

Setup at service startup

Call Configure once in main/server bootstrap; everything else flows from the context.

loggers := logx.Configure(logx.LoggerConfig{
	Level:         zerolog.InfoLevel, // zerolog.NoLevel keeps the existing global default
	Writer:        os.Stderr,         // defaults to os.Stdout
	Pretty:        cfg.PrettyLog,     // human-readable console output for local dev
	IncludeCaller: cfg.Debug,         // file:line on every event; costs a runtime.Caller per event
	IncludeStack:  true,              // render error stack traces (wires zerolog's pkgerrors marshaler)
	WithEcho:      true,              // also build the echo-compatible logger (loggers.Echo)
	SetGlobal:     true,              // make this the zerolog global, so FromContext fallback matches
})

Set SetGlobal in binaries (servers, CLIs, workers) so code paths without a context-carried logger still produce consistently formatted output. Leave it unset in tests and libraries.

Every configured logger stamps a severity field mapped for GCP Cloud Logging, so log-based metrics and alerting work without a fluentd rewrite: trace/debug → DEBUG, info → INFO, warn → WARNING, error → ERROR, fatal → CRITICAL, panic → ALERT.

Request logging middleware

Both middlewares do the same job: seed the request context with a request-scoped logger (so every handler and downstream call logs with correlation fields attached), then emit one request-summary event when the request completes, leveled via RequestErrorLevel.

echo v5 (github.com/labstack/echo/v5)
e.Use(logx.Middleware(logx.MiddlewareConfig{
	Logger:                logger,      // defaults to a stdout logger if nil
	RequestIDHeader:       "X-Request-ID", // header to read the id from (default X-Request-Id)
	RequestIDKey:          logx.FieldRequestID, // log key for the id (default request_id)
	HandleError:           true,
	AttachRequestMetadata: true,
}))

Place it after your request-ID middleware and before recovery, so panics are logged with the request-scoped logger.

echox (github.com/theopenlane/echox)
e.Use(middleware.RequestID())
e.Use(logx.LoggingMiddleware(logx.Config{
	Logger:                logger,
	HandleError:           true,
	AttachRequestMetadata: true,
}))
Behavior worth knowing
  • AttachRequestMetadata: true puts client origin metadata (remote_ip, user_agent, request_protocol, and the True-Client-IP / X-Forwarded-For / X-Real-IP headers when present) on the request-scoped logger and durable fields, so every log line in the request carries them. When unset, the metadata appears only on the final request-summary event.
  • HandleError: true forwards handler errors to the registered HTTPErrorHandler and then consumes them (returns nil), preventing the router from invoking the error handler a second time. Leave it false only if another middleware up the chain owns error handling.
  • The request-summary event includes host, method, uri, status, latency_ms, bytes_in, bytes_out, query, and referer; failed requests are leveled by RequestErrorLevel, so benign disconnects and 4xx responses log at warn rather than error.
  • Skipper short-circuits logging for matching requests (health checks, probes).

Do / don't

  • Do use logx.FromContext(ctx) everywhere a context exists; don't use the zerolog global or log.Ctx in request/job paths.
  • Do keep log statements on a single line; don't split builder chains across newlines.
  • Do use .Err(err) and ErrorEvent; don't stringify errors into ad hoc fields.
  • Do use the Field* constants for shared concepts; don't invent new spellings of an existing key.
  • Do classify expected failures (via BenignError/ErrorEvent) instead of logging every failure at error level.

Pretty console output

Pretty: true routes output through the consolelog subpackage — a colorized, human-ordered console writer for local development. Production should always use the default JSON output.

Level conversion

MatchEchoLevel converts a gommon/echo log level to its zerolog equivalent, for wiring echox-configured services:

zlvl, _ := logx.MatchEchoLevel(log.WARN) // zerolog.WarnLevel

Composing outputs

Configure accepts any io.Writer, so zerolog's writer combinators apply directly — e.g. zerolog.MultiLevelWriter to tee destinations, or a lumberjack.Logger for rotated files.

Documentation

Overview

Package logx provides zerolog-based structured logging with context propagation, durable log fields, request logging middleware for echo, and GCP severity mapping

Index

Constants

View Source
const (
	// FieldRequestID is the log field for the request id
	FieldRequestID = "request_id"
	// FieldOrganizationID is the log field for the organization id
	FieldOrganizationID = "organization_id"
	// FieldSubjectID is the log field for the authenticated subject id
	FieldSubjectID = "subject_id"
	// FieldSubjectEmail is the log field for the authenticated subject email
	FieldSubjectEmail = "subject_email"
	// FieldCapabilities is the log field for the caller capability set
	FieldCapabilities = "capabilities"
	// FieldOperation is the log field for the operation being performed
	FieldOperation = "operation"
	// FieldRemoteIP is the log field for the client origin ip
	FieldRemoteIP = "remote_ip"
	// FieldUserAgent is the log field for the client user agent
	FieldUserAgent = "user_agent"
	// FieldRequestProtocol is the log field for the request protocol
	FieldRequestProtocol = "request_protocol"
	// FieldTrueClientIP is the log field for the True-Client-IP header
	FieldTrueClientIP = "true_client_ip"
	// FieldForwardedFor is the log field for the X-Forwarded-For header
	FieldForwardedFor = "x_forwarded_for"
	// FieldRealIP is the log field for the X-Real-IP header
	FieldRealIP = "x_real_ip"
)

Canonical log field names shared by the middleware, caller enrichment, and consumers so the same concept always lands under the same key

Variables

This section is empty.

Functions

func BenignError

func BenignError(err error) bool

BenignError reports whether err is an expected request-lifecycle failure rather than a server fault

func ErrorEvent

func ErrorEvent(ctx context.Context, err error) *zerolog.Event

ErrorEvent returns a context-logger event for err at error level, or warn level when the error is benign

func FromContext

func FromContext(ctx context.Context) *zerolog.Logger

FromContext returns the logger stored on the context or falls back to the global logger

func LoggingMiddleware

func LoggingMiddleware(config Config) echo.MiddlewareFunc

LoggingMiddleware is a middleware that logs requests using the provided logger

func MatchEchoLevel

func MatchEchoLevel(level log.Lvl) (zerolog.Level, log.Lvl)

MatchEchoLevel returns a zerolog level and echo level for a given echo level

func Middleware

func Middleware(config MiddlewareConfig) echo.MiddlewareFunc

Middleware returns echo v5 middleware that logs requests using the configured logger and seeds the request context with a request-scoped logger carrying durable fields

func RequestErrorLevel

func RequestErrorLevel(err error, status int) zerolog.Level

RequestErrorLevel returns the log level for a request that finished with err and HTTP status

func SeedContext

func SeedContext(ctx context.Context) context.Context

SeedContext ensures the provided context carries a logger, returning a derived context when necessary

func SlogLogger

func SlogLogger(l zerolog.Logger) *slog.Logger

SlogLogger wraps a zerolog logger in a *slog.Logger for consumers that require the standard library interface; level filtering is left to the underlying zerolog logger

func WithField

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

WithField adds a single field to both the logger and the durable field store on the context; a new LogFields map is allocated on each call so that sibling contexts do not share mutable state

func WithFields

func WithFields(ctx context.Context, fields map[string]any) context.Context

WithFields adds multiple fields to both the logger and the durable field store on the context; a new LogFields map is allocated on each call so that sibling contexts do not share mutable state

Types

type Config

type Config struct {
	// Logger is a custom instance of the logger to use
	Logger *Logger
	// Skipper defines a function to skip middleware
	Skipper middleware.Skipper
	// RequestIDHeader is the header name to use for the request ID in a log record
	RequestIDHeader string
	// RequestIDKey is the key name to use for the request ID in a log record
	RequestIDKey string
	// HandleError indicates whether to forward errors to the global error handler, so it can decide the appropriate status code
	HandleError bool
	// AttachRequestMetadata controls whether stable request metadata (client origin IP, user agent, and forwarding headers)
	// is attached to the request-scoped logger context so downstream log entries can include it
	AttachRequestMetadata bool
}

Config defines the config for the echolog middleware

type LogFields

type LogFields map[string]any

LogFields holds structured log fields that can be captured and restored

func FieldsFromContext

func FieldsFromContext(ctx context.Context) LogFields

FieldsFromContext returns the durable log fields stored on the context

type Logger

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

Logger is a wrapper around zerolog.Logger that provides an implementation of the echo.Logger interface

func (*Logger) Debug

func (l *Logger) Debug(i ...any)

Debug implements echo.Logger interface

func (*Logger) Debugf

func (l *Logger) Debugf(format string, i ...any)

Debugf implements echo.Logger interface

func (*Logger) Debugj

func (l *Logger) Debugj(j log.JSON)

Debugj implements echo.Logger interface

func (*Logger) Error

func (l *Logger) Error(err error)

Error implements echo.Logger interface

func (*Logger) Errorf

func (l *Logger) Errorf(format string, i ...any)

Errorf implements echo.Logger interface

func (*Logger) Errorj

func (l *Logger) Errorj(j log.JSON)

Errorj implements echo.Logger interface

func (*Logger) Fatal

func (l *Logger) Fatal(i ...any)

Fatal implements echo.Logger interface

func (*Logger) Fatalf

func (l *Logger) Fatalf(format string, i ...any)

Fatalf implements echo.Logger interface

func (*Logger) Fatalj

func (l *Logger) Fatalj(j log.JSON)

Fatalj implements echo.Logger interface

func (*Logger) Info

func (l *Logger) Info(i ...any)

Info implements echo.Logger interface

func (*Logger) Infof

func (l *Logger) Infof(format string, i ...any)

Infof implements echo.Logger interface

func (*Logger) Infoj

func (l *Logger) Infoj(j log.JSON)

Infoj implements echo.Logger interface

func (*Logger) Level

func (l *Logger) Level() log.Lvl

Level implements echo.Logger interface

func (*Logger) Output

func (l *Logger) Output() io.Writer

Output implements echo.Logger interface

func (*Logger) Panic

func (l *Logger) Panic(i ...any)

Panic implements echo.Logger interface

func (*Logger) Panicf

func (l *Logger) Panicf(format string, i ...any)

Panicf implements echo.Logger interface

func (*Logger) Panicj

func (l *Logger) Panicj(j log.JSON)

Panicj implements echo.Logger interface

func (*Logger) Prefix

func (l *Logger) Prefix() string

Prefix implements echo.Logger interface

func (*Logger) Print

func (l *Logger) Print(i ...any)

Print implements echo.Logger interface

func (*Logger) Printf

func (l *Logger) Printf(format string, i ...any)

Printf implements echo.Logger interface

func (*Logger) Printj

func (l *Logger) Printj(j log.JSON)

Printj implements echo.Logger interface

func (*Logger) SetHeader

func (l *Logger) SetHeader(_ string)

SetHeader implements echo.Logger interface

func (*Logger) SetLevel

func (l *Logger) SetLevel(level log.Lvl)

SetLevel implements echo.Logger interface

func (*Logger) SetOutput

func (l *Logger) SetOutput(newOut io.Writer)

SetOutput implements echo.Logger interface; zerolog preserves the logger's context, hooks, and level across the output switch

func (*Logger) SetPrefix

func (l *Logger) SetPrefix(newPrefix string)

SetPrefix implements echo.Logger interface

func (*Logger) Unwrap

func (l *Logger) Unwrap() zerolog.Logger

Unwrap returns the underlying zerolog.Logger

func (*Logger) Warn

func (l *Logger) Warn(i ...any)

Warn implements echo.Logger interface

func (*Logger) Warnf

func (l *Logger) Warnf(format string, i ...any)

Warnf implements echo.Logger interface

func (*Logger) Warnj

func (l *Logger) Warnj(j log.JSON)

Warnj implements echo.Logger interface

func (*Logger) WithContext

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

WithContext returns a new context with the provided logger

func (*Logger) Write

func (l *Logger) Write(p []byte) (n int, err error)

Write implements the io.Writer interface for Logger

type LoggerConfig

type LoggerConfig struct {
	// Level sets the zerolog level. Use zerolog.NoLevel to keep the existing global default
	Level zerolog.Level
	// Pretty toggles human-readable console output instead of structured JSON
	Pretty bool
	// Writer controls where logs are written. Defaults to os.Stdout
	Writer io.Writer
	// IncludeCaller attaches caller information to each log entry
	IncludeCaller bool
	// IncludeStack wires zerolog's ErrorStackMarshaler to pkgerrors.MarshalStack so events
	// carrying an error render its stack trace under the stack field
	IncludeStack bool
	// WithEcho instructs Configure to build an echo-compatible logger
	WithEcho bool
	// SetGlobal updates zerolog's global logger (`log.Logger`) using the configured settings
	SetGlobal bool
}

LoggerConfig controls how loggers produced by Configure behave

type LoggerSet

type LoggerSet struct {
	// Logger is the configured root zerolog logger
	Logger zerolog.Logger
	// Echo is the echo-compatible logger, populated only when LoggerConfig.WithEcho is set
	Echo *Logger
}

LoggerSet contains the loggers produced by Configure

func Configure

func Configure(cfg LoggerConfig) LoggerSet

Configure builds loggers according to the supplied configuration

type MiddlewareConfig

type MiddlewareConfig struct {
	// Logger is a custom instance of the logger to use
	Logger *Logger
	// Skipper short-circuits the middleware when it returns true
	Skipper func(c *echo.Context) bool
	// RequestIDHeader is the header name to use for the request ID in a log record
	RequestIDHeader string
	// RequestIDKey is the key name to use for the request ID in a log record
	RequestIDKey string
	// HandleError indicates whether to forward errors to the global error handler, so it can decide the appropriate status code
	HandleError bool
	// AttachRequestMetadata controls whether stable request metadata (client origin IP, user agent, and forwarding headers)
	// is attached to the request-scoped logger context so downstream log entries can include it
	AttachRequestMetadata bool
}

MiddlewareConfig configures the echo v5 request logging middleware

Directories

Path Synopsis
Package consolelog is a zerolog consolewriter output formatter that can be used generically with any zerolog instantiation so that it's not specific to a particular application
Package consolelog is a zerolog consolewriter output formatter that can be used generically with any zerolog instantiation so that it's not specific to a particular application

Jump to

Keyboard shortcuts

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