6.7 KiB
| title | description |
|---|---|
| Architecture | Internals of go-log -- types, data flow, and design decisions |
Architecture
go-log is split into two complementary halves that share a single package:
structured logging (log.go) and structured errors (errors.go).
The two halves are wired together so that when an *Err value appears in a
log line's key-value pairs the logger automatically extracts the operation
name and stack trace.
Key Types
Level
type Level int
const (
LevelQuiet Level = iota // suppress all output
LevelError // errors only
LevelWarn // warnings + errors
LevelInfo // info + warnings + errors
LevelDebug // everything
)
Levels are ordered by increasing verbosity. A message is emitted only when its
level is less than or equal to the logger's configured level. LevelQuiet
suppresses all output, including errors.
Logger
type Logger struct {
mu sync.RWMutex
level Level
output io.Writer
redactKeys []string
// Overridable style functions
StyleTimestamp func(string) string
StyleDebug func(string) string
StyleInfo func(string) string
StyleWarn func(string) string
StyleError func(string) string
StyleSecurity func(string) string
}
All fields are protected by sync.RWMutex, making the logger safe for
concurrent use. The Style* function fields default to the identity function;
consumers (such as a TUI layer) can replace them to add ANSI colour or other
decoration without forking the logger.
Err
type Err struct {
Op string // e.g. "user.Save"
Msg string // human-readable description
Err error // underlying cause (optional)
Code string // machine-readable code (optional, e.g. "VALIDATION_FAILED")
}
Err implements both the error and Unwrap interfaces so it participates
fully in the standard errors.Is / errors.As machinery.
Options and RotationOptions
type Options struct {
Level Level
Output io.Writer
Rotation *RotationOptions
RedactKeys []string
}
type RotationOptions struct {
Filename string
MaxSize int // megabytes, default 100
MaxAge int // days, default 28
MaxBackups int // default 5
Compress bool // default true
}
When Rotation is provided and RotationWriterFactory is set, the logger
writes to a rotating file instead of the supplied Output.
Data Flow
Logging a Message
caller
|
v
log.Info("msg", "k1", v1, "k2", v2)
|
v
defaultLogger.Info(...) -- package-level proxy
|
v
shouldLog(LevelInfo) -- RLock, compare level, RUnlock
| (if filtered out, return immediately)
v
log(LevelInfo, "[INF]", ...)
|
+-- format timestamp with StyleTimestamp
+-- scan keyvals for error values:
| if any value implements `error`:
| extract Op -> append "op" key if not already present
| extract FormatStackTrace -> append "stack" key if not already present
+-- format key-value pairs:
| string values -> %q (quoted, injection-safe)
| other values -> %v
| redacted keys -> "[REDACTED]"
+-- write single line to output:
"<timestamp> <prefix> <msg> <kvpairs>\n"
Building an Error Chain
root cause (any error)
|
v
log.E("db.Query", "query failed", rootErr)
| -> &Err{Op:"db.Query", Msg:"query failed", Err:rootErr}
v
log.Wrap(err, "repo.FindUser", "user lookup failed")
| -> &Err{Op:"repo.FindUser", Msg:"user lookup failed", Err:prev}
v
log.Wrap(err, "handler.Get", "request failed")
| -> &Err{Op:"handler.Get", Msg:"request failed", Err:prev}
v
log.StackTrace(err)
-> ["handler.Get", "repo.FindUser", "db.Query"]
log.FormatStackTrace(err)
-> "handler.Get -> repo.FindUser -> db.Query"
log.Root(err)
-> rootErr (the original cause)
Wrap preserves any Code from a wrapped *Err, so error codes propagate
upward automatically.
Combined Log-and-Return
LogError and LogWarn combine two operations into one call:
func LogError(err error, op, msg string) error {
wrapped := Wrap(err, op, msg) // 1. wrap with context
defaultLogger.Error(msg, ...) // 2. log at Error level
return wrapped // 3. return wrapped error
}
Both return nil when given a nil error, making them safe to use
unconditionally.
Must follows the same pattern but panics instead of returning, intended for
startup-time invariants that must hold.
Security Features
Log Injection Prevention
String values in key-value pairs are formatted with %q, which escapes
newlines, quotes, and other control characters. This prevents an attacker
from injecting fake log lines via user-controlled input:
l.Info("msg", "key", "value\n[SEC] injected message")
// Output: ... key="value\n[SEC] injected message" (single line, escaped)
Key Redaction
Keys listed in RedactKeys have their values replaced with [REDACTED]:
l := log.New(log.Options{
Level: log.LevelInfo,
RedactKeys: []string{"password", "token"},
})
l.Info("login", "user", "admin", "password", "secret123")
// Output: ... user="admin" password="[REDACTED]"
Security Log Level
The Security method uses a dedicated [SEC] prefix and logs at LevelError
so that security events remain visible even in restrictive configurations:
l.Security("unauthorised access", "user", "admin", "ip", "10.0.0.1")
// Output: 14:32:01 [SEC] unauthorised access user="admin" ip="10.0.0.1"
Log Rotation
go-log defines the RotationOptions struct and an optional
RotationWriterFactory variable:
var RotationWriterFactory func(RotationOptions) io.WriteCloser
This is a seam for dependency injection. The core/go-io package (or any
other provider) can set this factory at init time. When Options.Rotation is
provided and the factory is non-nil, the logger creates a rotating file writer
instead of using Options.Output.
This design keeps go-log free of file-system and compression dependencies.
Concurrency Model
- All Logger fields are guarded by
sync.RWMutex. shouldLogandlogacquire a read lock to snapshot the level, output, and redact keys.SetLevel,SetOutput, andSetRedactKeysacquire a write lock.- The default logger is a package-level variable set at init time.
SetDefaultreplaces it (not goroutine-safe itself, but intended for use during startup).
Default Logger
A package-level defaultLogger is created at import time with LevelInfo and
os.Stderr output. All top-level functions (log.Info, log.Error, etc.)
delegate to it. Use log.SetDefault to replace it with a custom instance.