logger

package
v0.66.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package logger provides logging functionality with zerolog adapter

Package logger provides filtering capabilities for sensitive data in log output.

Package logger defines the logging interface used throughout the application. It provides a contract for structured logging implementations.

Index

Constants

View Source
const (
	LevelTrace = "trace"
	LevelDebug = "debug"
	LevelInfo  = "info"
	LevelWarn  = "warn"
	LevelError = "error"
	LevelFatal = "fatal"
	LevelPanic = "panic"
)

Log level string constants matching zerolog level names. Exported so other packages (server, app, config) can reuse the canonical level identifiers without redefining them.

View Source
const (
	FieldCorrelationID = "correlation_id"
	FieldTraceID       = "trace_id"
	FieldSpanID        = "span_id"
)

Identity field keys. Two identifier spaces share every log line and hold different values by design:

  • FieldCorrelationID is the framework's own cross-service id: the inbound X-Request-ID when one arrived, else the trace id derived from a traceparent, else a UUID minted at the boundary. It is always present, travels on both messaging lanes' outcome lines, and survives with tracing switched off.
  • FieldTraceID and FieldSpanID are the OpenTelemetry span identifiers of the span the line was written under. They appear only while a tracer provider is registered and never equal the correlation id.

Every stamping site uses these names so the key has one definition; the long-form rationale lives in wiki/observability.md ("Correlation Fields and Exemplars"). observability/dual_processor.go reads the OTel two as OTLP record attributes by literal to stay free of a logger import.

View Source
const (
	// DefaultMaxDepth is the default maximum recursion depth for filtering
	DefaultMaxDepth = 8

	// DefaultMaxPayloadBytes is the default ceiling on an opaque payload the
	// filter will parse. 64 KiB is comfortably above the request and response
	// bodies services log in practice and well below the size at which decoding
	// on the logging path becomes the expensive part of handling a request.
	// A payload above it is masked whole rather than shipped unread.
	DefaultMaxPayloadBytes = 64 * 1024
)
View Source
const (
	FormatAuto    = "auto"
	FormatConsole = "console"
	FormatJSON    = "json"
)

Log format constants used by config.OutputConfig.Format. "pretty" and "structured" are accepted aliases for "console" and "json".

View Source
const DefaultMaskValue = "***"

DefaultMaskValue is the value used to mask sensitive data.

Variables

This section is empty.

Functions

func AddAMQPElapsed

func AddAMQPElapsed(ctx context.Context, nanos int64)

AddAMQPElapsed adds elapsed nanoseconds to the AMQP elapsed time in the context

func AddDBElapsed

func AddDBElapsed(ctx context.Context, nanos int64)

AddDBElapsed adds elapsed nanoseconds to the database elapsed time in the context

func GetAMQPCounter

func GetAMQPCounter(ctx context.Context) int64

GetAMQPCounter returns the current AMQP message count from the context

func GetAMQPElapsed

func GetAMQPElapsed(ctx context.Context) int64

GetAMQPElapsed returns the current AMQP elapsed time in nanoseconds from the context

func GetDBCounter

func GetDBCounter(ctx context.Context) int64

GetDBCounter returns the current database operation count from the context

func GetDBElapsed

func GetDBElapsed(ctx context.Context) int64

GetDBElapsed returns the current database elapsed time in nanoseconds from the context

func IncrementAMQPCounter

func IncrementAMQPCounter(ctx context.Context)

IncrementAMQPCounter increments the AMQP message counter in the context

func IncrementDBCounter

func IncrementDBCounter(ctx context.Context)

IncrementDBCounter increments the database operation counter in the context

func ResolvePretty added in v0.32.0

func ResolvePretty(format string, legacyPretty, otlpLogsActive, isTerminal bool) bool

ResolvePretty decides whether the logger should run in pretty (console) mode.

legacyPretty=true wins over format because users opting in via the older log.pretty boolean expect their preference to stick. Combined with otlpLogsActive=true it will panic at startup via WithOTelProvider — that fail-fast is intentional: silently muting the log shipping pipeline would be a worse outcome than a clear startup error.

func StdoutIsTerminal added in v0.32.0

func StdoutIsTerminal() bool

StdoutIsTerminal reports whether os.Stdout is attached to a terminal.

func WithAMQPCounter

func WithAMQPCounter(ctx context.Context) context.Context

WithAMQPCounter seeds the shared per-request counters. Retained for backward compatibility; prefer WithRequestCounters (both seed the same struct).

func WithDBCounter

func WithDBCounter(ctx context.Context) context.Context

WithDBCounter seeds the shared per-request counters. Retained for backward compatibility; prefer WithRequestCounters (both seed the same struct).

func WithRequestCounters added in v0.41.0

func WithRequestCounters(ctx context.Context) context.Context

WithRequestCounters attaches the shared per-request counters struct (AMQP and DB operation counts and elapsed times) exactly once. It is idempotent — a second call returns the context unchanged, never resetting recorded values — and nil-safe (a nil context is returned as-is). This is the single seeder; a context seeded once exposes all four counters regardless of which name was used.

func WithSeverityHook added in v0.13.0

func WithSeverityHook(ctx context.Context, hook func(zerolog.Level)) context.Context

WithSeverityHook attaches a severity hook to the context. The hook is used by the logging adapter to propagate WARN/ERROR logs back to request middleware for routing.

Types

type FilterConfig

type FilterConfig struct {
	// SensitiveFields contains field names that should be masked in logs
	SensitiveFields []string
	// MaskValue is the value used to replace sensitive data (default: "***")
	MaskValue string
	// ErrorRedactor, when non-nil, replaces the message written by
	// LogEvent.Err(err) with its return value. Field-name masking cannot see
	// inside an error message, and the framework calls Err with
	// consumer-authored errors at dozens of sites, so a consumer-side scrub
	// helper never reaches those lines without this seam. Nil (the default,
	// and what DefaultFilterConfig returns) keeps Err byte-identical to
	// zerolog's own rendering. Code door only: app.Options.LoggerFilterConfig,
	// which replaces the whole config — start from DefaultFilterConfig() and
	// set this field. The YAML log.sensitivefields merge path leaves it nil,
	// the value being a function.
	ErrorRedactor func(error) string
	// MaxPayloadBytes caps how large an opaque payload may be before the filter
	// stops trying to read it. A JSON-LOOKING payload past the cap is masked
	// whole — it is opaque and unread, which is the case the mask exists for.
	// A payload that is not JSON-shaped passes through UNCHANGED whatever its
	// size, since it was never going to be parsed; the cap still bounds the PEM
	// header scan over it, so an oversized blob is not scanned end to end on
	// the logging path. Parsing is the only way to
	// see inside bytes, and parsing is linear in their size, so an unbounded
	// cap would let one oversized payload — a bulk export, a base64 blob that
	// happens to open with a brace — do arbitrary decode work on the logging
	// path. Zero (the default, and what DefaultFilterConfig returns) means
	// DefaultMaxPayloadBytes; a negative value disables the payload door
	// entirely, leaving bytes and strings judged by NAME alone as they were
	// before ADR-086.
	MaxPayloadBytes int
}

FilterConfig defines the configuration for sensitive data filtering

func DefaultFilterConfig

func DefaultFilterConfig() *FilterConfig

DefaultFilterConfig returns a default configuration with common sensitive field names

type LogEvent

type LogEvent interface {
	Msg(msg string)
	Msgf(format string, args ...any)
	Err(err error) LogEvent
	Str(key, value string) LogEvent
	Int(key string, value int) LogEvent
	Int64(key string, value int64) LogEvent
	Uint64(key string, value uint64) LogEvent
	Dur(key string, d time.Duration) LogEvent
	// Interface adds an arbitrary value. Rendering it can panic — the filter walks
	// it by reflection and the encoder marshals it — so a call inside a defer that
	// has already spent its recover() must wrap this in a nested guard, or the
	// panic escapes that defer and skips whatever the handler had left to do.
	Interface(key string, i any) LogEvent
	Bytes(key string, val []byte) LogEvent
	Bool(key string, value bool) LogEvent
	// Enabled reports whether the event will actually be emitted at its level
	// (i.e. not dropped by the logger's level or sampling). Callers can use it to
	// skip building expensive fields when the event would be discarded.
	Enabled() bool
}

LogEvent represents a structured log event that can be built with fields and sent. It provides methods for adding various field types and sending the final log message.

type LogEventAdapter

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

LogEventAdapter adapts zerolog events to our logger interface

