log

package
v0.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Discard

func Discard() *slog.Logger

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

func FromContext(ctx context.Context) *slog.Logger

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

func LevelFromString(s string) (slog.Level, error)

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

func LevelToString(level slog.Level) string

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

func RedactErr(err error) error

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

func RedactString(s string) string

RedactString strips DSN passwords and bearer tokens from a rendered string.

DHF-REQ: keel/requirement-56

func ResolveGitCommit

func ResolveGitCommit(gitCommit string) string

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

func WithLogger(ctx context.Context, l *slog.Logger) context.Context

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

func New(cfg Config) (*Logger, error)

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

func (l *Logger) BuildIdentity(title, version, gitCommit string)

BuildIdentity emits a startup banner and a structured build identity record.

DHF-REQ: keel/requirement-32

func (*Logger) Close added in v0.2.0

func (l *Logger) Close() error

Close releases file sinks opened by New.

func (*Logger) Debug added in v0.2.0

func (l *Logger) Debug(msg string, args ...any)

Debug emits a DEBUG record.

func (*Logger) DebugContext added in v0.2.0

func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)

DebugContext emits a DEBUG record with ctx.

func (*Logger) Emit added in v0.4.0

func (l *Logger) Emit(event string, attrs ...slog.Attr)

Emit logs a metrics event at Info level.

func (*Logger) Error added in v0.2.0

func (l *Logger) Error(msg string, args ...any)

Error emits an ERROR record.

func (*Logger) ErrorContext added in v0.2.0

func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any)

ErrorContext emits an ERROR record with ctx.

func (*Logger) Event added in v0.2.0

func (l *Logger) Event(verb, msg string, fields ...any)

Event emits an INFO record with event_type set to verb.

func (*Logger) Field added in v0.2.0

func (l *Logger) Field(label string, value any)

Field emits one aligned label/value row.

func (*Logger) Fields added in v0.4.0

func (l *Logger) Fields(rows []FieldRow)

Fields emits aligned label/value rows.

func (*Logger) Header added in v0.2.0

func (l *Logger) Header(title string, version string)

Header emits a ruled banner only, rendered per console mode.

func (*Logger) Info added in v0.2.0

func (l *Logger) Info(msg string, args ...any)

Info emits an INFO record.

func (*Logger) InfoContext added in v0.2.0

func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any)

InfoContext emits an INFO record with ctx.

func (*Logger) JSONLLogPath added in v0.5.0

func (l *Logger) JSONLLogPath() string

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

func (l *Logger) LogBuildIdentity(version, gitCommit string)

LogBuildIdentity logs a single build identity record.

func (*Logger) RunLogLine added in v0.2.0

func (l *Logger) RunLogLine() int

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

func (l *Logger) RunLogPath() string

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

func (l *Logger) Section(name string)

Section emits a ruled section banner, rendered per console mode.

func (*Logger) Slog added in v0.2.0

func (l *Logger) Slog() *slog.Logger

Slog returns the wrapped slog logger for APIs that have not migrated yet.

func (*Logger) StartDailyBuildIdentity added in v0.5.0

func (l *Logger) StartDailyBuildIdentity(ctx context.Context, version, gitCommit string)

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

func (l *Logger) TextLogPath() string

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) Warn added in v0.2.0

func (l *Logger) Warn(msg string, args ...any)

Warn emits a WARN record.

func (*Logger) WarnContext added in v0.2.0

func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any)

WarnContext emits a WARN record with ctx.

func (*Logger) With added in v0.2.0

func (l *Logger) With(args ...any) *Logger

With returns a logger carrying the supplied attrs.

func (*Logger) WithGroup added in v0.2.0

func (l *Logger) WithGroup(name string) *Logger

WithGroup returns a logger that groups subsequent attrs under name.

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.)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL