Documentation
¶
Overview ¶
Package logutil provides configuration-driven logging utilities built around Go's standard log/slog package.
It wraps slog with a composable configuration model:
- Config and Option build loggers from strongly typed settings.
- Config.SlogLogger, Config.SlogHandler, and Config.SlogDefaultLogger construct handler/logger instances.
- ParseLevel/ParseFormat and ValidLevel/ValidFormat convert and validate runtime configuration values.
- NewSlogHookHandler allows interception of log messages via HookFunc.
- NewSlogWriter and NewLogFromSlog bridge standard log.Logger output into slog.
Output can be JSON, text console, or discard mode. The severity model extends the syslog-style levels (emergency through debug) with a trace level.
Notes ¶
The handlers built by Config.SlogHandler and NewSlogTraceIDHandler write a record through the standard library's JSON or text handler, but they filter it first (see the sanitizing handler documented on NewSlogTraceIDHandler), so the output deliberately differs from a bare slog handler's in three ways. Two are repairs of shapes the standard library encodes incorrectly:
- A group whose members all render nothing is dropped. slog rolls the buffer back past such a group without closing it, so the next attribute is written with no separator: invalid JSON, or, in text format, a field silently renamed with the dead group's prefix.
- A time.Time whose year falls outside [0,9999] is rewritten as an RFC 3339 string. slog's JSON encoder writes an "!ERROR:" string for it and then writes the value as well, putting two JSON strings under one key.
The third keeps the two backends of this module interchangeable, since logsrv encodes through zerolog:
- A nil-pointer error writes no field, where slog renders it as the string "<nil>". A group left empty by one is dropped with it, and a typed nil logged under the trace ID key no longer suppresses the injected trace ID, which would otherwise correlate the record by the string "<nil>". A nil error of any other kind (a nil slice, map, func or channel, the shape of aggregate errors such as validator.ValidationErrors) still renders as its message.
A group that resolves to zero members is likewise dropped rather than written as a bare "{}".
Index ¶
- Constants
- func LevelName(l LogLevel) string
- func NewLogFromSlog(logger *slog.Logger) *log.Logger
- func NewSlogTraceIDHandler(h slog.Handler, f TraceIDFunc) slog.Handler
- func ValidFormat(f LogFormat) bool
- func ValidLevel(l LogLevel) bool
- type Attr
- type Config
- type HookFunc
- type LogFormat
- type LogLevel
- type Option
- func WithCommonAttr(a ...Attr) Option
- func WithFormat(f LogFormat) Option
- func WithFormatStr(f string) Option
- func WithHookFn(f HookFunc) Option
- func WithLevel(l LogLevel) Option
- func WithLevelStr(l string) Option
- func WithOutWriter(w io.Writer) Option
- func WithSource(enabled bool) Option
- func WithTraceIDFn(f TraceIDFunc) Option
- type SlogHookHandler
- type SlogWriter
- type TraceIDFunc
Constants ¶
const TraceIDKey = "trace_id"
TraceIDKey is the record attribute key used to carry the trace ID. It is the single source of truth for the field name across nurago's logging packages.
Variables ¶
This section is empty.
Functions ¶
func LevelName ¶
LevelName returns the string name of the specified log level (e.g., "error", "debug").
slog levels are arbitrary integers, so a level that is not one of the named severities has no name here. It falls back to slog's own banded form ("INFO+1", "WARN-2"), never to a bare number: a bare number would collide with the numeric syslog vocabulary that ParseLevel accepts, where "1" means alert, so a record one notch above info would be read back as a near-fatal severity.
func NewLogFromSlog ¶
NewLogFromSlog constructs a standard log.Logger that routes writes to an slog.Logger.
func NewSlogTraceIDHandler ¶
func NewSlogTraceIDHandler(h slog.Handler, f TraceIDFunc) slog.Handler
NewSlogTraceIDHandler wraps h so each record gains a trace ID attribute resolved, per record, from f. A nil f returns h unchanged (no trace ID attribute is added), mirroring how a nil TraceIDFunc is treated elsewhere in the package. Resolving the trace ID per record (rather than once at construction) lets a dynamic TraceIDFunc reflect the current request/context on every line. The trace ID is emitted at the root of the record even when the logger is derived with WithGroup.
A nil h falls back to the handler of the current slog.Default, captured now, so the returned handler never panics on first use.
The result is wrapped in the sanitizing handler (see slogSanitizeHandler), so records and derived attributes are stripped of the groups that render nothing before the trace ID is injected: that injected attribute is exactly the one the standard library's elided-group separator bug would leave without its comma. Attributes already applied to h before it is wrapped do not pass through the filter, and are not covered.
Neither is a slog.HandlerOptions.ReplaceAttr callback installed on h: it runs below this handler and can empty a group the filter has already passed by deleting its members (returning the zero slog.Attr), which re-creates the separator bug and can strip the trace ID. Use Config.SlogHandler, whose only callback never deletes, or supply a handler whose ReplaceAttr does not delete attributes.
And the filter repairs a time.Time that slog's JSON encoder cannot write (a year outside [0,9999], which it renders as an "!ERROR:" string followed by the value, making the line invalid) only where it is an *attribute*. A record's own timestamp is not an attribute and never reaches the filter; it is repaired by the ReplaceAttr callback Config.SlogHandler installs (see replaceLevelName), which this constructor cannot install on a handler it merely wraps. A hand-built record carrying such a timestamp (slog.Logger always stamps time.Now(), so only a middleware, tee or replay handler can produce one) therefore still yields an invalid line here. Use Config.SlogHandler, or give h a ReplaceAttr that rewrites slog.TimeKey.
A caller-supplied root-level trace ID wins over the injected one (see Handle). Attributes already applied to h before it is wrapped are invisible to that check too: apply them through the returned handler's WithAttrs, or use Config.SlogHandler, which accounts for Config.CommonAttr.
func ValidFormat ¶
ValidFormat reports whether the given log format is recognized.
func ValidLevel ¶
ValidLevel reports whether the given log level is recognized.
Types ¶
type Config ¶
type Config struct {
Out io.Writer
Format LogFormat
Level LogLevel
CommonAttr []Attr
HookFn HookFunc
TraceIDFn TraceIDFunc
Source bool
}
Config holds common logger parameters.
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns a pre-initialized Config with stderr output, JSON format, info level, and empty trace ID.
func (*Config) OutWriter ¶ added in v1.149.0
OutWriter returns the effective output destination: Config.Out, or os.Stderr when Out is unusable: nil, or a typed nil (a nil *os.File, say, held in a non-nil io.Writer interface), which Out being an exported field allows even though WithOutWriter rejects it. Both backends resolve the destination through it, so a hand-built Config never yields a handler that panics on the first write.
func (*Config) SlogDefaultLogger ¶
SlogDefaultLogger constructs a slog.Logger from Config settings and installs it as the process default.
As a side effect of slog.SetDefault, this also redirects the standard library log package's default output through the returned logger's handler. Use SlogLogger to obtain a logger without mutating that global state.
func (*Config) SlogHandler ¶
SlogHandler constructs a slog.Handler from Config settings with optional hook interception.
func (*Config) SlogLogger ¶
SlogLogger constructs a slog.Logger from Config settings (format, level, common attributes, hooks).
type HookFunc ¶
HookFunc is used to intercept the log message before passing it to the underlying handler.
type LogFormat ¶
type LogFormat int8
LogFormat selects how log records are encoded for output.
func ParseFormat ¶
ParseFormat converts a string ("json", "console", "none"/"discard"/"noop") to a log format. For unrecognized input it returns FormatJSON together with an error, so a caller that ignores the error degrades to visible JSON logs rather than silently discarding output (which returning FormatNone would cause).
type LogLevel ¶
LogLevel is an alias for slog.Level to represent extended log severity levels.
const ( LevelEmergency LogLevel = 64 // (+) 0 - Emergency - System is unusable. LevelAlert LogLevel = 32 // (+) 1 - Alert - Immediate action required. LevelCritical LogLevel = 16 // (+) 2 - Critical - Critical conditions. LevelError LogLevel = 8 // (=) 3 - Error - Error conditions. LevelWarning LogLevel = 4 // (=) 4 - Warning - Warning conditions. LevelNotice LogLevel = 2 // (+) 5 - Notice - Normal but noteworthy events. LevelInfo LogLevel = 0 // (=) 6 - Informational - General informational messages. LevelDebug LogLevel = -4 // (=) 7 - Debug - Detailed debugging information. LevelTrace LogLevel = -8 // (+) Additional Trace level when supported. )
Extended slog levels.
func ParseLevel ¶
ParseLevel converts syslog-style level strings ("0"-"7", syslog names, or "trace") to log levels. For unrecognized input it returns LevelInfo together with an error, so a caller that ignores the error degrades to a safe, non-verbose level rather than to debug output.
type Option ¶
Option configures a Config instance.
func WithCommonAttr ¶
WithCommonAttr sets the attributes attached to every log record, replacing any previously configured common attributes.
func WithFormat ¶
WithFormat overrides the log output format (JSON, console, or discard).
func WithFormatStr ¶
WithFormatStr overrides the log format using a string ("json", "console", "none").
func WithHookFn ¶
WithHookFn adds a callback that intercepts each log record before the underlying handler processes it.
func WithLevelStr ¶
WithLevelStr overrides the log level using a string (e.g., "error", "debug", "trace").
func WithOutWriter ¶
WithOutWriter overrides the output destination for log messages. A nil writer is rejected so misconfiguration fails at construction instead of panicking on the first log write. A typed nil (a nil *os.File, say, held in a non-nil io.Writer interface) is rejected too: it panics just the same.
func WithSource ¶
WithSource enables or disables source location (file:line) annotation on each record. It is disabled by default to avoid the per-record runtime.CallersFrames cost.
func WithTraceIDFn ¶
func WithTraceIDFn(f TraceIDFunc) Option
WithTraceIDFn adds a callback that dynamically retrieves the trace ID for each record.
type SlogHookHandler ¶
SlogHookHandler is a slog.Handler that wraps another handler to add custom logic.
func NewSlogHookHandler ¶
func NewSlogHookHandler(h slog.Handler, f HookFunc) *SlogHookHandler
NewSlogHookHandler wraps an slog.Handler with a hook function invoked for each log record. A nil h falls back to the handler of the current slog.Default, captured now, so the returned handler never panics on first use. A nil f is tolerated too (see Handle).
func (SlogHookHandler) Handle ¶
Handle intercepts the log record, invokes the hook (when set), then passes the record to the underlying handler. A nil hook is tolerated so a handler built with NewSlogHookHandler(h, nil) does not panic.
func (SlogHookHandler) WithAttrs ¶
func (h SlogHookHandler) WithAttrs(attrs []slog.Attr) slog.Handler
WithAttrs returns a new SlogHookHandler whose underlying handler carries the given attributes, preserving the hook function so it keeps firing for derived loggers. Per the slog.Handler contract, an empty attribute list returns the receiver unchanged.
func (SlogHookHandler) WithGroup ¶
func (h SlogHookHandler) WithGroup(name string) slog.Handler
WithGroup returns a new SlogHookHandler whose underlying handler opens the given group, preserving the hook function so it keeps firing for derived loggers. Per the slog.Handler contract, an empty group name returns the receiver unchanged.
type SlogWriter ¶
type SlogWriter struct {
// Logger is the destination. A nil Logger writes to slog.Default, resolved per write, so the
// zero value works and a logger installed later is picked up.
Logger *slog.Logger
// Level is the severity used for every bridged line. The zero value is
// LevelInfo; construct with NewSlogWriter for the Error default.
Level LogLevel
}
SlogWriter is a custom io.Writer that writes to slog.Logger at a configurable level.
The zero value is usable: it writes to slog.Default at LevelInfo.
func NewSlogWriter ¶
func NewSlogWriter(logger *slog.Logger) *SlogWriter
NewSlogWriter constructs an io.Writer that routes writes to an slog.Logger at error level.
func NewSlogWriterLevel ¶
func NewSlogWriterLevel(logger *slog.Logger, level LogLevel) *SlogWriter
NewSlogWriterLevel constructs an io.Writer that routes writes to an slog.Logger at the given level. This is the level-aware counterpart to NewSlogWriter: bridged standard log.Logger output is not necessarily error-severity, so callers can pick the level that matches the source. A nil logger falls back to slog.Default so writes never panic.
type TraceIDFunc ¶
type TraceIDFunc func() string
TraceIDFunc is the type of function used to retrieve a Trace ID.