func (*LogEventAdapter) Bool added in v0.26.0

func (lea *LogEventAdapter) Bool(key string, value bool) LogEvent

Bool adds a boolean field to the log event

func (*LogEventAdapter) Bytes

func (lea *LogEventAdapter) Bytes(key string, val []byte) LogEvent

Bytes adds a byte slice field to the log event.

A byte slice is an OPAQUE payload: the name filter sees one leaf called by this key, however many named fields the bytes carry of their own. So the payload goes through the filter rather than straight to the encoder, and what comes back decides the door — an untouched payload keeps zerolog's own Bytes rendering, while a masked one is written as the value the filter chose (#1133).

It calls the payload door DIRECTLY rather than through FilterValue, whose parameter is `any`: a slice header does not fit in an interface word, so boxing one costs an allocation on every byte-slice field logged — paid before the cheap not-JSON rejection inside the door has even run. The door answers without boxing on the untouched path, which is the common one.

func (*LogEventAdapter) Dur

func (lea *LogEventAdapter) Dur(key string, d time.Duration) LogEvent

Dur adds a duration field to the log event

func (*LogEventAdapter) Enabled added in v0.41.0

func (lea *LogEventAdapter) Enabled() bool

Enabled reports whether the underlying zerolog event will be emitted. It is nil-safe: zerolog returns a nil *Event for disabled levels, and *Event.Enabled returns false on a nil receiver.

func (*LogEventAdapter) Err

func (lea *LogEventAdapter) Err(err error) LogEvent

Err adds an error to the log event

func (*LogEventAdapter) Int

func (lea *LogEventAdapter) Int(key string, value int) LogEvent

Int adds an integer field to the log event

func (*LogEventAdapter) Int64

func (lea *LogEventAdapter) Int64(key string, value int64) LogEvent

Int64 adds an int64 field to the log event

func (*LogEventAdapter) Interface

func (lea *LogEventAdapter) Interface(key string, i any) LogEvent

Interface adds an any field to the log event

func (*LogEventAdapter) Msg

func (lea *LogEventAdapter) Msg(msg string)

Msg logs the message

func (*LogEventAdapter) Msgf

func (lea *LogEventAdapter) Msgf(format string, args ...any)

Msgf logs a formatted message

func (*LogEventAdapter) Str

func (lea *LogEventAdapter) Str(key, value string) LogEvent

Str adds a string field to the log event

func (*LogEventAdapter) Uint64

func (lea *LogEventAdapter) Uint64(key string, value uint64) LogEvent

Uint64 adds a uint64 field to the log event

type Logger

type Logger interface {
	Info() LogEvent
	Error() LogEvent
	Debug() LogEvent
	Warn() LogEvent
	Fatal() LogEvent
	WithContext(ctx any) Logger
	WithFields(fields map[string]any) Logger
}

Logger defines the contract for structured logging throughout the application. It provides methods for creating log events at different severity levels and for contextual logging.

type OTelBridge added in v0.13.0

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

OTelBridge converts zerolog JSON output to OpenTelemetry log records. It implements io.Writer to intercept zerolog's output stream.

func NewOTelBridge added in v0.13.0

func NewOTelBridge(provider *sdklog.LoggerProvider) *OTelBridge

NewOTelBridge creates a new bridge that converts zerolog logs to OTel log records.

func (*OTelBridge) Write added in v0.13.0

func (b *OTelBridge) Write(p []byte) (n int, err error)

Write implements io.Writer by parsing zerolog JSON and emitting OTel log records.

type OTelProvider added in v0.13.0

type OTelProvider interface {
	// LoggerProvider returns the configured logger provider.
	// Returns nil if logging is disabled.
	LoggerProvider() *sdklog.LoggerProvider

	// ShouldDisableStdout returns true if stdout should be disabled when OTLP is enabled.
	// This method is implemented via type assertion to avoid exposing internal config.
	ShouldDisableStdout() bool
}

OTelProvider is a minimal interface for accessing OpenTelemetry logger provider and configuration. This interface allows the logger package to integrate with observability without creating circular dependencies.

type Redactor added in v0.65.0

type Redactor interface {
	RedactedForLog() any
}

