Documentation
¶
Overview ¶
Package logger provides aiwf's opt-in, default-off diagnostic-log surface: a thin wrapper around log/slog whose configuration comes from three AIWF_LOG* env vars, then aiwf.yaml's logging: block, then a no-op default (ADR-0017).
Index ¶
- func New(cfg Config, w io.Writer) *slog.Logger
- func NewRunID() string
- func OpenDestination(cfg Config, now time.Time, getenv func(string) string) (io.WriteCloser, error)
- func ResolveConfigWithSources(getenv func(string) string, yamlCfg YAMLConfig) (Config, Sources, error)
- func WithVerb(l *slog.Logger, verb, entity, actor, runID string) *slog.Logger
- type Config
- type FieldSource
- type Sources
- type YAMLConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
New returns a *slog.Logger reflecting cfg. When cfg.Enabled is false — the default (ADR-0017 Decision #2) — the returned logger is backed by slog.DiscardHandler, whose Enabled method reports false for every level: emitting a record does no I/O and no allocation beyond the call itself, and w is never touched.
When cfg.Enabled is true, records at or above cfg.Level are written to w in cfg.Format ("text" or "json"). w's destination (the default XDG-state-home file vs. an explicit override) and its concurrent- append safety are resolved by the caller — this constructor only wraps whatever writer it is given.
func NewRunID ¶
func NewRunID() string
NewRunID returns a fresh, per-invocation correlation id: 16 hex characters from 8 cryptographically random bytes. Not an RFC-4122 UUID — nothing here needs that format, only enough entropy that two concurrent aiwf invocations never collide (ADR-0017 Decision #7), so a stdlib-only crypto/rand read is enough without a new dependency.
func OpenDestination ¶
OpenDestination resolves cfg.Destination into an io.WriteCloser: "stderr" opens os.Stderr, an explicit absolute path opens that file for append, and "" (the default) opens the daily $XDG_STATE_HOME/aiwf/logs/aiwf-YYYY-MM-DD.log file (falling back to ~/.local/state/aiwf/logs when XDG_STATE_HOME is unset), creating its directory and sweeping entries older than 30 days only on this call.
Returns (nil, nil) when cfg.Enabled is false: OpenDestination never touches disk for a disabled config, matching ADR-0017's default-off constraint regardless of what a caller passes.
now and getenv are injected so callers (and tests) control the clock and environment — internal/logger sits below the layering tier that may read the ambient wall clock directly (no-time-now-in-core).
func ResolveConfigWithSources ¶
func ResolveConfigWithSources(getenv func(string) string, yamlCfg YAMLConfig) (Config, Sources, error)
ResolveConfigWithSources is ResolveConfig plus the per-field Sources breakdown. See ResolveConfig's doc comment for the precedence rule and error conditions; this is the same resolution, not a second implementation of it.
func WithVerb ¶
WithVerb binds verb/entity/actor/run_id onto l (ADR-0017 Decision #7), scrubbing any macOS (/Users/<name>/) or Linux (/home/<name>/) home-directory fragment from verb/entity/actor first. The scrub operates on string content, not provenance, so it catches a leak regardless of source — including a value assembled from os.Args that a caller passed through verb/entity/actor unfiltered. runID (typically logger.NewRunID()) is bound unscrubbed: it is a random hex id, never operator-supplied text, so it can never carry a home-path fragment.
Types ¶
type Config ¶
type Config struct {
// Enabled is false when neither AIWF_LOG nor aiwf.yaml's
// logging.level was set — the default-off state (ADR-0017
// Decision #2). Level/Format/Destination are the zero value
// when Enabled is false.
Enabled bool
Level slog.Level
// Format is "text" or "json".
Format string
// Destination is "" (use the default XDG-state-home path),
// "stderr", or an absolute path.
Destination string
}
Config is the fully-resolved diagnostic-logging configuration for one aiwf invocation.
func ResolveConfig ¶
func ResolveConfig(getenv func(string) string, yamlCfg YAMLConfig) (Config, error)
ResolveConfig applies ADR-0017's env-beats-yaml-beats-default precedence, independently per setting: AIWF_LOG/AIWF_LOG_FORMAT/ AIWF_LOG_FILE each beat the corresponding aiwf.yaml logging: key, which beats the default. getenv is injected so callers (and tests) control the environment read; yamlCfg is the zero value when aiwf.yaml has no logging: block.
Logging is enabled only when a level is supplied from either source — setting only format or destination without a level never opts in, matching ADR-0017 Decision #2's default-off state.
Returns an error when a supplied level or format value (from either source) is not one of the closed sets ADR-0017 defines.
A thin wrapper over ResolveConfigWithSources for callers (most of them) that only need the merged result, not which tier supplied it.
type FieldSource ¶
type FieldSource string
FieldSource names which precedence tier supplied a resolved field's value.
const ( // SourceEnv means the corresponding AIWF_LOG* env var supplied the value. SourceEnv FieldSource = "env" // SourceYAML means aiwf.yaml's logging: block supplied the value. SourceYAML FieldSource = "yaml" // SourceDefault means neither source was set for this field. SourceDefault FieldSource = "default" )
type Sources ¶
type Sources struct {
Level FieldSource
Format FieldSource
Destination FieldSource
}
Sources reports, per field, which precedence tier supplied the value ResolveConfigWithSources actually used — for an operator- facing surface (`aiwf doctor`) that explains why a given value won, not just what the merged result is. The zero value (empty FieldSource on every field) is what a disabled Config carries: with no level from either source, format/destination are never resolved at all, so labeling them "default" would misleadingly imply they were consulted.
type YAMLConfig ¶
type YAMLConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
Destination string `yaml:"destination"`
}
YAMLConfig is the argument shape ResolveConfig accepts for aiwf.yaml's optional top-level logging: block (ADR-0017 Decision #3). All three fields are optional; a zero value means "absent." internal/config.Logging (config.Logging.ToYAMLConfig()) is what actually decodes a real aiwf.yaml file today — internal/logger can't import internal/config (see internal/config.Logging's doc comment), so this is a separately declared, structurally identical type callers convert into.