logging.MustInit(
logging.WithLevel(logging.LevelDebug),
logging.WithFormat(logging.FormatJSON),
logging.WithTarget(logging.StdOut),
)
defer logging.Sync()Init returns an error and keeps the active backend unchanged if the configuration is invalid or the output target cannot be opened. MustInit panics on error (acceptable only in main). If Init was never called, the first package-level call (NewLogger, NewNamedLogger, Sync, SetLevel, Default and so on) initializes the package with the default settings, and there is no panic.
Init called a second time does not return an error: it builds a new backend and makes it the active one. Loggers created earlier keep writing through the previous backend (with its levels, format and target). Call Init once at program start, before creating loggers. Only Backend.Init on an already initialized Backend value returns an error (logging: already configured).
log := logging.NewLogger() // level from Config.Level
db := logging.NewNamedLogger("db") // Config.CustomLevels["db"], otherwise Config.Level
dbg := logging.NewNamedLogger("db", logging.LevelDebug) // an explicit level wins over everythingLevel priority: the explicit argument, then CustomLevels[name], then Config.Level.
The level methods Trace..Error and their f variants are filtered by the logger level, Error included. Fatal and Panic ignore the level (see below).
Trace records are written with the level name trace; in syslog they have severity debug.
WithLevel(l) returns a copy with another level (more or less verbose than the parent).
NewLogger and NewNamedLogger do not flush on their own: call logging.Sync() before exit (loggers also implement logging.Syncer). NewLoggerCtx and NewNamedLoggerCtx flush when the context is done.
Message arguments follow fmt.Sprint rules (a space is added between operands only when neither is a string), but a single string argument is used as is. Infof with no arguments does not interpret the template, so Infof("100%") is safe. Info() with no arguments writes an empty message.
The Logger interface is not extended; additional capabilities are optional interfaces (like Syncer), implemented by the package loggers, Default and TestLogger:
if p, ok := log.(logging.Panicer); ok { p.Panicf("bad state: %v", err) } // writes a panic-level record, then panic(msg)
child := log.(logging.Namer).WithName("client") // named "http.client" if log is named "http"Panic and Panicf write and panic regardless of the logger level, like Fatal. They panic with the message string, and hooks run before the panic. WithName appends the name after a dot and keeps the parent fields and context. An explicit level of the parent (NewLogger(level), WithLevel) is inherited; otherwise the level is chosen by the full name as in NewNamedLogger (CustomLevels, then the global level; SetNamedLevel works too). On Default, WithName is the same as NewNamedLogger(name).
A logger created before Init (for example in a package-level var log = logging.NewLogger()) is bound to the backend that was active when it was created, and does not see a later Init. Use logging.Default instead: its zero value is ready to use and it always follows the active backend.
var log logging.Default // safe to declare at package level; do not copy after first use
func main() {
logging.MustInit(logging.WithFormat(logging.FormatJSON))
log.Info("started") // goes through the backend created by MustInit
}log.WithKey("user", 42).WithKeys(logging.Keys{"a": 1, "b": 2}).WithError(err).Info("done")Fields from WithKeys are written sorted by key, so the order is deterministic. WithError(nil), a nil or empty WithKeys return the same logger. WithError(err) stores the error text under the error key and always adds the caller, whatever the configured level.
Every WithKey/WithKeys clones the zap core (about 1 us and 6 to 11 allocations), so do not build a logger with fields for every record on a hot path: create it once and pass it around, and set several fields with a single WithKeys. Numbers and method: research/keys-cost.md.
Declare keys once in the constants of a module and attach values with the Value method:
const (
KeyUser logging.IntKey = "user" // KeyUser.Value("x") does not compile
KeyOrderID logging.StringKey = "order_id"
KeyPassword logging.RedactedKey[string] = "password" // the value is always ***
KeyTrace logging.Key = "trace" // no type check
)
log = logging.WithValuesFrom(log, KeyUser.Value(42), KeyOrderID.Value("A-17"))TypedKey[T] and its aliases (StringKey, IntKey, Int64Key, Float64Key, BoolKey, DurationKey, TimeKey, ErrorKey) check the value type at compile time; for other types write TypedKey[T]. This is protection against mistakes in calls, not an optimization. RedactedKey[T] checks the type but never keeps or encodes the value: the record always contains ***. It has no SetToCtx and no Key() method, so a secret cannot bypass masking through the context.
A custom type can describe its own fields, and static module values are declared as logging.Values:
func (o Order) LoggingValues() []logging.Value {
return []logging.Value{KeyOrderID.Value(o.ID), KeyPassword.Value(o.Secret)}
}
var moduleFields = logging.Values{KeyModule.Value("billing")}
log = logging.WithValuesFrom(log, moduleFields, order)WithValuesFrom accepts Value, Values and any LoggingValuer. Fields are written in the order of the arguments and of their values, not alphabetically. nil sources and values with an empty key are skipped, and duplicate keys are not merged. All fields are added in one call, which is cheaper than a chain of WithKey (about 1 us, 11 allocations, see research/keys-cost.md). LoggingValues is called on every WithValuesFrom and a panic in it (for example on a nil pointer) is not recovered, so build a logger with the fields of an object once, not for every record. Config.Redact masks such fields by key name as well.
reqID := logging.Key("request_id")
logging.MustInit(logging.WithContextExtractors(reqID.Extractor()))
ctx := reqID.SetToCtx(ctx, "r-1")
logging.NewLoggerCtx(ctx).Info("x") // the record gets the request_id field
log.WithContext(ctx).Info("y") // the same for an existing loggerNewLoggerCtx and NewNamedLoggerCtx already apply WithContext; applying it again duplicates the key. They also call Sync when the context is done (context.AfterFunc on Go 1.21+, otherwise a goroutine; no goroutine is started for contexts that are never done, such as context.Background()).
Extractors are sorted by key, so extracted fields have a stable order. If several extractors report the same key, the last registered wins. An extractor returning a nil value omits the field. An extractor is called once with context.Background() at registration to learn its key, so it must not panic on an empty context. Keys.SetToCtx puts several values into a context at once, and logging.KeysCtx(ctx) returns the keys extracted from a context using the active configuration.
logging.Sync() flushes buffers. logging.CloseOutputs() syncs and closes all opened files, syslog connections and registered writers; process standard streams are kept open. For stdout and stderr, Sync errors like EINVAL/ENOTTY (terminals and pipes) are ignored.
logging.SetLevel(logging.LevelDebug) // global level of the active backend
logging.SetNamedLevel("db", logging.LevelTrace) // only loggers named db
logging.ResetNamedLevel("db") // back to the CustomLevels valueSetLevel and SetNamedLevel return an error for an unknown level and change nothing. The change is visible to already created loggers, including derived ones (WithKey, WithContext) and Default. Loggers created with an explicit level (NewLogger(level), WithLevel) are not changed. A named logger without its own level follows the global one. A level set in CustomLevels (or with SetNamedLevel) takes priority over the global one. ResetNamedLevel for a name without a CustomLevels entry makes the loggers follow the global level again.
Limitations: Init creates a new backend with its own levels, so calling SetLevel on the old one no longer affects new loggers. The caller field is added only if the most verbose configured level at Init (Level and CustomLevels) was debug or trace, or the logger was made with WithError; raising verbosity at run time does not add it.
The Logger interface has no SetLevel method, so third-party implementations of the interface keep compiling.
The package does not listen for signals or watch files: re-reading the configuration (SIGHUP, file watch, ConfigMap) is up to the application. After the application has re-read its config, apply the levels like this:
func reloadLevels(newCfg, oldCfg *logging.Config) error {
if err := newCfg.Validate(); err != nil { // check everything before applying
return err
}
if err := logging.SetLevel(newCfg.Level); err != nil {
return err
}
for name, level := range newCfg.CustomLevels {
if err := logging.SetNamedLevel(name, level); err != nil {
return err
}
}
for name := range oldCfg.CustomLevels { // names removed from the config
if _, ok := newCfg.CustomLevels[name]; !ok {
if err := logging.SetNamedLevel(name, newCfg.Level); err != nil {
return err
}
}
}
return nil
}Caveats:
- The file becomes the source of truth: levels set manually with
SetNamedLevelor over HTTP for names from the config are overwritten. - A name removed from the config gets the current global level as an explicit value and stops following the global level.
ResetNamedLeveldoes not fit here: it restores the value from the configurationInitwas called with, not from the previous reload. - Format, output target and context extractors cannot change on the fly. They need a new
Init(loggers created before it stay on the old backend).
logging.LevelsHandler() returns an http.Handler to view and change the levels of the active backend (after Init it serves the new backend). It is disabled by default: until levelsEndpoint: true (or logging.WithLevelsEndpoint(true)) is set explicitly, every request gets 404 and no levels change. The package never opens a port itself; you mount the handler. The protection is twofold: the handler has to be mounted on a server and enabled in the configuration.
Enable it in the configuration:
levelsEndpoint: trueor with logging.Init(logging.WithLevelsEndpoint(true)). The key affects only the HTTP handler; logging.SetLevel and the other functions called from code always work.
admin := http.NewServeMux()
admin.Handle("/debug/log-levels", logging.LevelsHandler())
go http.ListenAndServe("127.0.0.1:6060", admin)curl localhost:6060/debug/log-levels
# {"level":"info","loggers":{"db":"error","http":"info"}}
curl -X PUT -d '{"level":"debug"}' localhost:6060/debug/log-levels # global
curl -X PUT -d '{"logger":"db","level":"trace"}' localhost:6060/debug/log-levels # loggers named db
curl -X PUT -d '{"logger":"db","reset":true}' localhost:6060/debug/log-levels # restore from configloggers lists the loggers created by name or listed in CustomLevels, with their effective level. A name that does not exist yet can be set in advance. Levels are accepted the same way as in the config (debug, dbg, d, ...). Other methods get 405; invalid JSON, an unknown field or an invalid level get 400 with {"error": "..."}. The request body is limited to 4 KiB.
| Response | When |
|---|---|
200 and {"level":..., "loggers":{...}} |
GET, successful PUT |
400 and {"error":"..."} |
invalid JSON, unknown field, invalid level, reset without a logger name or together with a level |
| 404 | levelsEndpoint is disabled |
405 with Allow: GET, PUT |
any other method |
Security: an enabled handler has no authentication. Anyone who can reach it can turn on verbose logging or hide errors. Enable it only when it is mounted on an administrative port that is closed from the outside.
WithHook(min, fn) calls fn for every written record of level min and above, for example to count errors:
logging.Init(logging.WithHook(logging.LevelError, func(e logging.HookEntry) {
errorsTotal.WithLabelValues(e.Logger).Inc()
}))HookEntry holds the time, level, logger name, message text and Caller (if collected); it does not contain the record fields. Records dropped by the logger level do not reach the hook. Hooks are called in registration order, synchronously, in the goroutine that writes the record, so the function must not block: use a counter or a non-blocking channel send. A panic in the function is recovered and ignored. For Fatal the hook runs before the process exits, for Panic before the panic. Hooks cannot be set in YAML; WithConfig appends the hooks of src to those already set, and TestLogger calls them too.
nilcontext.WithContext(nil),NewLoggerCtx(nil)andNewNamedLoggerCtx(nil, ...)are valid: no fields are extracted and no flush is scheduled.nilerror.WithError(nil)returns the same logger and adds nothing.- Empty keys.
WithKey("", v), an empty key inWithKeysand a value with an empty key inWithValuesFromare skipped: no field is added.Config.Fieldsrejects empty keys inValidate. - Duplicate keys are not merged:
WithKey("a", 1).WithKey("a", 2), a repeated key inWithValuesFrom, or a logger field with the same name as a static field are all written as separate fields. JSON output then contains the key more than once. FatalandPanicignore the level. A logger atLevelFatalor above still writes them;Fatalends the process withos.Exit(1)(deferred functions do not run),Panicpanics with the message string. On stdout and stderr targets aFatalrecord also gets a stack trace.Inittwice replaces the backend and does not return an error (see Initialization). OnlyBackend.Initon an initialized backend fails.- Loggers created before
Init(including package-level variables) stay on the old backend. Uselogging.Default, which follows the active backend. Traceis written astrace. In syslog its severity is debug, because syslog has no level below debug.calleris added only when the most verbose configured level (LevelandCustomLevels) is debug or trace, or the logger was made withWithError.- Text format escapes control characters in the message and the logger name: line breaks become
\nand\r, other control characters except tab become\xNN. JSON and syslog encoders escape strings themselves. See security.md. WithConfigcopiesLevelandLevelsEndpointfrom the source unconditionally (a zeroConfigmeansLevelTrace), takesFormatandOutputonly if non-empty, and mergesCustomLevels,Fields,Redact, hooks and context extractors into those already set. The source is copied, so later changes to it have no effect.- Unknown level or invalid configuration returns an error that matches
errors.Is(err, logging.Error);SetLevel(Level(99))returns an error too. - Redaction does not mask the message text, only fields (see security.md).
- Closed output. A closed writer returns
ErrClosedfromWrite; the logger neither blocks nor panics, and the record is lost.