go-log/docs/architecture.md
Virgil f72c8daf3b
Some checks failed
CI / test (push) Failing after 2s
CI / auto-fix (push) Failing after 0s
CI / auto-merge (push) Failing after 0s
refactor(ax): align recovery helpers with AX docs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 13:36:30 +00:00

243 lines
7.1 KiB
Markdown

---
title: Architecture
description: 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
```go
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
```go
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
```go
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")
Retryable bool // whether the caller can retry the operation
RetryAfter *time.Duration // optional retry delay hint
NextAction string // suggested next step when not retryable
}
```
`Err` implements both the `error` and `Unwrap` interfaces so it participates
fully in the standard `errors.Is` / `errors.As` machinery.
### Options and RotationOptions
```go
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
| extract recovery hints -> append "retryable",
| "retry_after_seconds",
| "next_action" 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:
```go
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:
```go
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]`:
```go
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:
```go
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:
```go
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`.
- `shouldLog` and `log` acquire a read lock to snapshot the level, output, and
redact keys.
- `SetLevel`, `SetOutput`, and `SetRedactKeys` acquire a write lock.
- The default logger is a package-level variable set at init time. `SetDefault`
replaces 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.