Documentation
¶
Overview ¶
Package advslog complements the standard log/slog package with the pieces needed for seamless contextual logging in services: process-wide logger initialization, storing a logger in a context.Context, zerolog-compatible level parsing with custom TRACE/FATAL/PANIC levels, and panic logging with stack traces.
The standard library deliberately leaves these features out (logger-in-context was removed from the slog design before Go 1.21; a Fatal level and level-name parsing were declined), expecting them to be provided by packages like this one.
Initialization ¶
Init sets up the process-wide default logger once: it logs to the file named by the GO_LOG environment variable (stderr by default), chooses colored human-readable output on a terminal and JSON otherwise, redirects the standard log package (inferring the level from prefixes such as "ERROR:" or "[WARN]", see DisableLevelInference), and respects the dynamic Level. InitTest does the same for tests, writing through testing.TB.Output; every test running code that logs must call it, because go test does not support log output written to stdout or stderr directly.
Contextual logging ¶
NewContext stores a logger in a context, Ctx retrieves it (falling back to slog.Default). This is the replacement for zerolog's Logger.WithContext / zerolog.Ctx idiom:
ctx = advslog.NewContext(ctx, logger.With("request_id", id))
...
advslog.Ctx(ctx).Info("request processed", "status", status)
With WithContextRouting, even code unaware of this package joins in: records logged through the default logger's *Context methods (e.g. slog.InfoContext(ctx, ...) inside an external library) are routed to the context logger as well.
Levels ¶
Level is the dynamic level shared by all handlers created by this package; change it at runtime with Level.Set (the replacement for zerolog.SetGlobalLevel). ParseLevel parses both slog and zerolog level names, including the custom LevelTrace, LevelFatal and LevelPanic. Fatal and FatalContext log at LevelFatal and exit. With WithConfig, the default and per-package levels are driven by OnlineConf and change on the fly.
Panic logging ¶
Recover is a deferred one-liner that recovers a panic and logs its typed reason with a stack trace; LogPanic, PanicAttrs and TracebackAttr are the underlying building blocks for custom recovery flows.
Subpackages ¶
The masking of sensitive data (URL parameters, JSON by XPath) lives in the logmask subpackage, and the outgoing HTTP request/response logger built on top of it lives in reqlog. They are separate packages so that their JSON processing dependencies stay out of programs that do not need them.
Index ¶
- Constants
- Variables
- func Ctx(ctx context.Context) *slog.Logger
- func DefaultLevelPrefixes() map[string]slog.Level
- func Err(err error) slog.Attr
- func Fatal(msg string, args ...any)
- func FatalContext(ctx context.Context, msg string, args ...any)
- func Init(opts ...Option) *slog.Logger
- func InitTest(tb TestingOutput, opts ...Option) *slog.Logger
- func LogPanic(ctx context.Context, msg string, reason any)
- func NewContext(ctx context.Context, l *slog.Logger) context.Context
- func PanicAttrs(reason any) []slog.Attr
- func ParseLevel(s string) (slog.Level, error)
- func Recover(ctx context.Context, msg string)
- func TracebackAttr() slog.Attr
- type Config
- type Option
- type TestingOutput
Examples ¶
Constants ¶
const ( LevelTrace = slog.Level(-8) LevelFatal = slog.Level(12) LevelPanic = slog.Level(16) )
Additional levels complementing the standard slog.Level values, matching the zerolog level set. Handlers created by Init and InitTest render them by name ("TRACE", "FATAL", "PANIC") instead of slog's default offset spelling ("DEBUG-4", "ERROR+4", "ERROR+8").
LevelPanic exists for level parsing and rendering only: unlike zerolog, there is no helper that logs and panics.
const EnvGoLog = "GO_LOG"
EnvGoLog names the environment variable specifying the log destination file for Init. Empty or unset means stderr.
Variables ¶
var Level = defaultLevel()
Level is the dynamic logging level used by all handlers created by Init and InitTest. Adjust it at runtime with Level.Set — the replacement for zerolog.SetGlobalLevel.
It defaults to LevelTrace: like zerolog, everything is logged until a level is set explicitly.
var TestTimeFormat = "15:04:05"
TestTimeFormat is the time format for terminal output (Init on a TTY and InitTest). It may be changed before the Init* call.
JSON output always uses RFC 3339 with nanoseconds, the slog.JSONHandler default.
Functions ¶
func Ctx ¶
Ctx returns the logger stored in ctx by NewContext, or slog.Default if there is none. It replaces zerolog.Ctx with zerolog.DefaultContextLogger set.
A nil logger stored in the context is treated as absent. ctx may be nil: functions accepting a context exclusively for logging should document that callers may pass nil.
Example ¶
The logger enriched with request-scoped fields travels in the context; every layer below logs through it without passing the logger explicitly.
package main
import (
"context"
"log/slog"
"os"
advslog "github.com/adventures-team/go-adv-slog"
)
// stdoutLogger returns a JSON logger with deterministic output (no time) for
// the examples.
func stdoutLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if len(groups) == 0 && a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
}
func main() {
logger := stdoutLogger().With("request_id", "req-42")
ctx := advslog.NewContext(context.Background(), logger)
processRequest(ctx)
}
func processRequest(ctx context.Context) {
advslog.Ctx(ctx).Info("request processed", "status", 200)
}
Output: {"level":"INFO","msg":"request processed","request_id":"req-42","status":200}
func DefaultLevelPrefixes ¶ added in v0.0.6
DefaultLevelPrefixes returns the level prefixes Init and InitTest infer standard-log levels from unless WithLevelPrefixes says otherwise:
[TRACE] TRACE: LevelTrace [DEBUG] DEBUG: slog.LevelDebug [INFO] INFO: slog.LevelInfo [WARN] WARN: [WARNING] WARNING: slog.LevelWarn [ERROR] ERROR: [ERR] ERR: slog.LevelError [FATAL] FATAL: LevelFatal [PANIC] PANIC: LevelPanic
Matching is ASCII case-insensitive, so "Error:" and "error:" are covered as well, and the delimiter is part of the prefix: "ERROR:" does not match "ERRORS: 3".
Every call returns a fresh copy: programs may modify it freely, typically to add prefixes of their own before passing it to WithLevelPrefixes. The defaults themselves cannot be changed.
func Err ¶
Err returns an attr holding err under the "error" key, following the zerolog convention. A nil err yields an empty Attr, which the built-in handlers omit.
Example ¶
package main
import (
"context"
"log/slog"
"os"
advslog "github.com/adventures-team/go-adv-slog"
)
// stdoutLogger returns a JSON logger with deterministic output (no time) for
// the examples.
func stdoutLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if len(groups) == 0 && a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
}
func main() {
logger := stdoutLogger()
_, err := os.Open("/nonexistent")
logger.LogAttrs(context.Background(), slog.LevelError, "open failed", advslog.Err(err))
}
Output: {"level":"ERROR","msg":"open failed","error":"open /nonexistent: no such file or directory"}
func Fatal ¶
Fatal logs msg at LevelFatal to the default logger and terminates the process with exit status 1. It replaces zerolog's log.Fatal().
func FatalContext ¶
FatalContext is Fatal logging through the logger from ctx (see Ctx).
func Init ¶
Init sets up the process-wide default logger and returns it.
Logs go to the file named by the GO_LOG environment variable (EnvGoLog), stderr by default. If the destination is a terminal, output is colored and human-readable (honoring NO_COLOR, see https://no-color.org); otherwise it is JSON in the native log/slog format. Either way the level is controlled by the dynamic Level and the custom levels are rendered by name.
The logger is installed with slog.SetDefault and it is what Ctx falls back to for contexts without a logger. The standard log package is routed into it as well: every log.Print* message becomes a record attributed to its caller, at the level its prefix says ("ERROR: ...", "[WARN] ...", see DisableLevelInference and DefaultLevelPrefixes) or at the level of WithStdLogLevel otherwise.
Init runs once: subsequent calls return the logger created by the first one.
Example ¶
Init is called once at program startup; everything else uses the standard slog API or the context helpers.
package main
import (
"log/slog"
advslog "github.com/adventures-team/go-adv-slog"
)
func main() {
logger := advslog.Init()
logger.Info("service started", "version", "1.2.3")
slog.Info("the global slog logger works too")
}
Output:
func InitTest ¶
func InitTest(tb TestingOutput, opts ...Option) *slog.Logger
InitTest sets up the default slog logger for use from tests and returns it: colored human-readable output (honoring NO_COLOR) with second-precision time (TestTimeFormat), written through tb.Output so that it interleaves correctly with the test output and is shown only for failing tests (unless -v is given).
Every test running code that logs must call InitTest first, whatever logging library that code uses, so that no log line reaches stdout or stderr directly. This is the position of the Go maintainers, not a matter of preference: test output belongs in testing.TB.Log or testing.TB.Output. Output written past them is not attributed to its test, is printed for passing tests too, and since Go 1.27 is mangled by go test -json, which drops or misreads the control characters of its own framing in it (ESC among them, so any colored output). A report of that was closed as working as intended with exactly this advice: https://go.dev/issue/81592. Code that logs through a logger of its own rather than the default one must be given the returned logger, e.g. through NewContext.
Unlike Init it is not guarded by a once: call it from any test, even when some library has already called Init. It accepts the same options as Init and routes the standard log package the same way.
For example:
func TestHandle(t *testing.T) {
logger := advslog.InitTest(t) // colored output through t.Output, shown for failures and with -v
logger.Info("fixture ready", "user", "alice")
// the code under test logs through the context it is given
handle(advslog.NewContext(t.Context(), logger.With("request_id", "test-1")))
}
func LogPanic ¶
LogPanic logs a recovered panic value with its stack trace at slog.LevelError through the logger from ctx (see Ctx).
func NewContext ¶
NewContext returns a copy of ctx with l stored in it, to be retrieved with Ctx. It replaces zerolog's Logger.WithContext.
func PanicAttrs ¶
PanicAttrs converts a recovered panic value into attrs: a typed "panic_reason" and the stack trace from TracebackAttr.
A reason implementing fmt.Stringer, or being a string or an error, is logged as a string or an error respectively (some built-in panics arrive as errors); anything else is logged as-is, marshaled through reflection.
func ParseLevel ¶
ParseLevel converts a level name to a slog.Level. On top of the names and offsets understood by slog.Level.UnmarshalText (case-insensitive "debug", "INFO", "ERROR+4", ...), it accepts the zerolog names: "trace", "warning", "fatal" and "panic".
Example ¶
package main
import (
"fmt"
advslog "github.com/adventures-team/go-adv-slog"
)
func main() {
for _, name := range []string{"warn", "trace", "fatal"} {
level, _ := advslog.ParseLevel(name)
fmt.Println(level)
}
}
Output: WARN DEBUG-4 ERROR+4
func Recover ¶
Recover recovers a panic in flight, logs it like LogPanic and suppresses its propagation. It must be used directly in a defer statement:
defer advslog.Recover(ctx, "panic in the config watcher")
When there is no panic, Recover does nothing.
Because the panic is swallowed, Recover fits only the simple handle-log-forget scenarios: a background worker iteration, a watcher callback — places where execution just moves on. When the handling is anything more than logging (re-panicking, converting the panic into a returned error, failing the affected request, updating a metric), recover the panic manually in your own deferred function and report it with LogPanic:
defer func() {
if reason := recover(); reason != nil {
advslog.LogPanic(ctx, "panic in the handler", reason)
err = fmt.Errorf("handler panicked: %v", reason)
}
}()
Example ¶
Recover logs a panic with its typed reason and stack trace through the context logger. Use it in goroutines and worker loops.
package main
import (
"context"
"log/slog"
"os"
advslog "github.com/adventures-team/go-adv-slog"
)
// stdoutLogger returns a JSON logger with deterministic output (no time) for
// the examples.
func stdoutLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if len(groups) == 0 && a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
}
func main() {
ctx := advslog.NewContext(context.Background(), stdoutLogger())
go func() {
defer advslog.Recover(ctx, "panic in the worker")
panic("unexpected state")
}()
}
Output:
func TracebackAttr ¶
TracebackAttr returns the current goroutine's stack trace as a "traceback" attr.
If stderr is a terminal, the stack is printed there instead and an empty Attr (omitted by handlers) is returned: during local debugging the logging context is not needed — there is a single user and the failing request is known — and a raw stack is easier to read.
Types ¶
type Config ¶ added in v0.0.6
type Config interface {
GetStringIfExists(path string) (string, bool)
GetStrings(path string, dfl []string) []string
GetStruct(path string, valuePtr any) (bool, error)
SubscribeChanSubtree(path string, ch chan<- struct{}) error
}
Config is the configuration source read by WithConfig: the subset of onlineconf-go/v2 shared by *onlineconf.Module and *onlineconf.Subtree. Both satisfy it as-is.
type Option ¶
type Option func(*options)
Option configures Init and InitTest.
func DisableLevelInference ¶ added in v0.0.6
func DisableLevelInference() Option
DisableLevelInference turns the level inference of standard-log messages off: every message logged through the standard log package is recorded as is, at the level given by WithStdLogLevel.
With the inference on — the default — a message starting with one of the level prefixes (DefaultLevelPrefixes unless WithLevelPrefixes says otherwise), e.g. "ERROR: cannot open /proc/self/io" or "[WARN] retrying", is recorded at that level with the prefix and the spaces after it removed. Libraries that log through the standard log package have no other way to say how serious a line is; without the inference, all of them are INFO.
func WithAddSource ¶
func WithAddSource() Option
WithAddSource makes the handler record the source position of the log call (see slog.HandlerOptions).
func WithConfig ¶ added in v0.0.6
WithConfig enables per-package log levels and the standard-log settings driven by OnlineConf through cfg — a *onlineconf.Module or, recommended, a *onlineconf.Subtree scoped to the service's logging section (a Module subscribes at the root, which OnlineConf notifies on every database update).
Configuration layout, relative to cfg:
/level default level (drives [Level]; absent = keep current)
/<import-path>/level override for one package, e.g.
/github.com/adventures-team/go-adv-slog/reqlog/level
/stdlog/disable_level_inference OnlineConf bool (empty or "0" is false): turns the
level inference off, like [DisableLevelInference]
/stdlog/level_prefixes JSON object {"ERROR:": "error", "[WARN]": "warn", ...}
replacing the level prefixes in force
/stdlog/default_level level of standard-log messages without a prefix,
like [WithStdLogLevel]
Level values are names accepted by ParseLevel. Changes apply on the fly via onlineconf-go subscriptions; the OnlineConf child_lists feature must be enabled for the module (subscriptions themselves already require it). An absent /stdlog key leaves the corresponding option in force; an invalid value is reported and keeps the previous one.
A record belongs to the package whose code called the slog API, resolved from the record's PC — helpers logging on a caller's behalf own their records unless they stamp the caller's PC (as advslog's Fatal, LogPanic and Recover do). Handler Enabled reports at floor granularity — min(default, lowest override) — because it receives no caller information; the per-package decision happens when the record is handled. For expensive log arguments prefer slog.LogValuer values: records dropped by the per-package gate never resolve them.
func WithContextRouting ¶
func WithContextRouting() Option
WithContextRouting makes Init (and InitTest) wrap the handler so that records logged through the default logger's context-aware methods (slog.InfoContext, slog.Logger.Log, ...) are routed to the logger stored in the context by NewContext, when there is one.
This brings contextual logging to code that knows nothing about this package: an external library calling slog.InfoContext(ctx, ...) emits its record through the request's logger with all of the logger's accumulated attributes. In code you control, Ctx remains the preferred way to log contextually.
Routing intentionally applies only at the root handler:
- Derived loggers (slog.Default().With(...)) log through their own plain handler and are not routed. Re-routing them would silently drop the attributes accumulated in the derived handler's state.
- Only the *Context call forms can be routed: slog.Info(...) carries no context to extract a logger from.
The record is forwarded to the context logger's handler as-is, preserving the source attribution, and Enabled consults that handler too, so the context logger's level gating applies. Routing is loop-safe: the extracted logger is not re-extracted even when its own handler chain contains the routing handler (e.g. when the bare default logger is stored in the context).
Example ¶
With context routing enabled, records logged by foreign code through the default logger's *Context methods are routed to the request-scoped logger stored in the context and pick up its attributes.
package main
import (
"context"
"log/slog"
advslog "github.com/adventures-team/go-adv-slog"
)
// fetchFromAPI simulates code in an external library that knows nothing about
// this package: it logs with a plain log/slog context-aware call through the
// default logger, not through [advslog.Ctx].
func fetchFromAPI(ctx context.Context) {
slog.InfoContext(ctx, "fetching", "attempt", 1)
}
// With context routing enabled, records logged by foreign code through the
// default logger's *Context methods are routed to the request-scoped logger
// stored in the context and pick up its attributes.
func main() {
advslog.Init(advslog.WithContextRouting())
ctx := advslog.NewContext(context.Background(),
slog.Default().With("request_id", "req-42"))
fetchFromAPI(ctx)
// The foreign record carries the context logger's attributes:
// {"time":"...","level":"INFO","msg":"fetching","request_id":"req-42","attempt":1}
}
Output:
func WithLevelPrefixes ¶ added in v0.0.6
WithLevelPrefixes replaces the level prefixes the standard-log level inference consults (DefaultLevelPrefixes) with prefixes. Matching is ASCII case-insensitive and the delimiter is part of the prefix: a prefix must end in a byte that is not a letter or digit, such as ":" or "]"; others are skipped with a warning.
To extend the defaults, amend the copy DefaultLevelPrefixes returns.
func WithStdLogLevel ¶ added in v0.0.6
WithStdLogLevel sets the level of standard-log messages that start with no level prefix (or of all of them under DisableLevelInference), INFO by default. It replaces slog.SetLogLoggerLevel, which has no effect once Init or InitTest has routed the standard log package into advslog.
type TestingOutput ¶ added in v0.0.4
TestingOutput is the subset of testing.TB needed by InitTest: the Output method of *testing.T, *testing.B and *testing.F. It is a separate interface so that importing advslog does not pull in the testing package.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package logmask masks sensitive values in logged data: plain strings, URL query parameters (ValuesMasker) and JSON documents addressed by XPath (XPathMasker).
|
Package logmask masks sensitive values in logged data: plain strings, URL query parameters (ValuesMasker) and JSON documents addressed by XPath (XPathMasker). |
|
Package reqlog logs outgoing requests to external APIs — with duration, status and masking of sensitive data — through the logger stored in the context (see advslog.Ctx).
|
Package reqlog logs outgoing requests to external APIs — with duration, status and masking of sensitive data — through the logger stored in the context (see advslog.Ctx). |