Redactor is implemented by a value that knows its own log-safe shape. The filter logs RedactedForLog's result in place of the value, at every depth and through both Interface and WithFields, and still applies the needle list to that result. A value under a sensitive key is masked whole without calling it.

Implement it with a VALUE receiver: a pointer-receiver method leaves a bare value of the type unrecognized, so it would be walked field by field instead.

type SensitiveDataFilter

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

SensitiveDataFilter filters sensitive data from logs. Filtering is enforced by the adapter layer (LogEventAdapter) — never add a Zerolog() accessor to ZeroLogger as that would create a bypass path around this filter boundary.

func NewSensitiveDataFilter

func NewSensitiveDataFilter(config *FilterConfig) *SensitiveDataFilter

NewSensitiveDataFilter creates a new filter with the given configuration

func (*SensitiveDataFilter) FilterFields

func (f *SensitiveDataFilter) FilterFields(fields map[string]any) map[string]any

FilterFields filters a map of fields for sensitive data

func (*SensitiveDataFilter) FilterString

func (f *SensitiveDataFilter) FilterString(key, value string) string

FilterString filters sensitive data from string values

func (*SensitiveDataFilter) FilterValue

func (f *SensitiveDataFilter) FilterValue(key string, value any) any

FilterValue filters sensitive data from any values

type ZeroLogger

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

ZeroLogger wraps zerolog.Logger to implement the Logger interface. It provides structured logging functionality with configurable output formatting.

func New

func New(level string, pretty bool) *ZeroLogger

New creates a new ZeroLogger instance with the specified log level and formatting options. If pretty is true, output will be formatted for human readability.

func NewWithFilter

func NewWithFilter(level string, pretty bool, filterConfig *FilterConfig) *ZeroLogger

NewWithFilter creates a new ZeroLogger instance with custom filter configuration. This allows applications to customize which fields are considered sensitive.

func (*ZeroLogger) Debug

func (l *ZeroLogger) Debug() LogEvent

Debug creates a debug-level log event

func (*ZeroLogger) Error

func (l *ZeroLogger) Error() LogEvent

func (*ZeroLogger) Fatal

func (l *ZeroLogger) Fatal() LogEvent

Fatal creates a fatal-level log event

func (*ZeroLogger) Info

func (l *ZeroLogger) Info() LogEvent

Info creates an info-level log event

func (*ZeroLogger) Warn

func (l *ZeroLogger) Warn() LogEvent

Warn creates a warning-level log event

func (*ZeroLogger) WithContext

func (l *ZeroLogger) WithContext(ctx any) Logger

WithContext returns a logger with context information attached. It follows a two-phase approach for maximum flexibility:

Phase 1 (Explicit): If the context contains an explicit zerolog logger (set via zerolog.Ctx), that logger takes precedence. This maintains backward compatibility with existing code that uses zerolog's context pattern.

Phase 2 (Automatic): If no explicit logger is found, automatically extracts trace_id and span_id from the OpenTelemetry span context and adds them as fields. This enables automatic trace correlation without requiring explicit logger management in every handler.

This hybrid approach provides:

  • Deterministic behavior: same context always produces same logger
  • Backward compatibility: existing zerolog.Ctx usage continues to work
  • Automatic correlation: trace IDs appear in logs without boilerplate

func (*ZeroLogger) WithFields

func (l *ZeroLogger) WithFields(fields map[string]any) Logger

WithFields returns a logger with additional fields attached to all log entries.

func (*ZeroLogger) WithOTelProvider added in v0.13.0

func (l *ZeroLogger) WithOTelProvider(provider OTelProvider) *ZeroLogger

WithOTelProvider attaches an OpenTelemetry logger provider for OTLP log export. Returns the same logger if provider is nil/disabled, or creates a new logger with dual output (stdout + OTLP) or OTLP-only based on the provider's configuration.

IMPORTANT: OTLP export requires JSON mode (pretty=false). This method fails fast with a panic if pretty mode is active, ensuring configuration errors are caught during initialization rather than silently degrading observability.

Configuration conflict detection:

  • If logger is created with pretty=true AND OTLP export is enabled, panics with clear error message directing user to fix their configuration.

Output modes:

  • DisableStdout=false (default): logs go to both stdout and OTLP (useful for dev)
  • DisableStdout=true: logs only go to OTLP (production efficiency)

Jump to

Keyboard shortcuts

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