Documentation
¶
Overview ¶
Package log is keel's structured-logging foundation: a thin layer over the standard library's log/slog that every keel consumer shares. It is imported under the alias "logging" by convention (import logging "github.com/david-aggeler/keel/log") to avoid colliding with the stdlib "log" package.
Four sinks ¶
A production logger fans one log record out to the sinks selected by Config:
- the console — sparse-AI, human-readable text, machine JSON, or none;
- a daily human-readable rolling file under Config.TextDir; and
- a daily JSON Lines rolling file under Config.JSONLDir.
New is the single public logger constructor. File sinks opened by New are owned by the returned Logger and released by Logger.Close. All sinks share one field schema — ts (RFC3339Nano), level (uppercase), msg, service — so the JSON and human renderings of a record always agree.
Typical use ¶
Construct one logger from a Config, defer its Logger.Close, then log through the leveled methods and the banner/field helpers:
logger, err := log.New(log.Config{Service: "gateway", Console: log.ConsolePlain, TextDir: ".logs"})
if err != nil {
return err
}
defer logger.Close()
logger.BuildIdentity("gateway", version, gitCommit) // ruled startup banner + build identity
logger.Debug("config loaded", "path", cfgPath)
logger.Info("listening", "addr", addr)
logger.Warn("retrying", "attempt", n)
logger.Error("request failed", "err", err)
logger.Section("shutdown")
The leveled methods — Logger.Debug, Logger.Info, Logger.Warn, Logger.Error and their *Context variants — take a message and alternating key/value args, exactly like log/slog. The minimum level emitted is set by Config.ConsoleVerbosity (nil defaults to Info), so Debug is dropped unless ConsoleVerbosity is lowered to slog.LevelDebug. File sinks use Config.FileVerbosity (nil defaults to Debug).
Request-scoped loggers ¶
WithLogger carries a request-scoped log/slog.Logger through a context.Context, and FromContext reads it back at downstream call sites, falling back to slog.Default when the context has no logger. Callers enrich the logger with slog's own With method before storing or emitting; keel does not interpret context values or inject attributes itself.
Redaction at the boundary ¶
Every rendered string — messages, attr values, and error text — passes through the same secret-scrubbing path before it reaches any sink: DSN passwords, bearer tokens, and PATs in URLs or query params are masked, and attrs whose key looks sensitive (token/password/secret/pat) are dropped wholesale. RedactErr exposes the same treatment for errors. Redaction is applied once, at the log boundary, so callers never have to pre-scrub values they log.
Beyond the sinks ¶
OperationalError is an error type that bundles an operation name, a human-facing message, the underlying cause, task/log-file/line/exit-code/hint diagnostics, and arbitrary structured metadata into one value. It implements log/slog.LogValuer, so its string content is redacted at the log boundary like any other logged value; reach for it only where the same multi-field failure context is logged repeatedly.
The remaining surface hangs off Logger. Logger.Header emits a banner-only rule, Logger.BuildIdentity emits a startup banner plus the structured build identity event, and Logger.Section emits section banners. Logger.Field and Logger.Fields emit aligned label/value rows (FieldRow) — these render in every console mode, not just plain text. Logger.Emit logs a metrics event, and Logger.LogBuildIdentity logs only the one-line build-identity record.
Index ¶
- func Discard() *slog.Logger
- func FromContext(ctx context.Context) *slog.Logger
- func LevelFromString(s string) (slog.Level, error)
- func LevelToString(level slog.Level) string
- func RedactErr(err error) error
- func RedactString(s string) string
- func ResolveGitCommit(gitCommit string) string
- func WithLogger(ctx context.Context, l *slog.Logger) context.Context
- type Config
- type Console
- type FieldRow
- type Logger
- func (l *Logger) BuildIdentity(title, version, gitCommit string)
- func (l *Logger) Close() error
- func (l *Logger) Debug(msg string, args ...any)
- func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)
- func (l *Logger) Emit(event string, attrs ...slog.Attr)
- func (l *Logger) Error(msg string, args ...any)
- func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any)
- func (l *Logger) Event(verb, msg string, fields ...any)
- func (l *Logger) Field(label string, value any)
- func (l *Logger) Fields(rows []FieldRow)
- func (l *Logger) Header(title string, version string)
- func (l *Logger) Info(msg string, args ...any)
- func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any)
- func (l *Logger) JSONLLogPath() string
- func (l *Logger) LogBuildIdentity(version, gitCommit string)
- func (l *Logger) RunLogLine() int
- func (l *Logger) RunLogPath() string
- func (l *Logger) Section(name string)
- func (l *Logger) Slog() *slog.Logger
- func (l *Logger) StartDailyBuildIdentity(ctx context.Context, version, gitCommit string)
- func (l *Logger) TextLogPath() string
- func (l *Logger) Warn(msg string, args ...any)
- func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any)
- func (l *Logger) With(args ...any) *Logger
- func (l *Logger) WithGroup(name string) *Logger
- type OperationalError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Discard ¶
Discard returns a logger that drops every record at every level. It is the one way to spell "produce no output" at an injection point that takes a logger, replacing the ad-hoc slog.New(slog.NewTextHandler(io.Discard, nil)) self-bootstrap that consumers otherwise hand-roll.
DHF-REQ: keel/requirement-122
func FromContext ¶
FromContext returns the request-scoped logger carried by ctx, or slog.Default when no non-nil logger has been stored.
DHF-REQ: keel/requirement-68
Example ¶
DHF-TEST: keel/requirement-68
package main
import (
"bytes"
"context"
"fmt"
"log/slog"
"strings"
logging "github.com/david-aggeler/keel/log"
)
func main() {
var buf bytes.Buffer
ctx := logging.WithLogger(context.Background(), exampleTextLogger(&buf))
logging.FromContext(ctx).InfoContext(ctx, "done", "attempt", 2)
fmt.Println(strings.TrimSpace(buf.String()))
}
func exampleTextLogger(w *bytes.Buffer) *slog.Logger {
return slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
}
Output: level=INFO msg=done attempt=2
func LevelFromString ¶ added in v0.5.0
LevelFromString converts keel's lowercase level vocabulary to a slog level. Empty input defaults to Info; unknown non-empty input is rejected.
DHF-REQ: keel/requirement-56
func LevelToString ¶ added in v0.5.0
LevelToString converts slog's standard levels to keel's lowercase vocabulary. Non-standard slog levels use slog's rendering in lowercase.
DHF-REQ: keel/requirement-56
func RedactErr ¶
RedactErr walks the error string and strips DSN passwords and bearer tokens. Returns nil for nil input. Delegates regex work to redactString; flatten-no-wrap contract is unchanged — errors.Is/As do NOT see through a redacted error.
func RedactString ¶
RedactString strips DSN passwords and bearer tokens from a rendered string.
DHF-REQ: keel/requirement-56
func ResolveGitCommit ¶
ResolveGitCommit resolves the git commit stamped into build-identity records. An explicit non-"dev" value wins; otherwise Go build info vcs.revision is used, with "-modified" appended when vcs.modified is true. If no revision is available, it returns "unknown".
DHF-REQ: keel/requirement-56
func WithLogger ¶
WithLogger returns a child context carrying l as the request-scoped logger.
DHF-REQ: keel/requirement-68
Example ¶
DHF-TEST: keel/requirement-68
package main
import (
"bytes"
"context"
"fmt"
"log/slog"
"strings"
logging "github.com/david-aggeler/keel/log"
)
func main() {
var buf bytes.Buffer
ctx := logging.WithLogger(context.Background(), exampleTextLogger(&buf).With("request_id", "req-123"))
logging.FromContext(ctx).InfoContext(ctx, "handled")
fmt.Println(strings.TrimSpace(buf.String()))
}
func exampleTextLogger(w *bytes.Buffer) *slog.Logger {
return slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
}
Output: level=INFO msg=handled request_id=req-123
Types ¶
type Config ¶
type Config struct {
// Service is the value stamped into the "service" field of every record.
Service string
// ConsoleVerbosity is the minimum severity emitted to the console sink.
// Nil -> Info.
ConsoleVerbosity slog.Leveler
// FileVerbosity is the minimum severity emitted to text and JSONL file
// sinks. Nil -> Debug.
FileVerbosity slog.Leveler
// Console selects the console rendering. Empty → ConsoleSparseAI.
Console Console
// Writer is the console sink destination. Nil → os.Stdout. Set it to a
// bytes.Buffer (or any io.Writer) to capture console output in tests.
Writer io.Writer
// TextDir, when non-empty, opens a daily human-readable .log file sink.
TextDir string
// JSONLDir, when non-empty, opens a daily JSON Lines .jsonl file sink.
JSONLDir string
// PerRun, when true, makes JSONLDir use a per-invocation JSON Lines file
// whose path and line counter back RunLogPath and RunLogLine. When false,
// JSONLDir uses the daily rolling JSONL file.
PerRun bool
// SourceInFiles keeps automatic caller source enabled for text file sinks.
SourceInFiles bool
// ForceColor forces ANSI color on the console sink even when the writer is
// not a terminal. Ignored when NO_COLOR is set or DisableColor is true.
ForceColor bool
// DisableColor suppresses ANSI color on the console sink unconditionally.
DisableColor bool
// ConsoleOmitKeys suppresses selected attrs from the human console sink.
// Machine JSON logging is intentionally unaffected.
ConsoleOmitKeys []string
// Handlers are additional slog handlers fanned out after the built-in sinks.
// Optional packages such as log/otel use this hook so the core log package
// keeps its dependency surface unchanged.
Handlers []slog.Handler
}
Config holds the parameters for constructing a production logger. The zero value is usable: Service is blank, console verbosity defaults to Info, file verbosity defaults to Debug, and output goes to os.Stdout with the sparse-AI console and no file sinks. All fields are optional.
DHF-REQ: keel/requirement-30, keel/requirement-33, keel/requirement-56
type Console ¶ added in v0.2.0
type Console string
Console selects the process console rendering for New.
const ( // ConsoleSparseAI emits sparse JSON events used by agent-oriented runs. ConsoleSparseAI Console = "sparse-ai" // ConsolePlain emits the human-readable console formatter. ConsolePlain Console = "plain" // ConsoleJSON emits verbose JSON records to the console writer. ConsoleJSON Console = "json" // ConsoleNone disables the console sink. ConsoleNone Console = "none" )
type FieldRow ¶
type FieldRow struct {
// Label is the left-hand column text; the widest Label in a batch sets the
// alignment width for all rows.
Label string
// Value is the right-hand value, rendered with fmt's default %v verb.
Value any
}
FieldRow is one aligned label/value row rendered by [Fields].
type Logger ¶ added in v0.2.0
type Logger struct {
// contains filtered or unexported fields
}
Logger is keel/log's public logger. It wraps slog while owning any file sinks opened by New.
DHF-REQ: keel/requirement-16, keel/requirement-33
func New ¶
New creates a production logger from the four-sink Config model.
DHF-REQ: keel/requirement-16, keel/requirement-22, keel/requirement-29, keel/requirement-33, keel/requirement-56, openbrain/requirement-602
Example ¶
ExampleNew shows constructing a JSON logger over a caller-supplied writer and reading back the single record it emits. Production callers leave Writer nil to log to os.Stdout; here a bytes.Buffer captures the output so the example is deterministic.
DHF-TEST: keel/user_need-1 (keel/ac-48)
package main
import (
"bytes"
"encoding/json"
"fmt"
logging "github.com/david-aggeler/keel/log"
)
func main() {
var buf bytes.Buffer
logger, err := logging.New(logging.Config{Service: "demo", Console: logging.ConsoleJSON, Writer: &buf})
if err != nil {
fmt.Println(err)
return
}
logger.Info("service starting", "port", 8080)
var rec map[string]any
_ = json.Unmarshal(buf.Bytes(), &rec)
fmt.Println(rec["service"], rec["level"], rec["msg"], rec["port"])
}
Output: demo INFO service starting 8080
func (*Logger) BuildIdentity ¶ added in v0.6.1
BuildIdentity emits a startup banner and a structured build identity record.
DHF-REQ: keel/requirement-32
func (*Logger) DebugContext ¶ added in v0.2.0
DebugContext emits a DEBUG record with ctx.
func (*Logger) ErrorContext ¶ added in v0.2.0
ErrorContext emits an ERROR record with ctx.
func (*Logger) Header ¶ added in v0.2.0
Header emits a ruled banner only, rendered per console mode.
func (*Logger) InfoContext ¶ added in v0.2.0
InfoContext emits an INFO record with ctx.
func (*Logger) JSONLLogPath ¶ added in v0.5.0
JSONLLogPath returns the JSONL file path opened by this logger, or an empty string when JSONLDir is not configured.
DHF-REQ: keel/requirement-56
func (*Logger) LogBuildIdentity ¶ added in v0.4.0
LogBuildIdentity logs a single build identity record.
func (*Logger) RunLogLine ¶ added in v0.2.0
RunLogLine returns the 1-based line number of the last JSONL record written by this logger. It returns zero when no JSONL sink is configured or no record has been written yet.
DHF-REQ: keel/requirement-19
func (*Logger) RunLogPath ¶ added in v0.2.0
RunLogPath returns the JSONL run-log path selected by Config.PerRun, or the JSONL file path for the current logger when JSONLDir is configured.
DHF-REQ: keel/requirement-19
func (*Logger) Section ¶ added in v0.2.0
Section emits a ruled section banner, rendered per console mode.
func (*Logger) Slog ¶ added in v0.2.0
Slog returns the wrapped slog logger for APIs that have not migrated yet.
func (*Logger) StartDailyBuildIdentity ¶ added in v0.5.0
StartDailyBuildIdentity starts the daily build-identity heartbeat for this logger. The goroutine exits when ctx is canceled.
DHF-REQ: keel/requirement-56
func (*Logger) TextLogPath ¶ added in v0.5.0
TextLogPath returns the daily text log path opened by this logger, or an empty string when TextDir is not configured.
DHF-REQ: keel/requirement-56
func (*Logger) WarnContext ¶ added in v0.2.0
WarnContext emits a WARN record with ctx.
type OperationalError ¶
type OperationalError struct {
Op string // operation/handler name, e.g. "link_blocks"
Message string // human-facing summary, e.g. "cross-product link rejected"
Err error // underlying cause; may be nil
Task string // failing task label, e.g. "ci:vet"
LogFile string // run-log file carrying the task's records
StartLine int // 1-based line number of the task's first run-log record
ExitCode int // process exit code to return for this failure
Hint string // human instruction pointing at LogFile and StartLine
Metadata map[string]any // structured context; string values are redacted in LogValue
}
OperationalError is an opt-in log carrier (not a wire-envelope type) that bundles an operation name, a human message, an underlying error, and arbitrary structured metadata into one value, and renders itself for slog with redaction applied at the log boundary (slog.LogValuer).
It is NOT a replacement for the CR-0221 KD1 flat-field convention. Use it only where the same multi-field failure context is logged repeatedly.
Redaction contract: LogValue() routes root_cause and every *string* value in Metadata through RedactString. Non-string metadata values are emitted via slog.Any WITHOUT redaction — callers MUST NOT place secrets (DSNs, bearer tokens) in non-string metadata. (KD8.)
DHF-REQ: keel/requirement-18
func (*OperationalError) Error ¶
func (e *OperationalError) Error() string
Error renders Op, Message, and Err, skipping empty segments. NOT redacted — this is the developer-facing string; redaction happens only at LogValue. (KD10.) Returns "<nil>" when called on a nil receiver. Returns "operational error" when all fields are empty.
func (*OperationalError) LogValue ¶
func (e *OperationalError) LogValue() slog.Value
LogValue implements slog.LogValuer. It emits a GroupValue nested under the caller-chosen attr key (e.g. slog.Any("err", opErr) → an "err" group). It never emits G1 reserved keys (ts/level/msg/service). (KD8.) Metadata keys op/message/root_cause are reserved and silently dropped to avoid colliding with the carrier's own fields. Returns an empty GroupValue when called on a nil receiver.
DHF-REQ: keel/requirement-18
func (*OperationalError) Unwrap ¶
func (e *OperationalError) Unwrap() error
Unwrap exposes the cause so errors.Is/errors.As work through the carrier. Unlike the wire-string concat sites, a Go error chain genuinely survives here. (KD10.)
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package logtest provides keel/log's optional test capture handler.
|
Package logtest provides keel/log's optional test capture handler. |
|
Package otel provides keel/log's optional OpenTelemetry log exporter bridge.
|
Package otel provides keel/log's optional OpenTelemetry log exporter bridge. |
|
Package recent provides an optional in-memory recent-log tail handler.
|
Package recent provides an optional in-memory recent-log tail handler. |