Documentation
¶
Overview ¶
Package config loads typed configuration for Go services from one tagged struct, on top of koanf. The struct is the schema: its koanf, alias, env, envref, reload, default, sep, format, example, validate, desc, sensitive and secret tags give each field its key, former names, variables, default, list separator, text format, example, rules, description and secrecy. Loading, validation, redaction, usage tables, starter files and the JSON Schema all read them. Load merges struct-tag defaults, files, environment variables and explicit overrides into a *T, then resolves secrets, normalizes and validates it in one pipeline; Reloader re-runs that pipeline on a file change, signal, poll tick or application notification and publishes a new config only when it passed.
Quick Start ¶
type AppConfig struct {
Port int `koanf:"port" default:"8080" validate:"min=1,max=65535" desc:"HTTP listen port"`
Timeout time.Duration `koanf:"timeout" default:"30s"`
DB struct {
MaxConns int `koanf:"max_conns" default:"25"`
Password string `koanf:"password" validate:"required"`
} `koanf:"db"`
}
cfg, err := config.Load[AppConfig](ctx,
config.WithFile("config.yaml"), // optional
config.WithEnvPrefix("APP"), // APP_PORT, APP_DB_MAX_CONNS, ...
)
Keys and Tags ¶
FieldsChecked describes how a struct becomes keys and reports invalid or ambiguous schemas. Fields returns the same catalog, or nil on failure. MatchFields associates reflected targets with config fields in one traversal, for integrations such as CLI flag bridges. Unmatched targets have an empty Field.Path; ambiguous addresses return ErrKey.
The koanf tag names a segment (",squash" merges into the parent, "-" skips), pointers are followed, and a struct that decodes itself is one key. Every output names a value in one grammar — db.host, items[0].port, named["primary"].port, and items[*].port for any element — and FieldFor maps a key back to its declared field. Defaults, environment names, redacted printing, PrintUsage and the kongprovider flag bridge all use it. The other tags:
- default:"..." — the lowest-precedence value; key=value entries for a map
- sep:";" — the separator that splits a list's strings, or "none"
- format:"json" — a string set for a section, map or list is its JSON
- alias:"old" — former keys, still read, with a warning
- env:"PORT" — more variables, read as written, below the prefixed one
- envref:"true" — decoded strings name variables read by the application
- reload:"live" — inherited reload class; the default is restart
- example:"..." — a sample value for starter files, usage and the schema
- validate:"..." — go-playground rules
- desc:"..." — the description every output shows
- sensitive:"true" — redacted in diagnostics; "false" exempts a field its name would redact
- secret:"<ref>" — resolved by SecretsFrom, and redacted
Invalid sensitive, alias, env, envref, reload, sep, format and example tags, a default on a section, and malformed defaults fail with ErrSchema before sources are read. KeyOf names a field from its address. ParseKey exposes the same canonical grammar as typed segments, including quoted keys and wildcards.
Sources ¶
Highest wins: override providers (WithOverrideProvider, e.g. CLI flags), environment variables (WithEnvPrefix, WithEnvPrefixes), files (WithFile, WithRequiredFile; YAML or JSON by suffix, any parser through WithFileParser), base providers (WithBaseProvider), programmatic defaults (WithDefaults), code defaults (a Defaulter's SetDefaults, for what a tag cannot say: per program, per path), and default tags. Any koanf.Provider loads at either provider layer, with any parser; a ContextProvider is read with the load's context.
With WithEnvPrefix("APP"), APP_DB_MAX_CONNS sets db.max_conns: the name is looked up in the key tree, so an underscore inside a key survives. A map of structs reads APP_FEATURES_<KEY>_<FIELD>, any other map APP_LABELS_<KEY>. A field's env tag adds variables read as written, without the prefix (env:"PORT"); the prefixed variable wins over them. A variable that fits two keys fails the load with ErrValidation naming both; one that fits none is not loaded. WithEnviron replaces os.Environ.
WithEnvPrefixes reads several prefixes as one layer, most specific first: with WithEnvPrefixes("APP_WORKER", "APP"), db.host reads APP_WORKER_DB_HOST, then APP_DB_HOST, then its env tag's names, so services sharing a binary share APP_* and override it under their own prefix. A variable that fits two keys across prefixes fails the load as within one. WithKnownVariables, WithVariableFilter and decoded envref fields claim otherwise unmatched names for unknown-input reports and debug diagnostics.
Keys match fields regardless of letter case, as decoding does; each source's keys are rewritten into the fields' spelling before the sources merge, so precedence holds whatever case a source used. A field's alias tag names former keys rewritten the same way, so a renamed setting keeps reading deployed files; the load logs a warning naming each. An element struct's default tags, and what its SetDefaults sets above them, apply to every map entry and slice element; an entry set to null stays null — write {} for an entry that takes its defaults.
Pipeline ¶
merge → decode, unknown keys refused → secrets (WithSecretPopulator) → post-load (WithPostLoad) → validate (the validator, then a Validatable's Validate method)
Decoding adds string → time.Duration, comma-separated string → slice (a field's sep tag names another separator, or none), key=value entries → map and encoding.TextUnmarshaler to mapstructure's weak typing; WithDecodeHook hooks precede the built-in hooks, and a sep field is split before any hook. A key no field decodes fails the load — a misspelled key cannot leave a service on the default it meant to override — unless a WithUnknownKeys handler accepts it, say after logging a warning; WithAllowUnknownKeys accepts every one. Snapshot.UnknownKeys retains the accepted report with its published config. Clean publications clear it; refused or canceled attempts leave the previous report intact. Handlers, snapshots and results get detached copies. Put derivation and normalization in a post-load hook, not after Load returns: the hook runs inside the load, so a Reloader applies it before deciding whether to swap.
Errors ¶
Every error wraps one category sentinel — ErrValidation, ErrFileRead, ErrFileParse, ErrSource, ErrSecretProvider, ErrPostLoad, and for a reload ErrReload, ErrReloadInProgress, ErrReloaderStopped — together with its cause, so errors.Is and errors.As reach either. The package defines no error codes; map the categories to your own. The errors it writes quote no value meant for a sensitive field: a decode failure on a field that is, or could hold, a sensitive value — a decode hook's failure included — names the field and type only, and a ParseError names the source and line only. A failure on an ordinary field keeps its decoder's message, which may quote the value. Errors your providers, secret populator, post-load hooks and validator return pass through as written.
Hot Reload ¶
reloader, err := config.NewReloader[AppConfig](ctx,
[]config.Option{config.WithFile("config.yaml"), config.WithEnvPrefix("APP")},
config.WithFileWatch(0), // 0 = DefaultFileWatchDebounce
config.WithSignalReload(syscall.SIGHUP),
config.WithApplier(config.Applier[AppConfig]{
Name: "server",
Prefixes: []string{"timeout"},
Apply: func(ctx context.Context, ev config.ApplyEvent[AppConfig]) (config.ApplyResult, error) {
server.SetTimeout(ev.Desired.Timeout) // ev.Changes names what it owes
return config.ApplyResult{}, nil
},
}),
)
if err != nil {
return err
}
if err := reloader.Arm(ctx); err != nil {
return err // a trigger cannot be delivered, e.g. an unwatchable directory
}
go func() {
if err := reloader.Run(ctx); err != nil {
logger.Error("config reloader ended", "error", err)
}
}()
defer func() {
if err := reloader.Stop(shutdownCtx); err != nil {
logger.Warn("config reloader did not stop in time", "error", err)
}
}()
cfg := reloader.Config() // one atomic load
A reload runs the whole pipeline and swaps only on success, returning a ReloadResult: the Snapshot it published, the keys that changed, and the edits a higher-precedence source kept from taking effect — a file's key that a leftover environment variable or a typed flag also sets — each also logged as a warning: a key meant to change live must be set only by sources that change. A failed reload keeps the previous config and is reported to WithOnReloadError, the log and Reloader.Stats. Reloader.Snapshot returns the config, its generation and the reload stats from one moment, and encodes to JSON for a status endpoint. WithApplier registers code that pushes a published config into the running process — a log level, a pool's limits — by key prefix: each is called with the changes it owns, measured from what it last acknowledged, offered again after a failure, and what no applier applies is listed as waiting for a restart. Reloads are serialized: an automatic trigger waits for one in flight and then reads the sources again, while a manual Reloader.Reload returns ErrReloadInProgress. For a source the reloader cannot watch, call Reloader.SourceChanged. NewReloaderFunc gives a caller's own loader the same lifecycle. Published configs are shared by every reader and must be treated as immutable.
File Watch Limits ¶
WithFileWatch detects direct writes, rename-based saves, symlink swaps of the file or of any directory on its path, and directories removed, recreated or replaced further up — including Kubernetes ConfigMap and Secret volume updates. Paths resolve as the kernel resolves them, and a periodic recheck finds what sends no event: an ancestor directory renamed away and recreated, or — on macOS and BSD — a relink whose only trace is a temporary name renamed over the link. It cannot detect sources that are not files (environment variables, providers): use WithPollInterval or SourceChanged. A subPath-mounted ConfigMap never updates inside the container at all; mount the volume without subPath.
Diagnostics ¶
WithReloadTags opts into declared reload classes: restart keys are withheld from appliers and reported in PendingRestart, including beneath a broad prefix. WithReloadTagChecks also verifies live-key ownership before loading. Untagged schemas retain prefix routing. RestartChanges filters declarations for an offline diff; runtime ownership can defer additional changes.
Diff lists every key whose value differs between two configs, comparing decoded values, with sensitive values redacted.
PrintRedacted, RedactedMapChecked, RedactedTreeChecked and PrintEnv show a config with every sensitive value redacted: tagged sensitive:"true", beneath such a field, text-capable and named like a credential (unless tagged sensitive:"false"), or under such a map key. Numeric counters, booleans and durations keep ordinary diagnostics. The checked map/tree helpers return an error on an invalid structural schema and never return partial results. They inspect live values without decoding default or example tags. PrintUsage lists every key an operator can set with its environment variable, type, default, reload class and description; a default that is, or could hold, a sensitive value is withheld (Field.PrintableDefault), as is such an example tag's (Field.PrintableExample). PrintTemplate writes the config file a struct describes: every key with its default, which loads back as the defaults, and a key with none commented out, showing its example. LoadSnapshot, or a reloader WithProvenance, records where each value came from — the default, file, environment variable, provider or flag the merge kept, and the stages that changed it after decoding — and PrintExplain prints it, values redacted. Snapshot.Shadowed lists what the merge did not keep: every input a higher-precedence source outranks; WithShadowedInputs judges them at every load. JSONSchema describes the config file for editors and linters — advisory, Load being the authority. Usage exposes the same safe catalog as values for custom documentation; PrintUsage also supports FormatMarkdown. Tree exports raw resolved values, including secrets, and TreeFunc lets callers apply a leaf policy with the declared Field metadata. Both return detached trees. Text-marshal errors propagate; opaque values with no scalar representation fail, and cycles use [CYCLE] rather than promising a round trip.
Example ¶
Example mirrors the README Quick Start: struct-tag defaults, a file and APP_* environment variables, then the result printed with the password redacted. The example supplies its own file and environment, so its output does not depend on where it runs.
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/ashtonian/pkg/config"
)
// tempFile writes content to a file in a new temporary directory and
// returns its path and a cleanup function.
func tempFile(name, content string) (string, func()) {
dir, err := os.MkdirTemp("", "config-example")
if err != nil {
log.Fatal(err)
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
log.Fatal(err)
}
return path, func() { _ = os.RemoveAll(dir) }
}
func main() {
type DBConfig struct {
Host string `koanf:"host" default:"localhost"`
MaxConns int `koanf:"max_conns" default:"25" validate:"min=1"`
Password string `koanf:"password" default:"hunter2"`
}
type AppConfig struct {
Port int `koanf:"port" default:"8080" validate:"min=1,max=65535"`
Timeout time.Duration `koanf:"timeout" default:"30s"`
DB DBConfig `koanf:"db"`
}
file, cleanup := tempFile("config.yaml", "db:\n host: db.internal\n")
defer cleanup()
cfg, err := config.Load[AppConfig](context.Background(),
config.WithFile(file), // optional: skipped when absent
config.WithEnvPrefix("APP"), // APP_PORT, APP_DB_MAX_CONNS, ...
config.WithEnviron(func() []string { return []string{"APP_DB_MAX_CONNS=50"} }),
)
if err != nil {
fmt.Println("load:", err)
return
}
if err := config.PrintRedacted(cfg, config.WithWriter(os.Stdout)); err != nil {
fmt.Println("print:", err)
}
}
Output: FIELD VALUE TYPE ------------------------------------------- port 8080 int timeout 30s time.Duration db.host db.internal string db.max_conns 50 int db.password [REDACTED] string
Example (ReloadEndpoint) ¶
Example_reloadEndpoint mounts a manual reload on an HTTP mux. A refused reload answers with its error, a reload already running answers 409, and a published one answers with its generation and what changed, a credential's values redacted. Put authentication in front of it.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"github.com/ashtonian/pkg/config"
)
func main() {
type AppConfig struct {
Port int `koanf:"port" default:"8080"`
APIKey string `koanf:"api_key"`
}
ctx := context.Background()
settings := map[string]any{"api_key": "k-1"} // stands in for a settings table
source, err := config.NewMapProvider(func(context.Context) (map[string]any, error) {
return settings, nil
}, ".")
if err != nil {
fmt.Println(err)
return
}
reloader, err := config.NewReloader[AppConfig](ctx, []config.Option{config.WithBaseProvider(source, nil)})
if err != nil {
fmt.Println("load:", err)
return
}
defer func() { _ = reloader.Stop(ctx) }()
mux := http.NewServeMux()
mux.HandleFunc("POST /admin/config/reload", func(w http.ResponseWriter, r *http.Request) {
result, err := reloader.Reload(r.Context())
switch {
case errors.Is(err, config.ErrReloadInProgress):
http.Error(w, err.Error(), http.StatusConflict)
return
case errors.Is(err, config.ErrReloaderStopped):
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
case err != nil:
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Generation uint64 `json:"generation"`
Changes []config.Change `json:"changes"`
}{result.Snapshot.Generation, result.Changes})
})
settings = map[string]any{"port": 9090, "api_key": "k-2"}
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/config/reload", nil))
fmt.Print(rec.Code, " ", rec.Body.String())
}
Output: 200 {"generation":2,"changes":[{"key":"port","old":{"present":true,"type":"int","text":"8080"},"new":{"present":true,"type":"int","text":"9090"}},{"key":"api_key","old":{"present":true,"type":"string","text":"[REDACTED]","redacted":true},"new":{"present":true,"type":"string","text":"[REDACTED]","redacted":true}}]}
Index ¶
- Constants
- Variables
- func CheckKey[T any](ctx context.Context, key string, value any, opts ...Option) error
- func Decode[T any](ctx context.Context, data map[string]any, opts ...Option) (*T, error)
- func DecodeKey[T any](ctx context.Context, key string, value any, opts ...Option) (*T, error)
- func DefaultsFromTags[T any]() map[string]any
- func FieldName(f reflect.StructField) string
- func JSONSchema[T any](envPrefixes ...string) (json.RawMessage, error)
- func KeyOf[T, F any](cfg *T, field *F) (string, error)
- func KeyOfValue[T any](cfg *T, field reflect.Value) (string, error)
- func Load[T any](ctx context.Context, opts ...Option) (*T, error)
- func PrintEnv(cfg any, envPrefix string, opts ...PrintOption) error
- func PrintExplain[T any](s Snapshot[T], opts ...PrintOption) error
- func PrintRedacted(cfg any, opts ...PrintOption) error
- func PrintTemplate[T any](envPrefix string, opts ...PrintOption) error
- func PrintUsage[T any](envPrefix string, opts ...PrintOption) error
- func RedactText(text string) string
- func RedactedMap(cfg any) map[string]string
- func RedactedMapChecked(cfg any) (map[string]string, error)
- func RedactedTree(cfg any) map[string]any
- func RedactedTreeChecked(cfg any) (map[string]any, error)
- func Tree(cfg any) (map[string]any, error)
- func TreeFunc(cfg any, leaf func(key string, f Field, v any) any) (map[string]any, error)
- type Applier
- type ApplierFailure
- type ApplierStatus
- type ApplyError
- type ApplyEvent
- type ApplyOutcome
- type ApplyResult
- type Change
- type ContextProvider
- type Defaulter
- type Field
- type Key
- type KeyError
- type Option
- func WithAllowUnknownKeys() Option
- func WithBaseProvider(p koanf.Provider, parser koanf.Parser, opts ...SourceOption) Option
- func WithDecodeHook(hook mapstructure.DecodeHookFunc) Option
- func WithDefaults(defaults map[string]any) Option
- func WithEnvPrefix(prefix string) Option
- func WithEnvPrefixes(prefixes ...string) Option
- func WithEnviron(environ func() []string) Option
- func WithFile(path string) Option
- func WithFileParser(suffix string, p koanf.Parser) Option
- func WithFiles(paths ...string) Option
- func WithKnownVariables(names ...string) Option
- func WithLogger(logger *slog.Logger) Option
- func WithOverrideProvider(p koanf.Provider, parser koanf.Parser, opts ...SourceOption) Option
- func WithPostLoad[T any](fn func(ctx context.Context, cfg *T) error) Option
- func WithRequiredFile(path string) Option
- func WithRequiredFiles(paths ...string) Option
- func WithSecretPopulator(p SecretPopulator) Option
- func WithShadowedInputs(handle func(ctx context.Context, shadowed *ShadowedInputsError) error) Option
- func WithSkipValidation() Option
- func WithUnknownKeys(handle func(ctx context.Context, unknown *UnknownKeysError) error) Option
- func WithValidator(v Validator) Option
- func WithVariableFilter(claimed func(name string) bool) Option
- type Origin
- type OriginProvider
- type ParseError
- type PrintOption
- type Problem
- type ReloadEvent
- type ReloadOption
- func WithApplier[T any](a Applier[T]) ReloadOption
- func WithFileWatch(debounce time.Duration) ReloadOption
- func WithOnReloadError(fn func(trigger string, err error)) ReloadOption
- func WithOnReloadSuccess[T any](fn func(ctx context.Context, ev ReloadEvent[T])) ReloadOption
- func WithPollInterval(interval time.Duration) ReloadOption
- func WithProvenance() ReloadOption
- func WithReloadLogger(logger *slog.Logger) ReloadOption
- func WithReloadTagChecks() ReloadOption
- func WithReloadTags() ReloadOption
- func WithSignalReload(signals ...os.Signal) ReloadOption
- func WithWatchedFiles(paths ...string) ReloadOption
- type ReloadResult
- type ReloadStats
- type Reloader
- func (r *Reloader[T]) Arm(ctx context.Context) error
- func (r *Reloader[T]) Config() *T
- func (r *Reloader[T]) Reload(ctx context.Context) (*ReloadResult[T], error)
- func (r *Reloader[T]) Run(ctx context.Context) error
- func (r *Reloader[T]) Snapshot() Snapshot[T]
- func (r *Reloader[T]) SourceChanged()
- func (r *Reloader[T]) Stats() ReloadStats
- func (r *Reloader[T]) Stop(ctx context.Context) error
- type SecretPopulator
- type Segment
- type SegmentKind
- type Shadow
- type ShadowedInputsError
- type Snapshot
- type Source
- type SourceKind
- type SourceOption
- type UnknownKey
- type UnknownKeysError
- type UsageBounds
- type UsageEntry
- type Validatable
- type Validator
- type Value
Examples ¶
Constants ¶
const ( FormatTable = "table" FormatJSON = "json" FormatYAML = "yaml" FormatEnv = "env" FormatMarkdown = "markdown" )
Print formats accepted by WithFormat.
const ( // ReloadRestart is the default reload class: a process restart is required. ReloadRestart = "restart" // ReloadLive declares that an applier applies changes in place. ReloadLive = "live" )
const ( TriggerManual = "manual" // an explicit Reload call TriggerFileChange = "file_change" // WithFileWatch saw a watched file or its symlink target change TriggerPoll = "poll" // WithPollInterval ticked TriggerSignal = "signal" // WithSignalReload received one of its signals TriggerSourceChanged = "source_changed" // the caller reported a change via SourceChanged )
Reload trigger names. These form a closed vocabulary so consumers can use them directly as a bounded metric label.
const ( KindApply = "apply" // ErrApply KindValidation = "validation" // ErrValidation KindFileRead = "file_read" // ErrFileRead KindFileParse = "file_parse" // ErrFileParse KindSource = "source" // ErrSource KindSecretProvider = "secret_provider" // ErrSecretProvider KindPostLoad = "post_load" // ErrPostLoad KindSchema = "schema" // ErrSchema KindUnknown = "unknown" // none of them )
Problem kinds: the category sentinel an error wraps.
const DefaultFileWatchDebounce = 500 * time.Millisecond
DefaultFileWatchDebounce is the WithFileWatch debounce window applied when the option's argument is zero or negative. 500ms is large enough to coalesce editor rename+write sequences yet small enough that operators see fresh config within one human reaction time.
const JSONSchemaDraft = "https://json-schema.org/draft/2020-12/schema"
JSONSchemaDraft is the dialect JSONSchema writes.
const TriggerInitial = "initial"
TriggerInitial is the Trigger of the apply an Applier with ApplyInitial receives once, after the first load. It never names a reload.
Variables ¶
var ( // ErrValidation reports invalid configuration: a validator rejected the // loaded struct, a value could not decode into its field's type, an // environment variable named more than one key, a Validate method // failed, or a source set a key no field decodes (unless a // WithUnknownKeys handler accepts it). ErrValidation = errors.New("configuration validation failed") // ErrFileRead reports a config file that could not be read: a required // file that is missing, or any file that exists but cannot be opened. ErrFileRead = errors.New("failed to read configuration file") // ErrFileParse reports a config file its parser (chosen by the file's // suffix; see WithFileParser) rejects, as distinct from one that could // not be read. ErrFileParse = errors.New("configuration file could not be parsed") // ErrSecretProvider reports a failure of the WithSecretPopulator function. ErrSecretProvider = errors.New("secret provider error") // ErrPostLoad reports a WithPostLoad hook that returned an error. ErrPostLoad = errors.New("configuration post-load hook failed") // ErrSource reports a source that failed to load — the defaults, the // environment, or a base or override provider — as opposed to a file // (ErrFileRead, ErrFileParse) or the secret populator (ErrSecretProvider). ErrSource = errors.New("configuration source failed to load") // ErrReload wraps every reload that ran the pipeline and failed; the // previous config stays live. The failure's own category is wrapped too. ErrReload = errors.New("configuration reload failed") // ErrApply reports a published config some [Applier] did not apply: the // config is live, and the appliers that failed are called again on the // next one. It does NOT wrap ErrReload, which means nothing was // published. An [ApplyError] carries it. ErrApply = errors.New("configuration published but not fully applied") // ErrReloadInProgress reports a manual Reload that found another reload // in flight. No pipeline ran and nothing failed, so it does NOT wrap // ErrReload: a burst of operator requests must not read as failures. ErrReloadInProgress = errors.New("configuration reload already in progress") // ErrReloaderStopped reports a call after Stop. It does NOT wrap ErrReload. ErrReloaderStopped = errors.New("configuration reloader is stopped") // ErrSchema reports a config type the package cannot address: a struct // key holding a character keys use for structure (".", "[", "]", "*", a // backslash, a quote or a control character), two fields with one key, a // type that is not a struct where one is required, or a value Diff cannot // compare (a non-nil func, channel or unsafe pointer). ErrSchema = errors.New("invalid configuration schema") // ErrKey reports a configuration key that does not parse, or that names // no field of the config type. ErrKey = errors.New("invalid configuration key") )
Category sentinels. Every error Load or a Reloader returns wraps exactly one of them together with its cause, so errors.Is tells the categories apart and errors.As reaches the cause. Codes, HTTP statuses and retry classes are the caller's to assign: match a sentinel and map it to your own error vocabulary.
var ErrSecretNotFound = errors.New("secret not found")
ErrSecretNotFound is what a SecretsFrom fetch function wraps when the store has no secret under the ref, so an optional secret can be left unset.
Functions ¶
func CheckKey ¶ added in v0.8.0
CheckKey checks one concrete key with all other fields at their declared and WithDefaults defaults. It decodes with Load's hooks and weak typing, then runs its validator and T.Validate. Unknown keys return a *KeyError matching ErrKey; invalid values match ErrValidation. Keys use ParseKey's grammar, accepting field aliases and case folding. A list index must exist in the defaults or append immediately after them; pass a whole list to create several new elements. Wildcards are not concrete keys.
Only WithDefaults, WithDecodeHook, WithValidator and WithSkipValidation affect this check. It reads no provider, file or environment and runs no secret populator, post-load or unknown-input handler. Rules relating fields judge the candidate against defaults; the final Load still validates the combined sources. No check can establish that arbitrary combinations of individually valid entries are valid together.
func Decode ¶ added in v0.4.0
Decode builds a T from data — nested maps keyed by field key, as a parsed document or a plugin's settings block holds them — through the pipeline Load runs: tag defaults beneath it, aliases and separators, strict keys, decode hooks, secrets, post-load hooks and validation. data loads as a base provider and is copied; opts add to the load as they add to Load's, and a source they add loads above data.
type Plugin struct {
Name string `koanf:"name" validate:"required"`
Timeout time.Duration `koanf:"timeout" default:"5s"`
}
p, err := config.Decode[Plugin](ctx, map[string]any{"name": "audit"})
func DecodeKey ¶ added in v0.8.0
DecodeKey returns the configuration CheckKey validates: one key set over defaults, with no source reads or post-load stages. On any error it returns nil. The returned configuration and its collections are caller-owned.
func DefaultsFromTags ¶
DefaultsFromTags returns T's `default:"value"` struct tags keyed by koanf dot-path, ready for WithDefaults or a confmap provider: each the tag's text (split into a list when sep is set), but a map field's key=value entries as a map, so a source that sets one entry merges with the others. Load reads them as its lowest-precedence source, and applies a map's or slice's element defaults to each entry the sources create; koanf and mapstructure do not read the tag themselves.
type ServerConfig struct {
Host string `koanf:"host" default:"localhost"`
Port int `koanf:"port" default:"8080"`
}
type AppConfig struct {
Server ServerConfig `koanf:"server"`
Debug bool `koanf:"debug" default:"false"`
}
config.DefaultsFromTags[AppConfig]()
// {"server.host": "localhost", "server.port": "8080", "debug": "false"}
func FieldName ¶
func FieldName(f reflect.StructField) string
FieldName returns the config key segment struct field f is known by: its koanf tag name, or its lower-cased Go name when the tag names none — the rule Fields applies. Register it on a go-playground validator (RegisterTagNameFunc) so validation errors read db.max_conns, the key the operator wrote, rather than DB.MaxConns. A field tagged koanf:"-" keeps its Go name, because "-" tells the validator to skip the field.
func JSONSchema ¶
func JSONSchema[T any](envPrefixes ...string) (json.RawMessage, error)
JSONSchema returns a JSON Schema (Draft 2020-12) for a config file of T, for editors and linters: the keys and their types, descriptions from `desc` tags, defaults diagnostics may print, and the `validate` rules JSON Schema can state. It is advisory: Load is the one authority, and the schema neither knows what other sources supply nor every rule.
- It describes one file. A `validate:"required"` key is not required in it — a default, another file or the environment may supply it — and is listed in the key's x-config-validation instead.
- Keys are spelled as the fields' koanf keys; Load also accepts them in any letter case, and a value of another scalar type it can decode weakly ("8080" for a number), neither of which the schema allows.
- Translated rules: min, max, len, gt, gte, lt, lte and eq on numbers, string lengths and collection sizes; oneof as an enum on strings and integers, read as the validator reads it, and eq on a string as a const. Every rule's name, translated or not, is listed in x-config-validation; an operand appears only where a rule is translated.
- A sensitive key is writeOnly. It, and any key that could hold a sensitive value, has no default, enum or const — an operand can itself be a credential — though its size bounds remain. Numeric value bounds on sensitive fields are withheld too.
- Rules after omitempty apply to nonzero values. The zero alternative follows the declared type, preserving nil versus non-nil empty values; earlier rules still apply. dive and keys stop translation.
- A duration is a string such as "30s". Its x-config-duration-bounds annotation describes runtime bounds in integer nanoseconds; JSON Schema validators do not enforce that annotation. A type that decodes itself is described by its Go type in x-config-type, unconstrained, or as a string when it decodes from text.
- A struct a type holds within itself is a $defs entry, referenced by a name made of its Go type's: letters, digits, '.', '_' and '-'.
- With envPrefixes, as WithEnvPrefixes takes them, a key a variable sets lists in x-config-env every variable that sets it, in precedence order: under each prefix, then the names its env tag lists. A map's entries are named with a <KEY> placeholder. A $defs entry, shared by every path that reaches it, names none.
The output is deterministic: properties sort by key. T that is not a struct, or whose keys cannot be told apart, is an ErrSchema error.
func KeyOf ¶
KeyOf returns the key of the field that field points at inside cfg:
key, err := config.KeyOf(&cfg, &cfg.DB.MaxConns) // "db.max_conns"
It reads addresses, never values, so the field's contents do not matter, and never creates pointer targets: a field behind a nil pointer has no address in cfg and is an error. A struct, map or list field has a key of its own; an element of a map or list does not. A pointer to cfg itself, to memory outside it, to a field that is not a key (unexported, koanf:"-", koanf:",remain", a squashed struct) or to an address two fields of one type share (zero-sized fields) is an ErrKey error. Field must point into cfg — not into a copy of it.
func KeyOfValue ¶
KeyOfValue is KeyOf for a field known by reflection: field is the field itself, addressable, as reflect.Value.Field or a flag target gives it.
func Load ¶
Load builds a configuration of type T: it merges the sources in precedence order, decodes them into a new T — failing on a key no field decodes, unless WithUnknownKeys decides otherwise — then resolves secrets, runs the post-load hooks, validates with the validator and, when *T is Validatable, with its Validate method.
Source precedence, highest first:
- Override providers (WithOverrideProvider, including kongprovider's CLI flags)
- Environment variables (WithEnvPrefix, WithEnvPrefixes)
- Config files (WithFile, WithRequiredFile), a later file winning
- Base providers (WithBaseProvider)
- Programmatic defaults (WithDefaults)
- Code defaults (Defaulter)
- Struct-tag defaults (default:"value")
ctx reaches the secret populator, every post-load hook, a context-aware validator and every ContextProvider.
Keys match fields regardless of letter case, as decoding does. Each source's keys are rewritten into the fields' own spelling before the sources merge, so a higher source wins whatever case it used; a source that spells one field two ways fails with ErrValidation. An element struct's `default` tags apply to every map entry and slice element the sources create, below what they set.
Every error wraps one category sentinel (ErrFileRead, ErrFileParse, ErrSource, ErrSecretProvider, ErrPostLoad, ErrValidation) and its cause, so errors.Is and errors.As reach either. The errors this package writes quote no value meant for a sensitive field: a decode failure on a field that is, or could hold, a sensitive value — a decode hook's failure included — names the field and type only, and a parse failure names the source and line only (see ParseError). A failure on an ordinary field keeps its decoder's message, which may quote the value (time.Parse does). Errors returned by the caller's own providers, secret populator, post-load hooks, validator and Validate method are passed through as they were written.
Example (Errors) ¶
ExampleLoad_errors shows the categories a failed load reports, and that a value meant for a sensitive field is never quoted.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/ashtonian/pkg/config"
)
// tempFile writes content to a file in a new temporary directory and
// returns its path and a cleanup function.
func tempFile(name, content string) (string, func()) {
dir, err := os.MkdirTemp("", "config-example")
if err != nil {
log.Fatal(err)
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
log.Fatal(err)
}
return path, func() { _ = os.RemoveAll(dir) }
}
func main() {
type AppConfig struct {
Port int `koanf:"port" validate:"min=1"`
Rotation time.Time `koanf:"rotation" sensitive:"true"`
}
ctx := context.Background()
_, err := config.Load[AppConfig](ctx, config.WithDefaults(map[string]any{"port": 0}))
fmt.Println(errors.Is(err, config.ErrValidation), err)
_, err = config.Load[AppConfig](ctx, config.WithRequiredFile("/nonexistent/config.yaml"))
fmt.Println(errors.Is(err, config.ErrFileRead), errors.Is(err, os.ErrNotExist))
_, err = config.Load[AppConfig](ctx, config.WithEnvPrefix("APP"),
config.WithEnviron(func() []string { return []string{"APP_ROTATION=s3cr3t-not-a-time"} }))
fmt.Println(errors.Is(err, config.ErrValidation), err)
file, cleanup := tempFile("config.yaml", "port: 8080\nrotation: [s3cr3t\n")
defer cleanup()
_, err = config.Load[AppConfig](ctx, config.WithFile(file))
var pe *config.ParseError
fmt.Println(errors.Is(err, config.ErrFileParse), errors.As(err, &pe), pe.Line > 0)
}
Output: true validating config: configuration validation failed: port(min=1) true true true unmarshaling config: configuration validation failed: decoding failed due to the following error(s): 'rotation' cannot be decoded into time.Time (the value is sensitive and not shown) true true true
Example (Layers) ¶
ExampleLoad_layers shows every source at its layer: a struct-tag default, a programmatic default, a file, an environment variable and an override, each winning over the ones below it.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/ashtonian/pkg/config"
)
// tempFile writes content to a file in a new temporary directory and
// returns its path and a cleanup function.
func tempFile(name, content string) (string, func()) {
dir, err := os.MkdirTemp("", "config-example")
if err != nil {
log.Fatal(err)
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
log.Fatal(err)
}
return path, func() { _ = os.RemoveAll(dir) }
}
func main() {
type AppConfig struct {
Region string `koanf:"region" default:"us-east-1"`
LogLevel string `koanf:"log_level" default:"info"`
Replicas int `koanf:"replicas" default:"1"`
Tags []string `koanf:"tags"`
Debug bool `koanf:"debug"`
}
file, cleanup := tempFile("config.yaml", "replicas: 3\ntags: [web, api]\n")
defer cleanup()
cfg, err := config.Load[AppConfig](context.Background(),
config.WithDefaults(map[string]any{"log_level": "warn"}),
config.WithFile(file),
config.WithEnvPrefix("APP"),
config.WithEnviron(func() []string { return []string{"APP_REPLICAS=5", "APP_TAGS=web,api,grpc"} }),
config.WithOverrideProvider(overrides{"debug": true}, nil),
)
if err != nil {
fmt.Println("load:", err)
return
}
fmt.Printf("region=%s log_level=%s replicas=%d tags=%v debug=%v\n",
cfg.Region, cfg.LogLevel, cfg.Replicas, cfg.Tags, cfg.Debug)
}
// overrides is a koanf.Provider over a fixed map — the shape kongprovider
// gives CLI flags.
type overrides map[string]any
func (o overrides) Read() (map[string]any, error) { return o, nil }
func (overrides) ReadBytes() ([]byte, error) { return nil, errors.New("overrides: use Read") }
Output: region=us-east-1 log_level=warn replicas=5 tags=[web api grpc] debug=true
func PrintEnv ¶
func PrintEnv(cfg any, envPrefix string, opts ...PrintOption) error
PrintEnv writes cfg as the environment variables that would load it under envPrefix — `export APP_DB_HOST='db.internal'` — one per key, named as the loader reads them: a list of scalars joined with its sep (commas by default), a map entry by its key. A key no variable sets alone is written as a comment saying so: a list element of a struct, a map key the variable naming cannot spell, a list its separator cannot represent, a name another key would also read — under envPrefix or, with WithFallbackEnvPrefixes, under any prefix the load reads. A sensitive value is never exported; its line is commented out with no value. WithWriter and WithFallbackEnvPrefixes apply; WithFormat does not.
func PrintExplain ¶
func PrintExplain[T any](s Snapshot[T], opts ...PrintOption) error
PrintExplain writes where each value of a snapshot's config came from: the key, the value as PrintRedacted shows it — sensitive values redacted — the input source the merge kept, and the stages that changed it after decoding. A snapshot recorded by LoadSnapshot or a reloader WithProvenance has provenance; any other is an error.
WithFormat selects FormatTable (the default) or FormatJSON (an array of rows); keys come in declaration order, or by name with WithSortByName. It returns the first write error, or an error for any other format.
Example ¶
ExamplePrintExplain shows where each value came from: a tag default, an environment variable by name, and a post-load hook that changed a value after decoding. The password's value stays redacted; its source does not.
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/ashtonian/pkg/config"
)
func main() {
type DB struct {
Host string `koanf:"host" default:"localhost"`
Password string `koanf:"password"`
}
type AppConfig struct {
Port int `koanf:"port" default:"8080"`
LogLevel string `koanf:"log_level" default:"info"`
DB DB `koanf:"db"`
}
snap, err := config.LoadSnapshot[AppConfig](context.Background(),
config.WithEnvPrefix("APP"),
config.WithEnviron(func() []string { return []string{"APP_PORT=9000", "APP_DB_PASSWORD=s3cret"} }),
config.WithPostLoad(func(_ context.Context, c *AppConfig) error {
c.LogLevel = strings.ToUpper(c.LogLevel)
return nil
}),
)
if err != nil {
fmt.Println(err)
return
}
if err := config.PrintExplain(snap, config.WithWriter(os.Stdout)); err != nil {
fmt.Println(err)
}
}
Output: KEY VALUE SOURCE --------------------------------------------------- port 9000 environment APP_PORT log_level INFO tag_default → post_load 0 db.host localhost tag_default db.password [REDACTED] environment APP_DB_PASSWORD
func PrintRedacted ¶
func PrintRedacted(cfg any, opts ...PrintOption) error
PrintRedacted writes a struct's fields, keyed by koanf path, redacting sensitive values. A value is sensitive when its field, or any struct, slice or map field containing it, is tagged sensitive:"true", or when it can hold text and a name it sits under matches a sensitive pattern (password, secret, token, key, ...): the leaf field's name, the name of any map or slice containing it, or any map key above it. A struct field's name does not cover the struct it holds — an Auth block's fields are judged by their own names — but a map key's covers everything beneath it. Maps and slices, including dynamic ones held in interfaces and nested to any depth, are expanded element by element, so no container is printed whole while something inside it is unchecked; a sensitive map, list or interface is one redacted value, since the keys it holds may themselves be secrets; a value printed whole — a struct decoded from text or by its own decoder, or a slice of them — is redacted when its type could hold a sensitive value. No option prints a sensitive value.
WithFormat selects FormatTable (the default: one row per key), FormatJSON (an object of key → value text) or FormatYAML (the config nested as it is written in a file, values typed as they decode, keys in declaration order); PrintEnv writes it as environment variables. It returns the first write error, a structural ErrSchema error, or an error for an unknown format. Structural errors produce no output. It never decodes default/example tags.
func PrintTemplate ¶
func PrintTemplate[T any](envPrefix string, opts ...PrintOption) error
PrintTemplate writes a config file for T, built from its struct: the file an application ships, or an operator starts from. Every key appears in declaration order with its default as the value decoding produces — a duration as 30s, a list as a list — preceded by its `desc` and a line naming its type, whether it is required or sensitive, and the environment variables that set it under envPrefix and the WithFallbackEnvPrefixes prefixes, in precedence order (none when there is no prefix). A key with no default is commented out with its printable example, if any; a sensitive key and a default Field.PrintableDefault withholds have no value. A map or list with no default is commented out with one example entry built from its element's defaults. A struct with no key to set is commented out whole, since an empty one would load as null over its defaults.
Loading the file gives the same config as loading no source at all, and strict keys accept it. WithFormat selects FormatYAML (the default) or FormatJSON, which has no comments and leaves the commented keys out; WithWriter and WithFallbackEnvPrefixes apply. Tag and Defaulter defaults are decoded as Load decodes them without WithDecodeHook: a default only a custom hook can decode is an ErrValidation error.
Example ¶
ExamplePrintTemplate writes the config file a struct describes: every key with its default, its description and the variable that sets it; a key with no default, and a secret, are commented out with no value.
package main
import (
"fmt"
"os"
"time"
"github.com/ashtonian/pkg/config"
)
func main() {
type DBConfig struct {
Host string `koanf:"host" default:"localhost" desc:"Database host"`
Password string `koanf:"password" validate:"required"`
}
type AppConfig struct {
Port int `koanf:"port" default:"8080" desc:"HTTP listen port"`
Timeout time.Duration `koanf:"timeout" default:"30s"`
Tags []string `koanf:"tags" default:"web,api"`
DB DBConfig `koanf:"db"`
}
if err := config.PrintTemplate[AppConfig]("APP", config.WithWriter(os.Stdout)); err != nil {
fmt.Println(err)
}
}
Output: # Configuration for config_test.AppConfig: every key with its default. A commented key has # no default, or one that is not shown; uncomment it to set it. # HTTP listen port # int; env APP_PORT port: 8080 # time.Duration; env APP_TIMEOUT timeout: 30s # []string; env APP_TAGS tags: - web - api db: # Database host # string; env APP_DB_HOST host: localhost # string, required, sensitive; env APP_DB_PASSWORD # password:
func PrintUsage ¶
func PrintUsage[T any](envPrefix string, opts ...PrintOption) error
PrintUsage writes every key of T an operator can set: its path, the environment variables that set it under envPrefix and the WithFallbackEnvPrefixes prefixes in precedence order (none when there is no prefix), its type, its `default` tag, whether `validate:"required"` makes it mandatory, and its `desc` tag. A map or list one variable can set whole, or one with a default, has a row of its own. A map's entries are one row per element field, keyed by the pattern features[*].limit with a <KEY> placeholder in the variable, and a slice of structs' elements one row per element field, items[*].port, with no variable. Tables mark withheld defaults as [REDACTED]; JSON omits default and sets withheld (see Field.PrintableDefault).
WithFormat selects a table (the default), JSON (an array of objects), FormatMarkdown (an escaped Markdown table), or FormatEnv: a template of `export NAME='default'` lines under the first prefix, with a key that has no default, a sensitive key and a map entry written as a commented-out line; FormatEnv needs an envPrefix. WithWriter, WithSortByName and WithFallbackEnvPrefixes apply; descriptions are always included.
Example ¶
ExamplePrintUsage prints what an operator can set, with the environment variable for each key; a sensitive default stays hidden.
package main
import (
"fmt"
"os"
"time"
"github.com/ashtonian/pkg/config"
)
func main() {
type AppConfig struct {
Port int `koanf:"port" default:"8080" desc:"HTTP listen port"`
Timeout time.Duration `koanf:"timeout" default:"30s" desc:"Request timeout"`
Name string `koanf:"name" validate:"required" desc:"Service name"`
Password string `koanf:"password" default:"hunter2"`
}
if err := config.PrintUsage[AppConfig]("APP", config.WithWriter(os.Stdout)); err != nil {
fmt.Println(err)
}
}
Output: KEY ENV TYPE DEFAULT RELOAD DESCRIPTION port APP_PORT int 8080 restart HTTP listen port timeout APP_TIMEOUT time.Duration 30s restart Request timeout name APP_NAME string (required) restart Service name password APP_PASSWORD string [REDACTED] restart
func RedactText ¶ added in v0.8.0
RedactText removes recognizable credentials from text: URL userinfo, PEM blocks, credential key/value pairs, Basic/Bearer credentials and JWTs. URLs keep their username, host and path when a password is present. Other text is retained. This is a diagnostic filter, not a secret detector: keep sensitive tags on fields whose values need protection regardless of shape.
func RedactedMap ¶
RedactedMap returns a config's leaf values as text keyed by path, every sensitive value (as PrintRedacted defines it) replaced by "[REDACTED]". It is for diagnostics — a status endpoint, a reload response, a log line — not serialization: every value is its fmt %v text, and maps and slices are expanded entry by entry. It returns nil on a structural schema error; use RedactedMapChecked to receive that error.
func RedactedMapChecked ¶ added in v0.5.0
RedactedMapChecked returns RedactedMap's values, or nil and ErrSchema when a reached type has invalid or ambiguous keys. No partial map is returned. Like PrintRedacted, it does not decode default or example tags.
func RedactedTree ¶
RedactedTree returns cfg's keys and values as a nested tree — maps keyed as the config's keys are, lists, and scalars of the types the config decodes from (a duration as "30s", a time as its text) — with every sensitive value replaced by "[REDACTED]", as PrintRedacted judges it; a sensitive map, list or interface is replaced whole, its keys included. Marshal it with any koanf parser to show a loaded config as YAML, JSON, TOML or dotenv. Loading supported finite values back gives the same config, apart from redaction; Tree describes the scalar and cycle limits. It returns nil on error; use RedactedTreeChecked to receive the error.
func RedactedTreeChecked ¶ added in v0.5.0
RedactedTreeChecked returns RedactedTree's values, or nil and an error when a reached type has invalid keys or no scalar representation, or its text marshaler fails. No partial tree is returned. It does not decode default or example tags.
func Tree ¶ added in v0.7.0
Tree returns cfg as nested maps, lists and decoder-compatible scalars, including sensitive values. The result is detached from cfg. Cycles are represented as [CYCLE], as in RedactedTree, and cannot round-trip. Opaque types without a scalar/text representation return ErrSchema; MarshalText errors are returned with the key. Custom decoders must accept that text.
func TreeFunc ¶ added in v0.7.0
TreeFunc projects cfg into a tree, replacing each leaf with leaf's result. The key uses the package's grammar; Field carries the declared metadata. Sensitive maps, lists and interfaces are passed once, whole, including their keys. Empty or nil containers are leaves. A nil callback is identity. The callback receives detached tree values and may change them freely.
Types ¶
type Applier ¶
type Applier[T any] struct { // Name identifies the applier in stats, errors and logs; unique within a // reloader. Name string // Prefixes are the keys the applier owns, in the package's key grammar: // a section (db), one value (logging.level), an entry // (features["audit"]) or a pattern (features[*].gate). It owns every key // at or beneath them unless another applier's prefix is more specific: // longer, or as long with more literal map keys and indexes. Prefixes []string // ApplyInitial applies the first load once, before the constructor // returns: every key the applier owns, as a change from nothing. Its // error, or any restart key, fails construction with [ErrApply]. Without // it the first load counts as applied: the caller builds what the applier // would set from [Reloader.Config] before serving. ApplyInitial bool // Apply applies ev.Changes. It must be idempotent: a retry passes // changes it may already have made. Apply func(ctx context.Context, ev ApplyEvent[T]) (ApplyResult, error) }
Applier applies the part of a published config it owns to the running process: a log level to a slog.LevelVar, limits to a pool, gates to a router. Register it with WithApplier.
After every published config the reloader calls each applier that owes something — once, in registration order, holding the reload's single-flight slot — with the keys it owns whose published value differs from what it last acknowledged. Returning nil acknowledges every change it was given except the RestartKeys it returns, which wait for a restart and are offered again on every later config. Returning an error, or panicking, acknowledges nothing and leaves the applier uncertain — what it did with the keys it was given may have half happened — so later calls give it those keys again, Retry set, until a call applies each of them.
type ApplierFailure ¶
ApplierFailure is one applier's failure to apply a published config.
type ApplierStatus ¶
type ApplierStatus struct {
Name string `json:"name"`
// Invocations counts calls, the initial apply's included.
Invocations uint64 `json:"invocations"`
// Failures counts calls that returned an error, panicked, or returned a
// restart key that was not one of their changes.
Failures uint64 `json:"failures"`
// LastApplied is when a call last returned nil, in UTC.
LastApplied time.Time `json:"last_applied,omitzero"`
// LastFailure is when a call last failed, in UTC.
LastFailure time.Time `json:"last_failure,omitzero"`
// LastError is the failure that left the applier uncertain or a config
// it was never called for; nil once a later call applies every key a
// failed call was given.
LastError *Problem `json:"last_error,omitempty"`
// Uncertain reports an applier a failed call left keys of that no later
// call has applied: what the failure did with them is unknown, so each
// is offered again, Retry set, until a call applies it.
Uncertain bool `json:"uncertain,omitempty"`
// Pending lists the keys it owns whose published value differs from
// what it acknowledged: failed, deferred to a restart, or blocked by a
// structure it does not own whole.
Pending []Change `json:"pending,omitempty"`
}
ApplierStatus is one applier's account.
type ApplyError ¶
type ApplyError struct {
Failures []ApplierFailure
}
ApplyError reports the appliers that did not apply a published config. The config is live; each of them is called again on the next one. It wraps ErrApply and every failure's cause.
func (*ApplyError) Error ¶
func (e *ApplyError) Error() string
func (*ApplyError) Unwrap ¶
func (e *ApplyError) Unwrap() []error
type ApplyEvent ¶
type ApplyEvent[T any] struct { // Trigger names what published the config: a Trigger* constant, or // [TriggerInitial]. Trigger string // Generation is the published config's generation. Generation uint64 // Retry reports a call after a failure: a failed call may have half // applied the keys it was given. Changes then also lists each of those // keys whose published value is again what the applier acknowledged — // Old and New alike — so that it is applied again. A key that waits on // a structure the applier does not own whole is left out of every call // until it can be applied, and the applier stays uncertain until then. Retry bool // Previous is the config published before Desired, nil for the initial // apply. It is not what this applier applied — Changes are measured // from that. Previous *T // Desired is the config just published; shared and immutable. Desired *T // Changes are the keys the applier owns whose published value differs // from what it last acknowledged, as [Diff] reports them, and on a // retry the keys a failed call left uncertain. It is never empty. Changes []Change }
ApplyEvent is what an Applier is asked to apply.
type ApplyOutcome ¶
type ApplyOutcome struct {
Applier string `json:"applier"`
// Applied are the keys it acknowledged.
Applied []string `json:"applied,omitempty"`
// Deferred are the keys it returned as restart keys.
Deferred []string `json:"deferred,omitempty"`
// Error is its failure; nil when it applied.
Error *Problem `json:"error,omitempty"`
}
ApplyOutcome is what one applier did for one published config.
type ApplyResult ¶
type ApplyResult struct {
// RestartKeys are keys of the event's Changes left unapplied because
// they take a restart. Every other change is acknowledged.
RestartKeys []string
}
ApplyResult is what an Applier that returns nil applied.
type Change ¶
type Change struct {
// Key is the runtime key (db.host, items[0].port, named["primary"].port).
Key string `json:"key"`
// Structural reports a change to a struct, map, list or interface itself
// rather than to a value inside it: it appeared, disappeared, became nil
// or non-nil, or an interface's value changed to or from a struct, map or
// list of another type. Old and New then describe presence and type
// only; the keys beneath it that changed are changes of their own.
Structural bool `json:"structural,omitempty"`
// Field is the declared field Key belongs to, as [FieldFor] reports it.
// Its Tag is metadata for code, not for output.
Field Field `json:"-"`
Old Value `json:"old"`
New Value `json:"new"`
}
Change is one key whose value differs between two configs.
func Diff ¶
Diff compares two configs of type T, a struct, and returns every key whose value differs, in walk order: fields as declared, map entries by sorted key, list elements by index. A nil config has no keys. It compares decoded values, never their text, and neither input is changed.
- Lists compare by index: removing the first of three elements changes the first two and removes the third. Map entries compare by key; an added or removed entry is a change of every key beneath it.
- A struct, map, list or interface that appears, disappears or turns nil, or a nil collection becoming empty, is a Structural change at its key, followed by the keys beneath it that changed. An interface whose value changes type, or turns nil, is compared whole: one change at its key, Structural when either value is a struct, map or list.
- Values decoded whole — scalars, self-decoding structs, maps keyed by non-strings — compare with reflect.DeepEqual: no Equal or String method decides, so two time.Time values for one instant in different locations differ, and NaN never equals itself.
- A value PrintRedacted would redact is redacted on both sides; a sensitive map, list or interface is compared whole, so the keys it holds, which may themselves be secrets, are never listed.
- A pointer, map or slice that leads back to one it lies within, on either side, is compared whole there, so a cycle ends.
A T that is not a struct, a struct whose keys cannot be told apart, an interface holding such a struct, and a non-nil func, channel or unsafe pointer anywhere in either config — inside a value compared whole included — are ErrSchema errors.
Example ¶
ExampleDiff lists what changed between two configs, keyed in the package's grammar; a credential's change is reported with its values redacted.
package main
import (
"fmt"
"github.com/ashtonian/pkg/config"
)
func main() {
type Limits struct {
Rate int `koanf:"rate"`
}
type AppConfig struct {
Port int `koanf:"port"`
Password string `koanf:"password"`
Tags []string `koanf:"tags"`
Features map[string]Limits `koanf:"features"`
}
old := &AppConfig{Port: 8080, Password: "a", Tags: []string{"web"}, Features: map[string]Limits{"audit": {Rate: 10}}}
next := &AppConfig{Port: 9090, Password: "b", Tags: []string{"web", "api"}, Features: map[string]Limits{"audit": {Rate: 20}, "export": {Rate: 1}}}
changes, err := config.Diff(old, next)
if err != nil {
fmt.Println(err)
return
}
for _, c := range changes {
switch {
case c.Structural:
fmt.Printf("%s: added (%s)\n", c.Key, c.Field.Path)
case !c.Old.Present:
fmt.Printf("%s: + %s\n", c.Key, c.New.Text)
default:
fmt.Printf("%s: %s -> %s\n", c.Key, c.Old.Text, c.New.Text)
}
}
}
Output: port: 8080 -> 9090 password: [REDACTED] -> [REDACTED] tags[1]: + api features["audit"].rate: 10 -> 20 features["export"]: added (features[*]) features["export"].rate: + 1
func RestartChanges ¶ added in v0.7.0
RestartChanges returns caller-owned changes whose resolved class is restart. It is an offline declaration check, independent of runtime applier ownership.
type ContextProvider ¶
type ContextProvider interface {
koanf.Provider
ReadContext(ctx context.Context) (map[string]any, error)
ReadBytesContext(ctx context.Context) ([]byte, error)
}
ContextProvider is a koanf.Provider whose reads take a context. A provider given to WithBaseProvider or WithOverrideProvider that implements it is read through ReadContext or ReadBytesContext with the load's context, so a remote read stops when the load is cancelled — by its caller, or by a Reloader's Stop.
func NewMapProvider ¶
func NewMapProvider(read func(ctx context.Context) (map[string]any, error), delimiter string) (ContextProvider, error)
NewMapProvider returns a provider over the configuration read returns, read afresh on every load — a database table, a key-value store, a cluster's replicated settings — for WithBaseProvider or WithOverrideProvider with a nil parser:
settings, err := config.NewMapProvider(func(ctx context.Context) (map[string]any, error) {
return loadSettings(ctx, db) // e.g. SELECT key, value FROM settings
}, ".")
reloader, err := config.NewReloader[AppConfig](ctx,
[]config.Option{config.WithBaseProvider(settings, nil)},
config.WithPollInterval(30*time.Second))
read is called with the load's context, so a slow store stops when the load is cancelled; a nil map is an empty source, and an error fails the load with ErrSource. What read returns is copied, so the caller may keep and change it. With delimiter "." the keys are dotted paths (db.max_conns) and are unflattened into the key tree; a key with an empty segment, or one that is a prefix of another (db and db.host), is an error, since it would set a value and a section at once. With delimiter "" the map is already nested. Any other delimiter is an error. It is a source, not a watch: call Reloader.SourceChanged when the store says it changed, or use WithPollInterval.
type Defaulter ¶ added in v0.10.0
type Defaulter interface {
SetDefaults()
}
Defaulter is a config type that sets defaults in code, for what a default tag cannot say: a shared section that each program embedding it sets differently, or one type at several paths with different defaults.
type Brokers struct {
Primary Broker `koanf:"primary"`
Secondary Broker `koanf:"secondary"`
}
// Only the primary broker has a default host.
func (b *Brokers) SetDefaults() { b.Primary.Host = "tcp://localhost:1883" }
A load fills a fresh value with the default tags, then calls SetDefaults on every Defaulter in it — the root, its sections and pointer sections, and fields of named types — innermost first, so an enclosing struct's SetDefaults overrides a section's. What the calls change is the code defaults layer: above the tags, below WithDefaults and every source. A field SetDefaults leaves alone keeps its tag. A map's or list's element type that is a Defaulter runs for each entry a source creates, after its tags, as element tags apply. A nil pointer section whose type sets defaults is allocated for its SetDefaults and kept only if it then holds anything.
An embedded struct's SetDefaults is promoted as Go promotes it: it runs as the enclosing struct's, unless the enclosing struct declares its own, which calls the embedded one when it should run. Two embedded Defaulters whose methods collide promote neither, which fails with ErrSchema, as does SetDefaults with a value receiver, which could change nothing.
SetDefaults is deterministic and does no I/O: it runs at every load and reload, and for Usage, JSONSchema, PrintTemplate, CheckKey and DecodeKey.
type Field ¶
type Field struct {
// Path is the koanf dot-path, e.g. "db.max_conns".
Path string
// Tag is the leaf's struct tag; read default, flag, desc and the rest from it.
Tag reflect.StructTag
// Type is the leaf's declared Go type.
Type reflect.Type
// Sensitive reports that PrintRedacted redacts the leaf's value: it, or
// a struct above it, is tagged sensitive:"true", or its Go field name
// looks like a credential (password, secret, token, key, ...) and its type
// can contain text, without sensitive:"false". Plain numbers, booleans and
// durations are not inferred sensitive. A struct field's name does not cover the
// struct it holds.
Sensitive bool
// EnvRef marks a string or []string whose decoded values name environment variables.
EnvRef bool
// Reload is the inherited reload class: restart by default, live or a custom label.
Reload string
// Index is the leaf's index sequence in the config struct, for
// reflect.Value.FieldByIndex (or FieldByIndexErr, when a pointer on the
// way may be nil).
Index []int
}
Field is one leaf key of a configuration struct: the unit a default tag, an environment variable or a CLI flag sets.
func FieldFor ¶
FieldFor returns the declared field a key of T names. A struct field's key returns its Field, Index included. A key inside a map or list — items[0].port, limits["/upload"] — returns a Field synthesized for every element or entry: Path is the pattern (items[*].port), Tag and Type the element's, Sensitive inherited from what holds it, and Index nil. A key beneath an interface returns the interface's field, which owns whatever the value holds. The map keys and indexes in key need not exist in any config. Inspection does not decode default/example tags. A field T does not have, a selector of the wrong kind, an index past a fixed array's end, or a key that does not parse is an ErrKey error.
func Fields ¶
Fields reports every leaf key of T in declaration order. It is the single definition of how a config struct maps to keys, shared by struct-tag defaults, environment-variable mapping, redacted printing and the kongprovider flag bridge:
- A key segment is the koanf tag's name, or the lowercased field name when the tag has none. Options after a comma are not part of the name.
- Unexported fields, koanf:"-" and koanf:",remain" are skipped. koanf:",squash" merges a struct's keys into its parent, as mapstructure decodes it — an unexported embedded struct's exported fields too, as encoding/json promotes them. An embedded pointer to an unexported struct cannot be allocated, and a schema squashing one is refused with ErrSchema.
- Pointers are followed. A struct is descended into unless it decodes itself — encoding.TextUnmarshaler (time.Time, netip.Addr, ...) or mapstructure's Unmarshaler: such a struct is one leaf decoded whole, exactly as decoding treats it.
- Maps, slices, arrays and interfaces are leaves.
- A struct type is never re-entered while it is being walked, so a recursive type yields the keys above the recursion rather than recursing forever. Values for the recursive part still decode from files; they just have no default, environment or flag key.
A T that is not a struct (or pointer to one), or whose keys are invalid or ambiguous, has no fields. Use FieldsChecked to distinguish an empty catalog from an invalid schema.
func FieldsChecked ¶ added in v0.5.0
FieldsChecked returns the same catalog as Fields, or ErrSchema for a non-struct root, invalid key or ambiguous name. It reads tags as metadata; it does not decode defaults or examples. Load validates those separately. The returned slice and field indexes are caller-owned copies of cached metadata.
func MatchFields ¶ added in v0.6.0
MatchFields associates reflected targets, such as CLI flag destinations, with fields in cfg in one traversal. Results correspond to targets in the same order. A result with an empty Path means the target is not a config field: invalid/unaddressable values, cfg itself, skipped fields, collection elements and values outside cfg do not match. A field behind a nil pointer has no address.
Matching reads addresses, not contents, and does not decode tags. It returns ErrSchema for invalid config metadata and ErrKey for nil cfg or ambiguous addresses (including shared zero-sized fields). Results and their indexes are caller-owned. They describe this call's cfg; call again after replacing pointers or targets. No instance addresses are retained in the metadata cache.
Example ¶
package main
import (
"fmt"
"reflect"
"github.com/ashtonian/pkg/config"
)
func main() {
type Settings struct {
Port int `koanf:"port" desc:"HTTP port"`
}
cfg := Settings{Port: 8080}
verbose := false
fields, err := config.MatchFields(&cfg, []reflect.Value{
reflect.ValueOf(&cfg.Port).Elem(),
reflect.ValueOf(&verbose).Elem(),
})
if err != nil {
panic(err)
}
fmt.Println(fields[0].Path, fields[0].Tag.Get("desc"))
fmt.Println(fields[1].Path == "")
}
Output: port HTTP port true
func (Field) EnvVar ¶
EnvVar is the environment variable the environment layer reads for f under prefix — a prefix WithEnvPrefix or WithEnvPrefixes was given, with or without its trailing "_". It is empty when prefix is.
func (Field) EnvVars ¶ added in v0.2.0
EnvVars returns every variable the environment layer reads for f under prefixes, as WithEnvPrefixes takes them, the first winning when several are set: Field.EnvVar under each prefix, most specific first, then the names its env tag lists, read as written. With no prefix the environment is not read, and there are none.
func (Field) PrintableDefault ¶
PrintableDefault returns f's `default` tag and true when diagnostics may print it, and false when f has no default or it must be withheld: f is Sensitive, or its type could hold a sensitive value beneath it — a map or interface, whose entries only a decoded value reveals, or a struct, slice or array holding a sensitive field or one of those.
func (Field) PrintableExample ¶ added in v0.2.0
PrintableExample returns f's `example` tag and true when diagnostics may print it: under the rule Field.PrintableDefault applies to a default.
type Key ¶ added in v0.7.0
type Key []Segment
Key is a configuration key in the grammar used by Diff, FieldFor and appliers.
func ParseKey ¶ added in v0.7.0
ParseKey parses fields, quoted map keys, decimal list indexes and [*] patterns. Malformed and empty keys return an error wrapping ErrKey.
func (Key) Fields ¶ added in v0.7.0
Fields returns only the struct fields, without map entries or list selectors. For example, items[0].port becomes items.port.
type KeyError ¶ added in v0.8.0
type KeyError struct {
Key string `json:"key"`
Unknown bool `json:"unknown,omitempty"`
Err error `json:"-"`
}
KeyError distinguishes an unknown key (Unknown, ErrKey) from a known key with an invalid value (ErrValidation). Error contains no input value; Err preserves the decoder or validator's cause for trusted diagnostics.
type Option ¶
type Option func(*options)
Option configures Load and the pipeline a Reloader re-runs.
func WithAllowUnknownKeys ¶
func WithAllowUnknownKeys() Option
WithAllowUnknownKeys lets a load succeed when a source sets a key no field decodes — for a file shared with another program — by accepting every one silently. Use WithUnknownKeys to accept them and still report them.
Example ¶
ExampleWithAllowUnknownKeys shows a typo in a config file failing the load instead of leaving the service on the default it meant to override, and the option that lets such a key through.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/ashtonian/pkg/config"
)
// tempFile writes content to a file in a new temporary directory and
// returns its path and a cleanup function.
func tempFile(name, content string) (string, func()) {
dir, err := os.MkdirTemp("", "config-example")
if err != nil {
log.Fatal(err)
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
log.Fatal(err)
}
return path, func() { _ = os.RemoveAll(dir) }
}
func main() {
type AppConfig struct {
Server struct {
Port int `koanf:"port" default:"8080"`
} `koanf:"server"`
}
file, cleanup := tempFile("config.yaml", "server:\n prot: 9000\n")
defer cleanup()
_, err := config.Load[AppConfig](context.Background(), config.WithFile(file))
fmt.Println(errors.Is(err, config.ErrValidation), err)
cfg, err := config.Load[AppConfig](context.Background(), config.WithFile(file), config.WithAllowUnknownKeys())
fmt.Println(cfg.Server.Port, err)
}
Output: true unmarshaling config: configuration validation failed: keys no field of config_test.AppConfig decodes: server.prot (did you mean server.port?) 8080 <nil>
func WithBaseProvider ¶
WithBaseProvider adds a koanf provider loaded above the defaults and below the config files — the place for a remote store (Consul, etcd, S3) that local files and the environment refine. parser is nil for a provider that returns a map (Read), or the parser for one that returns bytes (ReadBytes): a file, an HTTP body. Base providers load in the order added.
A provider that implements ContextProvider is read with the load's context. A provider failing to load fails the load with ErrSource. Source options such as WithInvalidEntries apply only to this provider.
func WithDecodeHook ¶
func WithDecodeHook(hook mapstructure.DecodeHookFunc) Option
WithDecodeHook adds a mapstructure decode hook. Hooks run in the order added, before the built-in ones (string → time.Duration, comma-separated string → slice, encoding.TextUnmarshaler), so a hook can take over a conversion the built-ins would otherwise make — splitting a slice on ";", say, or decoding a custom type from a map.
func WithDefaults ¶
WithDefaults adds programmatic default values keyed by koanf dot-path (e.g. "server.port"); they override the code defaults (Defaulter) and tags. Repeated calls merge, a later call winning on a shared key. The values are deep-copied when WithDefaults is called — every map key and value, slice, array and pointer target reachable through exported fields — so changing the caller's data afterwards changes nothing, including what a Reloader built from the option reads later. Unexported struct fields, channels and functions are copied by value.
func WithEnvPrefix ¶
WithEnvPrefix enables environment variables named PREFIX_<KEY>, the key path upper-cased with "." → "_" (APP_DB_MAX_CONNS sets db.max_conns). The environment is not read without a prefix. WithEnvPrefix(p) is WithEnvPrefixes(p).
func WithEnvPrefixes ¶ added in v0.9.0
WithEnvPrefixes enables environment variables under each prefix, most specific first: for every key, the first prefix whose variable is set wins, then the names the field's env tag lists. A binary that runs several services reads a shared layer beneath each service's own:
config.WithEnvPrefixes("APP_WORKER", "APP") // APP_WORKER_DB_HOST, then APP_DB_HOST
The environment stays one source, between the config files and the override providers. A prefix's trailing "_" is optional; empty and repeated prefixes are left out, and with none left the environment is not read. The last call wins.
A variable two keys could read, under one prefix or across two (APP_WORKER_DB_HOST is db.host under APP_WORKER and worker.db.host under APP), fails the load naming every reading. So does setting a whole map or format:"json" section and a key beneath it, under one prefix or two. A variable under a prefix that no key reads under any prefix is unknown (see WithUnknownKeys); claim the shared prefix's variables only other services read with WithVariableFilter or WithKnownVariables.
Example ¶
ExampleWithEnvPrefixes runs one service of a binary that hosts several: APP_* is the layer every service reads, APP_WORKER_* the worker's own, which wins key by key. The usage table lists both in the order the load reads them.
package main
import (
"context"
"fmt"
"os"
"github.com/ashtonian/pkg/config"
)
func main() {
type DBConfig struct {
Host string `koanf:"host" default:"localhost" desc:"Database host"`
MaxConns int `koanf:"max_conns" default:"10" desc:"Pool size"`
}
type WorkerConfig struct {
DB DBConfig `koanf:"db"`
Concurrency int `koanf:"concurrency" default:"4" desc:"Jobs run at once"`
}
environ := []string{
"APP_DB_HOST=db.internal", // every service
"APP_DB_MAX_CONNS=20",
"APP_WORKER_DB_MAX_CONNS=50", // the worker only
"APP_WORKER_CONCURRENCY=16",
}
cfg, err := config.Load[WorkerConfig](context.Background(),
config.WithEnvPrefixes("APP_WORKER", "APP"),
config.WithEnviron(func() []string { return environ }),
)
if err != nil {
fmt.Println("load:", err)
return
}
fmt.Printf("host=%s max_conns=%d concurrency=%d\n\n", cfg.DB.Host, cfg.DB.MaxConns, cfg.Concurrency)
if err := config.PrintUsage[WorkerConfig]("APP_WORKER", config.WithFallbackEnvPrefixes("APP"), config.WithWriter(os.Stdout)); err != nil {
fmt.Println(err)
}
}
Output: host=db.internal max_conns=50 concurrency=16 KEY ENV TYPE DEFAULT RELOAD DESCRIPTION db.host APP_WORKER_DB_HOST, APP_DB_HOST string localhost restart Database host db.max_conns APP_WORKER_DB_MAX_CONNS, APP_DB_MAX_CONNS int 10 restart Pool size concurrency APP_WORKER_CONCURRENCY, APP_CONCURRENCY int 4 restart Jobs run at once
func WithEnviron ¶
WithEnviron sets where environment variables are read from: a function returning "NAME=value" entries, as os.Environ does, called on every load and reload. The default is os.Environ. Tests and examples pass a fixed list; a deployment adapter can pass variables gathered elsewhere.
func WithFile ¶
WithFile adds an optional config file. A missing file is skipped; one that exists but cannot be read fails the load with ErrFileRead, and one the file parser rejects fails it with ErrFileParse. Files merge in the order added, a later file winning on a shared key.
Under a Reloader, a file that loaded once is required from then on: a reload that finds it gone fails and keeps the previous config rather than quietly reverting its keys to lower-precedence values.
func WithFileParser ¶
WithFileParser parses the config files whose names end in suffix with p, any koanf parser: WithFileParser(".toml", toml.Parser()). Each file takes the parser of the longest suffix its name ends in, ignoring case, so files of different formats load together. Built in, .yaml and .yml parse as YAML, .json as JSON, and any other file as YAML; the suffix "" names every file no longer suffix matches, so WithFileParser("", p) changes that default. A suffix without its leading dot gets one. A nil p removes suffix's parser, leaving its files to a shorter suffix.
The JSON parser reads one object. Integers decode as integers, a key set twice in one object fails the load, and a parse error names its line.
func WithFiles ¶
WithFiles adds several optional config files, merged in order. Each follows the WithFile rules.
func WithKnownVariables ¶ added in v0.7.0
WithKnownVariables claims exact, case-sensitive environment variable names read by the caller. They are omitted from unknown-variable reports; normal configuration decoding and unknown-key checks are unchanged. Calls accumulate.
func WithLogger ¶
WithLogger sets the logger for load diagnostics (the sources loaded, and environment variables that fit no key, at debug level) and, for a Reloader built by NewReloader, its reload events. Without it, or with a nil logger, every line goes to slog.Default() as it is when the line is written, so a logger configured after construction — from the config just loaded, say — takes effect.
func WithOverrideProvider ¶
WithOverrideProvider adds a koanf provider loaded above the environment, at the highest precedence — the place for explicit CLI flags (see the kongprovider package) and other operator overrides. Override providers load in the order added, a later one winning on a shared key. parser, ContextProvider and failures follow WithBaseProvider.
func WithPostLoad ¶
WithPostLoad registers a hook that runs after decoding and secret resolution, and before validation: the place for derivation and normalization decode hooks cannot express — collapsing a shorthand into its canonical form, expanding a profile into the knobs it stands for, deriving a default from the host.
Because the hook runs inside the load, a Reloader applies it on every reload before deciding whether to swap, so normalization is covered by validate-before-swap. The same work done after Load returns is not: under a Reloader the new config is already live by the time it fails.
The hook mutates *cfg in place. Its error fails the load with ErrPostLoad and, under a Reloader, keeps the previous config. Hooks run in registration order; the first error stops the chain. A hook written for a type other than the one loaded fails the load before any source is read.
func WithRequiredFile ¶
WithRequiredFile adds a config file that must exist: a missing or unreadable file fails the load with ErrFileRead, one the file parser rejects with ErrFileParse.
func WithRequiredFiles ¶
WithRequiredFiles adds several required config files, merged in order.
func WithSecretPopulator ¶
func WithSecretPopulator(p SecretPopulator) Option
WithSecretPopulator sets the secret resolver. It runs after decoding with the load's context and a pointer to the decoded config; its error fails the load with ErrSecretProvider.
func WithShadowedInputs ¶ added in v0.10.0
func WithShadowedInputs(handle func(ctx context.Context, shadowed *ShadowedInputsError) error) Option
WithShadowedInputs decides what a load does with shadowed inputs: values a source sets for a key that a higher-precedence source also sets, so the lower value cannot take effect — a config file's key that a leftover environment variable or a typed flag also sets. handle is called with them whenever there are any, at every Load and every reload. A nil return lets the load go on; an error fails it with ErrValidation, the error kept as its cause. Return shadowed to require that every key have one source:
config.WithShadowedInputs(func(_ context.Context, shadowed *config.ShadowedInputsError) error {
return shadowed
})
Overriding a file from the environment or the command line is a legitimate pattern, so without a handler shadowed inputs never fail a load: LoadSnapshot and every NewReloader load list them in Snapshot.Shadowed and log them at debug level, and Load does not look for them. A nil handle restores that default.
Two kinds of layering are designed overrides and never reported: defaults (default tags, Defaulter and WithDefaults) beneath any source, and the precedence inside the environment layer — WithEnvPrefixes' narrower prefixes over broader ones, prefixed names over env tag names — which is one source. Inputs are named by key and source, never value; an input at a key no field reads is left to WithUnknownKeys.
func WithSkipValidation ¶
func WithSkipValidation() Option
WithSkipValidation disables validation — the validator, validate:"required" included, and a root Validate method (see Validatable). Unknown keys still fail the load; WithUnknownKeys decides those separately.
func WithUnknownKeys ¶ added in v0.2.0
func WithUnknownKeys(handle func(ctx context.Context, unknown *UnknownKeysError) error) Option
WithUnknownKeys decides what a load does with input no field reads: keys a config file, provider or WithDefaults sets that no field decodes, and environment variables under a WithEnvPrefixes prefix that name no key under any of them. handle is called with them whenever there are any, at every Load and every reload, each with the key or variable it most likely misspells. A nil return lets the load go on without them; an error fails it with ErrValidation, the error kept as its cause. Return unknown to fail as the default does, or a copy holding only the keys that matter:
config.WithUnknownKeys(func(ctx context.Context, unknown *config.UnknownKeysError) error {
logger.WarnContext(ctx, "config input no field reads", "error", unknown)
return nil
})
Without it, a load fails on unknown keys, naming each one (server.prot, items[0].prot, named["primary"].prot), so a misspelled key cannot leave a service running on the default it was meant to override; unknown variables are logged at debug level and never fail it, since other programs may share the prefix. A nil handle restores that default.
The decoder reports the keys it did not use while it decodes the merged sources, so the check follows decoding exactly: into slice elements and map entries of structs, through squashed structs, and past input a decode hook or a self-decoding type consumes. A map's own keys, anything beneath an interface, and keys a koanf:",remain" field collects are accepted. A section no field holds is one key (legacy, not legacy.old). The merged tree does not record which source set a key, so keys are named, not their sources; values are never reported.
Example ¶
ExampleWithUnknownKeys accepts input no field reads and reports it: a misspelled key in the file and a misspelled environment variable, each with what it most likely meant.
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"github.com/ashtonian/pkg/config"
)
// tempFile writes content to a file in a new temporary directory and
// returns its path and a cleanup function.
func tempFile(name, content string) (string, func()) {
dir, err := os.MkdirTemp("", "config-example")
if err != nil {
log.Fatal(err)
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
log.Fatal(err)
}
return path, func() { _ = os.RemoveAll(dir) }
}
func main() {
type AppConfig struct {
Server struct {
Port int `koanf:"port" default:"8080"`
} `koanf:"server"`
}
file, cleanup := tempFile("config.yaml", "server:\n prot: 9000\n")
defer cleanup()
cfg, err := config.Load[AppConfig](context.Background(),
config.WithFile(file),
config.WithEnvPrefix("APP"),
config.WithEnviron(func() []string { return []string{"APP_SERVER_PROT=9000"} }),
config.WithUnknownKeys(func(_ context.Context, unknown *config.UnknownKeysError) error {
for _, k := range unknown.Keys {
fmt.Printf("warning: unknown key %s, did you mean %s?\n", k.Key, k.Suggestion)
}
for _, v := range unknown.Variables {
fmt.Printf("warning: unknown variable %s, did you mean %s?\n", v.Key, v.Suggestion)
}
return nil
}))
fmt.Println(cfg.Server.Port, err)
}
Output: warning: unknown key server.prot, did you mean server.port? warning: unknown variable APP_SERVER_PROT, did you mean APP_SERVER_PORT? 8080 <nil>
func WithValidator ¶
WithValidator sets the validator. nil restores the default: a go-playground validator that names fields by config key. Register FieldName on a custom go-playground validator to get the same naming.
func WithVariableFilter ¶ added in v0.7.0
WithVariableFilter claims otherwise unmatched environment variable names for which claimed returns true. It sees names only, never values. A nil predicate clears the filter. The last call wins.
type Origin ¶
type Origin struct {
// Input is the source whose value the merge kept for the key: what the
// key held when it was decoded, before any stage ran. A key no source
// set holds its zero value.
Input Source `json:"input"`
// Transforms are the stages that changed the value after decoding, in
// order: the secret populator, then post-load hooks by position. A stage
// that wrote the value the key already held is not listed. With any
// listed, the value is the last one's — computed from whatever it read —
// and Input names only what it replaced.
Transforms []Source `json:"transforms,omitempty"`
// Exact is false when Input cannot be named with certainty: decoding
// reshaped the input — a weakly decoded shape, or a decode hook that
// rebuilt a map or list — so Input is the one source of what the value
// was decoded from, or unknown.
Exact bool `json:"exact"`
}
Origin is where one key's value came from: the input source that set it as the sources merged, and the pipeline stages that changed it after decoding.
type OriginProvider ¶
type OriginProvider interface {
koanf.Provider
ReadWithOrigins(ctx context.Context) (map[string]any, map[string]Source, error)
}
OriginProvider is a map provider that names a source for each key it returns, read once with the load's context: kongprovider names the flag behind each value. Annotations are keyed in the package's key grammar; an annotation follows field aliases and case folding as the data does. An annotation on a section covers the keys beneath it, and a key with none is attributed to the provider. An annotation naming a key the data does not hold, using a selector of the wrong kind, naming a list element, or naming the key another annotation names with another source fails the load with ErrSource. A load reads the annotations when it attributes values: provenance (LoadSnapshot, a reloader WithProvenance) and shadowed-input reporting (every NewReloader load, and a load WithShadowedInputs); any other load calls Read. They are used for providers loaded with a nil parser.
type ParseError ¶
type ParseError struct {
// Source names what was parsed: a file path, or "base provider 0".
Source string
// Line is the line the parser reported, or 0.
Line int
// Err is the parser's error.
Err error
}
ParseError reports bytes a parser rejected: a config file, or a provider loaded with a parser. Its message names the line when the parser gave one, and leaves out the parser's own message, which may quote the source — a credential in a file whose syntax broke around it. Err is that message's error, for a diagnostic shown only where the source may be; errors.Unwrap and errors.As reach it.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
type PrintOption ¶
type PrintOption func(*printOptions)
PrintOption configures output of PrintRedacted.
func WithFallbackEnvPrefixes ¶ added in v0.9.0
func WithFallbackEnvPrefixes(prefixes ...string) PrintOption
WithFallbackEnvPrefixes names the prefixes a WithEnvPrefixes load reads beneath the one PrintUsage, PrintTemplate and PrintEnv are given, in precedence order, so their output follows that load: a load with WithEnvPrefixes("APP_WORKER", "APP") is described by
config.PrintUsage[Config]("APP_WORKER", config.WithFallbackEnvPrefixes("APP"))
Every variable is listed in the order the load reads them. Where one variable is written for a key, as PrintEnv and FormatEnv write one, it is the one under the first prefix, and a name another key reads under any of the prefixes is not written. The last call wins.
func WithFormat ¶
func WithFormat(format string) PrintOption
WithFormat sets the output format. Each printer documents the formats it writes and its default; any other value makes it return an error.
func WithIncludeDescriptions ¶
func WithIncludeDescriptions() PrintOption
WithIncludeDescriptions includes desc:"" tag values in the printed output. When set, the table, JSON, and env output includes the desc:"description" tag value alongside each field.
func WithSortByName ¶
func WithSortByName() PrintOption
WithSortByName sorts output fields alphabetically by name.
func WithWriter ¶
func WithWriter(w io.Writer) PrintOption
WithWriter sets the output destination. Defaults to os.Stdout.
type Problem ¶
type Problem struct {
// Kind is the error's category: one of the Kind* constants.
Kind string `json:"kind"`
// Message is the error's text. The package's own text quotes no value
// meant for a sensitive field; text from your providers, secret
// populator, hooks and validator is yours, as written.
Message string `json:"message"`
}
Problem is an error as data, for a status endpoint or a stats snapshot: its category and its message.
type ReloadEvent ¶
type ReloadEvent[T any] struct { // Trigger names what drove the reload: one of the Trigger* constants. Trigger string `json:"trigger"` // Old is the config the reload replaced; New the one it published. Both // are shared and immutable. Old *T `json:"-"` New *T `json:"-"` // Result is what Reload returns for the same reload. Result ReloadResult[T] `json:"result"` }
ReloadEvent is what a WithOnReloadSuccess callback receives: the trigger, the config replaced and the one published, and the reload's result.
type ReloadOption ¶
type ReloadOption func(*reloadOptions)
ReloadOption configures a Reloader.
func WithApplier ¶
func WithApplier[T any](a Applier[T]) ReloadOption
WithApplier registers a; appliers run in registration order. An applier with no name or a repeated one, no Apply, or no prefix fails construction, as does a prefix that names no key of the config, reaches beneath an interface or a value compared whole, or is as specific as another applier's and could name the same key. See Applier.
With appliers registered, the reloader accounts for every key: a change no applier owns, or one an applier returned as a restart key, is reported in Snapshot.PendingRestart until it is applied or reverted. Register an applier for keys your code reads from Reloader.Config on every use, so they do not read as waiting for a restart.
func WithFileWatch ¶
func WithFileWatch(debounce time.Duration) ReloadOption
WithFileWatch reloads when a watched file changes: the config files of a NewReloader, and any given to WithWatchedFiles. debounce coalesces a burst of filesystem events (editors that rename and write, atomic-write tools) into one reload; zero or negative selects DefaultFileWatchDebounce. A continuous stream of events delays a reload by at most ten debounce windows. A reloader with nothing to watch fails construction.
Detected: direct writes, write-temp-then-rename saves, symlink swaps of the file or of any directory on its path, and a directory removed and recreated — including Kubernetes ConfigMap and Secret volume updates, where the watched path is a symlink into `..data` and the leaf file is never written. Paths resolve as the kernel resolves them, ".." after a symlink included, so the watch follows the file the loader opens. Every four debounce windows, and no more often than once a second (every 2s at the default debounce), the watch also resolves its paths again with no event to prompt it. That catches what sends the watch no event: an ancestor directory renamed away and recreated, whose event goes to the old tree; and, on kqueue platforms (macOS, *BSD), a relink whose only trace is a temporary name renamed over the link (`ln -s new link.tmp && mv -Tf link.tmp link`), which fsnotify there does not report because the directory's set of names is unchanged.
NOT detected:
- ConfigMap/Secret volumes mounted with subPath. kubelet cannot swap a symlink through a bind mount, so these never update in the container at all — neither a watch nor polling can see a change. Mount the volume without subPath.
- Sources that are not files: environment variables and providers. Use WithPollInterval, or Reloader.SourceChanged from whatever learns of the change.
- A symlink directly under the filesystem root, such as /var or /tmp on macOS, being re-pointed. Those belong to the OS; watching / would hold a descriptor per entry on kqueue and can block on automount points.
func WithOnReloadError ¶
func WithOnReloadError(fn func(trigger string, err error)) ReloadOption
WithOnReloadError registers a callback invoked after every failed reload, whatever triggered it — including the watchers Reloader.Run serves, whose errors would otherwise reach only the log. It fires in addition to the error Reloader.Reload returns. trigger is one of the Trigger* constants. Callbacks follow the WithOnReloadSuccess rules.
Reloads that reach no verdict — ErrReloadInProgress, ErrReloaderStopped, or a reload abandoned because its context ended — do not invoke it.
func WithOnReloadSuccess ¶
func WithOnReloadSuccess[T any](fn func(ctx context.Context, ev ReloadEvent[T])) ReloadOption
WithOnReloadSuccess registers a callback invoked after every successful reload with the reload's context and a ReloadEvent: the trigger, the config replaced and the one published, and the keys that changed. It runs after the swap, so it reacts to a config already live and cannot veto it. Every successful reload publishes and calls it, a poll that changed nothing included — its event's Changes is then empty. Callbacks run in registration order, panics are recovered, and they run while the reload holds the single-flight slot: hand long work off, and never call Reloader.Stop or Reloader.Arm from one. A callback written for a type other than the reloader's fails construction.
func WithPollInterval ¶
func WithPollInterval(interval time.Duration) ReloadOption
WithPollInterval reloads every interval: the fallback for sources without filesystem events (environment variables, providers) and for filesystems whose events are unreliable (NFS, some container runtimes). Every tick runs the whole pipeline, secrets included.
func WithProvenance ¶
func WithProvenance() ReloadOption
WithProvenance records where every key's value came from on each load, as LoadSnapshot does: Reloader.Snapshot carries the origins of the config it publishes, and a refused reload keeps the previous ones. It costs a copy and a comparison of the decoded config around the secret populator and each post-load hook, per load. It needs NewReloader's pipeline: NewReloaderFunc refuses it.
func WithReloadLogger ¶
func WithReloadLogger(logger *slog.Logger) ReloadOption
WithReloadLogger sets the logger for reload events. Without it, a NewReloader logs through its load options' WithLogger logger. A nil logger here, or no logger anywhere, means slog.Default() as it is when each line is written, so a reloader built before the process logger is configured logs through the configured one.
func WithReloadTagChecks ¶ added in v0.7.0
func WithReloadTagChecks() ReloadOption
WithReloadTagChecks enables WithReloadTags and rejects live keys with no applier, and appliers that own no live keys, with ErrSchema. It checks the whole schema, including collection patterns, before reading any sources.
func WithReloadTags ¶ added in v0.7.0
func WithReloadTags() ReloadOption
WithReloadTags enables the schema's reload classes for change routing. Restart keys are withheld from appliers and reported in PendingRestart. Other classes, including caller-defined labels, are live. A schema with no reload tags retains prefix-based routing. Metadata is available without this option; opting in separates declarations from existing caller-owned policy.
func WithSignalReload ¶
func WithSignalReload(signals ...os.Signal) ReloadOption
WithSignalReload reloads when the process receives one of signals, typically syscall.SIGHUP.
func WithWatchedFiles ¶
func WithWatchedFiles(paths ...string) ReloadOption
WithWatchedFiles adds files the file watch covers, and that Run compares with the last load once the watch is armed, beyond a NewReloader's config files: a file a secret populator or base provider reads, or, for NewReloaderFunc, the files the caller's loader reads.
type ReloadResult ¶
type ReloadResult[T any] struct { Snapshot Snapshot[T] `json:"snapshot"` // Changes lists every key whose value differs from the config the // reload replaced, as [Diff] reports them; empty when the sources did // not change. Changes []Change `json:"changes"` // Shadowed lists the inputs that changed since the previous generation // while a higher-precedence source holds their key, so the change cannot // take effect: a key a source set anew, changed or removed. A key can be // in Changes too, when the source above it changed in the same reload. // Each is also logged at warn level. Only [NewReloader] reports it. Shadowed []Shadow `json:"shadowed,omitempty"` // Applied lists, in registration order, every applier the reload // called or owed a call it could not make; an applier that owed nothing // is absent. Applied []ApplyOutcome `json:"applied,omitempty"` }
ReloadResult is what one reload published: the snapshot it produced, the keys that changed from the config before it, and what each applier did.
type ReloadStats ¶
type ReloadStats struct {
// Attempts is Successes+Failures — every reload that ran the pipeline.
Attempts uint64 `json:"attempts"`
// Successes counts reloads that published a new config.
Successes uint64 `json:"successes"`
// Failures counts reloads that left the previous config in place.
Failures uint64 `json:"failures"`
// LastTrigger names what drove the most recent attempt: one of the
// Trigger* constants. Empty before the first attempt.
LastTrigger string `json:"last_trigger,omitempty"`
// LastSuccess is when the most recent successful reload published, in
// UTC; zero, and absent from JSON, when none has.
LastSuccess time.Time `json:"last_success,omitzero"`
// LastFailure is when the most recent failed reload was rejected, in
// UTC; zero, and absent from JSON, when none has failed.
LastFailure time.Time `json:"last_failure,omitzero"`
// LastError is the most recent attempt's failure, nil when that attempt
// succeeded. A non-nil LastError beside an earlier LastSuccess means the
// config from that success is still live. Each call that returns stats
// returns its own copy.
LastError *Problem `json:"last_error,omitempty"`
}
ReloadStats is a Reloader's reload history at one moment, as returned by Reloader.Stats and carried by a Snapshot. It encodes to JSON as is.
Only reloads that ran to a verdict are counted. A manual Reload turned away by ErrReloadInProgress or ErrReloaderStopped ran no pipeline, and a reload abandoned because its context ended (including one cancelled by Stop) reached no verdict about the sources. The initial load is not a reload.
type Reloader ¶
type Reloader[T any] struct { // contains filtered or unexported fields }
Reloader holds the current configuration and replaces it when a source changes, only ever with one that passed the whole load pipeline. Config returns the current one with a single atomic load.
A reload is triggered by Reloader.Reload, or by what Reloader.Run serves: the file watch, a poll tick, a signal, or Reloader.SourceChanged. On failure the previous config stays live and the error is reported through WithOnReloadError, the log, and — for Reload — the return value.
Reloads are serialized. A manual Reload that finds another reload in flight returns ErrReloadInProgress; an automatic trigger instead waits for it and then reloads, so a change that lands while another reload is reading the sources is never lost.
The configs a Reloader publishes are shared by every reader and must be treated as immutable, nested maps and slices included: atomic publication makes the swap safe, not mutation.
func NewReloader ¶
func NewReloader[T any](ctx context.Context, loadOpts []Option, opts ...ReloadOption) (*Reloader[T], error)
NewReloader loads the initial config with Load's pipeline and loadOpts, and returns a Reloader that re-runs that pipeline, with the same options, on every reload. Each reload uses the ctx passed to Reload, or Run's for automatic triggers. The WithLogger logger in loadOpts also receives reload events unless WithReloadLogger sets another. The file watch covers the config files loadOpts name, and any WithWatchedFiles adds.
Example ¶
ExampleNewReloader reloads a file: a valid edit is swapped in, an invalid one is refused and the previous config stays live.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/ashtonian/pkg/config"
)
// tempFile writes content to a file in a new temporary directory and
// returns its path and a cleanup function.
func tempFile(name, content string) (string, func()) {
dir, err := os.MkdirTemp("", "config-example")
if err != nil {
log.Fatal(err)
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
log.Fatal(err)
}
return path, func() { _ = os.RemoveAll(dir) }
}
func main() {
type AppConfig struct {
Rate int `koanf:"rate" validate:"min=1"`
}
file, cleanup := tempFile("config.yaml", "rate: 10\n")
defer cleanup()
ctx := context.Background()
reloader, err := config.NewReloader[AppConfig](ctx,
[]config.Option{config.WithFile(file)},
config.WithOnReloadSuccess(func(_ context.Context, ev config.ReloadEvent[AppConfig]) {
old, new := ev.Old, ev.New
fmt.Printf("rate %d -> %d\n", old.Rate, new.Rate)
}),
)
if err != nil {
fmt.Println("load:", err)
return
}
defer func() { _ = reloader.Stop(ctx) }()
_ = os.WriteFile(file, []byte("rate: 20\n"), 0o600)
if _, err := reloader.Reload(ctx); err != nil {
fmt.Println("reload:", err)
}
_ = os.WriteFile(file, []byte("rate: 0\n"), 0o600)
_, err = reloader.Reload(ctx)
fmt.Println(errors.Is(err, config.ErrReload), errors.Is(err, config.ErrValidation))
fmt.Println("live rate:", reloader.Config().Rate)
}
Output: rate 10 -> 20 true true live rate: 20
func NewReloaderFunc ¶
func NewReloaderFunc[T any](ctx context.Context, load func(ctx context.Context) (*T, error), opts ...ReloadOption) (*Reloader[T], error)
NewReloaderFunc returns a Reloader whose loader is the caller's: load builds a complete, validated config, and its error keeps the previous one live. Use it to give an existing loading path the reload lifecycle. The file watch covers the files given to WithWatchedFiles; each reload compares them with what they held just before the previous load.
Example ¶
ExampleNewReloaderFunc gives an existing loader the reload lifecycle.
package main
import (
"context"
"fmt"
"github.com/ashtonian/pkg/config"
)
func main() {
type AppConfig struct{ Version int }
version := 0
load := func(context.Context) (*AppConfig, error) {
version++
return &AppConfig{Version: version}, nil
}
ctx := context.Background()
reloader, err := config.NewReloaderFunc(ctx, load)
if err != nil {
fmt.Println("load:", err)
return
}
defer func() { _ = reloader.Stop(ctx) }()
fmt.Println(reloader.Config().Version)
if _, err := reloader.Reload(ctx); err != nil {
fmt.Println("reload:", err)
}
fmt.Println(reloader.Config().Version, reloader.Stats().Successes)
}
Output: 1 2 1
func (*Reloader[T]) Arm ¶
Arm sets up the reloader's triggers — the file watch and the signal registration — and reports whether they could be: nil once every configured trigger is armed, the setup error otherwise, with nothing left armed. A caller that must not report itself ready on a reloader that cannot deliver what it was configured for calls Arm before Run:
if err := reloader.Arm(ctx); err != nil {
return err // e.g. a config directory that cannot be watched
}
go reloader.Run(ctx)
Run serves the triggers Arm set up, so nothing is missed in between: a change to a config file after Arm returns, or one made after NewReloader loaded and before the watch existed, is reloaded once Run starts, and a signal received in between is served then instead of taking the signal's default action. Poll ticks begin with Run. Run arms the triggers itself when Arm was not called.
Arm returns nil without arming again when the triggers are already armed — by an earlier Arm, or by a Run serving them — and ErrReloaderStopped after Stop. Comparing the files with the last load waits for a reload in flight, and a concurrent Arm waits for this one; ctx bounds both waits, and an Arm whose ctx ends while it waits returns ctx's error. Triggers Arm sets up are released when the Run serving them returns, or by Stop. Like Stop, Arm must not be called from a WithOnReloadSuccess or WithOnReloadError callback.
func (*Reloader[T]) Config ¶
func (r *Reloader[T]) Config() *T
Config returns the current configuration. This is an atomic load and safe to call from any goroutine without locking.
func (*Reloader[T]) Reload ¶
func (r *Reloader[T]) Reload(ctx context.Context) (*ReloadResult[T], error)
Reload triggers an immediate config reload and returns what it published: the new snapshot, the keys that changed and what each applier did. On failure the previous config is preserved and an error wrapping both ErrReload and the underlying cause is returned. A config published but not applied by every applier returns its result together with an ApplyError, which wraps ErrApply, not ErrReload.
If another reload is in flight this call does not wait: it returns ErrReloadInProgress, which deliberately does NOT wrap ErrReload — no pipeline ran and the config is unchanged. After Stop it returns ErrReloaderStopped. If ctx ends while the reload runs, the reload is abandoned: the error carries ctx.Err(), not ErrReload, and nothing is recorded. Callers that alert on breakage check ErrReload alone.
func (*Reloader[T]) Run ¶
Run runs the reload loop until Stop is called or ctx ends: the configured watchers — file watch, poll interval, signals — and Reloader.SourceChanged requests. It returns nil after Stop (immediately if already stopped) and ctx.Err() when ctx ends. Typically called in a goroutine: go reloader.Run(ctx), after Reloader.Arm when the caller needs to know the triggers armed. Without Arm, Run arms them and returns the error when it cannot.
Every automatic reload runs on Run's goroutine with a context that Stop also cancels, and the file-watch debounce timer is owned by the same loop, so no automatic reload can begin once Run has returned.
func (*Reloader[T]) Snapshot ¶
Snapshot returns the published config with its generation and the reload stats, all from one moment. Safe to call from any goroutine.
func (*Reloader[T]) SourceChanged ¶
func (r *Reloader[T]) SourceChanged()
SourceChanged requests a reload because a source changed that the reloader does not watch itself — a provider source, or anything else the caller learns about through its own events. It never blocks: requests made while one is pending coalesce into it, and the reload they trigger starts after the latest of them, so it reads every change made before the call. Like the watchers, it waits for a reload already in flight instead of being dropped.
Run serves the requests; one made before Run starts is served once it does, and requests after Stop are ignored. Use Reload instead when the caller needs the outcome.
func (*Reloader[T]) Stats ¶
func (r *Reloader[T]) Stats() ReloadStats
Stats returns this reloader's reload history. Safe to call from any goroutine; every field describes the same moment.
func (*Reloader[T]) Stop ¶
Stop stops the reloader. From the moment it is called, Reload returns ErrReloaderStopped, no new reload starts, an automatic reload in flight is cancelled, and Run returns.
Stop then waits for the reload in flight, if any, and for the release of triggers Arm set up that no Run took. When it returns nil both are done: no reload is running or will run, and no WithOnReloadSuccess or WithOnReloadError callback will fire, so it is safe to tear down whatever the callbacks touch. A manual Reload in flight runs to completion under its caller's context; if ctx ends first, Stop returns ctx's error and that guarantee is not yet given: the reloader finishes stopping in the background once the reload returns, and a later Stop call waits for it again.
Stop is idempotent and safe for concurrent use, but must not be called from a WithOnReloadSuccess or WithOnReloadError callback: those run holding the slot Stop waits for.
type SecretPopulator ¶
SecretPopulator fills `secret`-tagged fields on the target struct, a pointer to the freshly decoded config. It is a function, not an interface, so a caller wires any secret backend without this package importing it.
func SecretsFrom ¶
SecretsFrom returns a SecretPopulator that sets every field tagged secret:"<ref>" to what fetch returns for ref, decoded into the field's type as a source value is decoded — a string, an int, a duration:
type DBConfig struct {
Host string `koanf:"host" default:"localhost"`
Password string `koanf:"password" secret:"prod/db#password"`
User string `koanf:"user" secret:"prod/db#username"`
APIKey string `koanf:"api_key" secret:"prod/api-key,optional"`
}
sm := secretsmanager.NewFromConfig(awsCfg)
cfg, err := config.Load[AppConfig](ctx, config.WithSecretPopulator(config.SecretsFrom(
func(ctx context.Context, ref string) (string, error) {
out, err := sm.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{SecretId: &ref})
var missing *types.ResourceNotFoundException
if errors.As(err, &missing) {
return "", fmt.Errorf("%w: %s", config.ErrSecretNotFound, ref)
}
if err != nil {
return "", err
}
return aws.ToString(out.SecretString), nil
})))
A tag is ref[#field][,optional]. With #field the secret is a JSON object and the field takes that member — a store that keeps a database's username and password in one secret is fetched once for both. With ,optional a fetch error wrapping ErrSecretNotFound, or a missing member, leaves the field as the sources set it, so a development file can supply what production keeps in the store. A tag of "-" names no secret.
The fetched value replaces whatever the sources set. fetch runs once per ref per load, with the load's context, in declaration order. Any other failure fails the load with ErrSecretProvider, naming the key and the ref and wrapping fetch's error; a value that does not decode into its field is reported by type, never quoted. A field tagged secret is sensitive in every output, like sensitive:"true". A field behind a nil pointer — a section no source configured — is left alone. A secret tag inside a map or list cannot name one ref for every entry, and fails the load.
type Segment ¶ added in v0.7.0
type Segment struct {
Kind SegmentKind
Name string
Index int
}
Segment is a field, map key, list index or wildcard. Name is used by SegmentField and SegmentKey; Index is used by SegmentIndex.
type SegmentKind ¶ added in v0.7.0
type SegmentKind uint8
SegmentKind identifies one step in a configuration key.
const ( SegmentField SegmentKind = SegmentKind(segField) // A struct field. SegmentKey SegmentKind = SegmentKind(segKey) // A quoted map key. SegmentIndex SegmentKind = SegmentKind(segIndex) // A list index. SegmentAny SegmentKind = SegmentKind(segAny) // Any map entry or list element: [*]. )
type Shadow ¶ added in v0.10.0
type Shadow struct {
// Key is the key in the package's key grammar, or the enclosing
// sensitive container when the keys inside it are withheld.
Key string `json:"key"`
// Source is the source whose input is shadowed. An environment variable
// that names a withheld key is reported by its prefix only.
Source Source `json:"source"`
// By is the source whose value the merge keeps for Key.
By Source `json:"by"`
}
Shadow is one shadowed input: Source sets Key, and a higher-precedence source, By, sets it too, so Source's value cannot take effect. It names sources, never values.
type ShadowedInputsError ¶ added in v0.10.0
type ShadowedInputsError struct {
// Type is the config type that was loaded.
Type reflect.Type `json:"-"`
// Inputs are the shadowed inputs, sorted by Key, then Source.
Inputs []Shadow `json:"inputs"`
}
ShadowedInputsError reports the inputs of a load that a higher-precedence source outranks (see WithShadowedInputs). A load fails with it only when a WithShadowedInputs handler returns it.
func (*ShadowedInputsError) Error ¶ added in v0.10.0
func (e *ShadowedInputsError) Error() string
type Snapshot ¶
type Snapshot[T any] struct { // Config is the published config: shared by every reader, immutable. Config *T `json:"-"` // Generation counts publications: 1 for the initial load, and one more // for each successful reload. Generation uint64 `json:"generation"` // UnknownKeys is the input accepted by the published load, or nil when clean. UnknownKeys *UnknownKeysError `json:"unknown_keys,omitempty"` // Stats is the reload history. Stats ReloadStats `json:"stats"` // Applying reports a published config whose appliers are still being // called: Generation is live, while Appliers and PendingRestart describe // what they owed before the calls. The snapshot published once they // return has it false. Applying bool `json:"applying,omitempty"` // Appliers is each [WithApplier] applier's account, in registration // order; empty without appliers. Appliers []ApplierStatus `json:"appliers,omitempty"` // Provenance maps each key that holds a value to where it came from: // the input source the merge kept and the stages that changed it. Only // [LoadSnapshot] and a reloader [WithProvenance] record it. Provenance map[string]Origin `json:"provenance,omitempty"` // Shadowed lists every input of the published load that a // higher-precedence source outranks: a key a source sets whose value the // merge takes from a source above it — a config file's key that an // environment variable or a typed flag also sets — sorted by key, then // source. Defaults and the layering inside the environment are designed // overrides and never listed (see [WithShadowedInputs]). [LoadSnapshot] // and every [NewReloader] load record it; [NewReloaderFunc] has no view // of its loader's sources and records nothing. Shadowed []Shadow `json:"shadowed,omitempty"` // PendingRestart lists, in walk order, what the published config // changes that the running process has not taken and will not before a // restart: a key no applier owns that differs from the first load, a // key an applier returned as a restart key that differs from what it // acknowledged, and every key of a structure that appeared, disappeared // or turned nil without one applier owning all of it. A key returns to // its old value, and it leaves the list. Empty without appliers unless // WithReloadTags enables the schema's restart declarations. PendingRestart []Change `json:"pending_restart,omitempty"` }
Snapshot is a Reloader's state at one moment: the published config, which publication it is, and the reload history up to it. Every field describes the same moment. It encodes to JSON without the config, for a status endpoint.
func LoadSnapshot ¶
LoadSnapshot runs Load and records where every key's value came from: the snapshot's Provenance holds an Origin for each key, and its Generation is 0. Recording costs a tree of the merged sources, a copy and a comparison of the decoded config around the secret populator and each post-load hook, and one walk for the rows. The snapshot's Shadowed lists the inputs a higher-precedence source outranks. Load records nothing, unless WithShadowedInputs asks it to find shadowed inputs.
type Source ¶
type Source struct {
Kind SourceKind `json:"kind"`
Name string `json:"name,omitempty"`
// Prefix is the [WithEnvPrefixes] prefix an environment variable was
// read under, without its trailing "_": "APP_WORKER" for
// APP_WORKER_DB_HOST. It is empty for a name an env tag lists, and for
// every other kind of source.
Prefix string `json:"prefix,omitempty"`
}
Source names what set a value: its kind, and a name — a file's path, a variable, a flag, a provider's position. It never holds a value.
type SourceKind ¶
type SourceKind string
SourceKind names the kind of source that set a value.
const ( SourceZero SourceKind = "zero" // no source set it: the type's zero value SourceTag SourceKind = "tag_default" // a default:"..." tag SourceCodeDefaults SourceKind = "code_defaults" // a Defaulter's SetDefaults SourceDefaults SourceKind = "defaults" // WithDefaults SourceBase SourceKind = "base_provider" // WithBaseProvider; Name is its position SourceFile SourceKind = "file" // a config file; Name is its path SourceEnv SourceKind = "environment" // Name is the variable, Prefix the prefix it was read under SourceOverride SourceKind = "override_provider" // WithOverrideProvider; Name is its position SourceFlag SourceKind = "flag" // a command-line flag, as an OriginProvider names it SourceSecrets SourceKind = "secrets" // the secret populator, as a transform SourcePostLoad SourceKind = "post_load" // a WithPostLoad hook, as a transform; Name is its position SourceMixed SourceKind = "mixed" // a value compared whole that more than one source set part of SourceUnknown SourceKind = "unknown" // decoding made the value: no source can be named )
Source kinds, lowest precedence first among the inputs.
type SourceOption ¶ added in v0.8.0
type SourceOption func(*providerSource)
SourceOption configures one base or override provider.
func WithInvalidEntries ¶ added in v0.8.0
func WithInvalidEntries(report func(context.Context, []KeyError) error) SourceOption
WithInvalidEntries withholds known entries that fail CheckKey, preserving lower-precedence values. report receives the sorted rejected keys after each read, including an empty slice when none were rejected; nil discards the report. Its error fails the load with ErrSource. Report errors omit values; their wrapped causes are intended only for trusted diagnostics.
Struct sections are checked field by field; collections and self-decoding values are atomic entries. Defaults must validate before any provider is read, so a broken baseline cannot be mistaken for bad stored entries. Unknown entries remain available to the normal unknown-input handler, including declared unknown children of a withheld collection. Read/parser errors and cancellation still fail the load. The final merged configuration must validate: cross-field failures are never resolved by choosing entries to discard.
type UnknownKey ¶ added in v0.2.0
type UnknownKey struct {
// Key names it, or the enclosing sensitive container when its contents
// must be withheld: in the package's key grammar for a key (server.prot,
// items[0].prot), by its name for a variable (APP_SERVER_PROT).
Key string `json:"key"`
// Suggestion is what Key most likely misspells, or "" when nothing is
// close: the field of the same struct fewest edits away (server.port), or
// the variable of a field (APP_SERVER_PORT).
Suggestion string `json:"suggestion,omitempty"`
}
UnknownKey is one key no field decodes, or one variable that names no key.
type UnknownKeysError ¶ added in v0.2.0
type UnknownKeysError struct {
// Type is the config type that was loaded.
Type reflect.Type `json:"-"`
// Keys are the keys no field decodes, sorted by Key.
Keys []UnknownKey `json:"keys,omitempty"`
// Variables are the environment variables under a WithEnvPrefixes prefix
// that name no key under any of them, sorted by Key, which holds the
// variable's name.
// Filled only for a WithUnknownKeys handler: by default they are ignored.
Variables []UnknownKey `json:"variables,omitempty"`
}
UnknownKeysError reports the input a load's sources gave that no field reads (see WithUnknownKeys). A load fails with it, wrapped together with ErrValidation, when sources set unknown keys and no handler accepts them; errors.As reaches it, so a caller can report the keys in its own words.
func (*UnknownKeysError) Error ¶ added in v0.2.0
func (e *UnknownKeysError) Error() string
type UsageBounds ¶ added in v0.8.0
type UsageBounds struct {
Target string `json:"target"`
Minimum json.Number `json:"minimum,omitempty"`
Maximum json.Number `json:"maximum,omitempty"`
ExclusiveMinimum bool `json:"exclusive_minimum,omitempty"`
ExclusiveMaximum bool `json:"exclusive_maximum,omitempty"`
ZeroAllowed bool `json:"zero_allowed,omitempty"`
}
UsageBounds describes one conjunction of translated bounds. Target is value, duration (integer nanoseconds), length (Unicode characters), items or properties. ZeroAllowed means this clause also admits the field's zero value, including nil for pointers and nullable containers. Other Bounds clauses still apply.
type UsageEntry ¶ added in v0.7.0
type UsageEntry struct {
EnvVars []string `json:"env_vars,omitempty"`
Allowed []string `json:"allowed,omitempty"`
Bounds []UsageBounds `json:"bounds,omitempty"`
Deprecated []string `json:"deprecated,omitempty"`
Withheld bool `json:"withheld,omitempty"`
Reload string `json:"reload"`
Field Field `json:"-"`
Key string `json:"key"`
Env string `json:"env,omitempty"` // the variable under the first prefix
AlsoEnv []string `json:"also_env,omitempty"` // the rest of EnvVars: under broader prefixes, then an env tag's
JSON bool `json:"json,omitempty"` // a variable sets it as JSON
Type string `json:"type"`
Default string `json:"default,omitempty"`
Example string `json:"example,omitempty"`
Required bool `json:"required,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
Description string `json:"description,omitempty"`
// contains filtered or unexported fields
}
UsageEntry is one key an operator can set, with safe printable metadata. Field retains the original tags for custom generators and is omitted from JSON.
func Usage ¶ added in v0.7.0
func Usage[T any](envPrefixes ...string) ([]UsageEntry, error)
Usage returns the catalog rendered by PrintUsage, in declaration order, naming each key's variables under envPrefixes as WithEnvPrefixes takes them: Env is the one under the first prefix, AlsoEnv the rest in precedence order, and EnvVars all of them. Defaults that are sensitive or could contain secrets are empty and Withheld is true. Allowed follows JSONSchema's enum and withholding rules. Collection entries use [*] patterns and environment placeholders where supported.
type Validatable ¶
type Validatable interface {
Validate() error
}
Validatable is a config with rules no tag can state: which keys go together, which settings exclude each other. When *T implements it, Load calls Validate after the validator passes, on the config the post-load hooks produced; its error fails the load with ErrValidation and is kept as the cause, its text in the message — keep values out of it. Validate must not change the config (normalize in a WithPostLoad hook) and does no I/O. WithSkipValidation skips it; a WithValidator validator does not replace it.
type Validator ¶
Validator validates the decoded config. A go-playground *validator.Validate satisfies it, as does any wrapper with a Struct method. A validator that also has StructCtx(ctx, v) error is called through that instead, with the load's ctx.
type Value ¶
type Value struct {
// Present is false when the key does not exist on this side: an entry
// not in the map, an element past the list's end, a field beneath a nil
// struct pointer.
Present bool `json:"present"`
// Nil reports a nil pointer, map, slice or interface.
Nil bool `json:"nil,omitempty"`
// Type is the value's Go type, with struct tags omitted: the dynamic
// type for an interface.
Type string `json:"type,omitempty"`
// Text is the value's %v text. It is empty for a structural change, and
// "[REDACTED]" when withheld whole, or text with recognized credentials
// replaced when Redacted reports partial shape-based redaction.
Text string `json:"text,omitempty"`
// Redacted reports a value PrintRedacted would redact.
Redacted bool `json:"redacted,omitempty"`
}
Value is one side of a Change: whether a value is there, and its text when it may be shown.
Source Files
¶
- applier.go
- check_key.go
- config.go
- constraints.go
- decode_errors.go
- decode_input.go
- deepcopy.go
- defaults.go
- diff.go
- doc.go
- element_defaults.go
- env.go
- env_refs.go
- envout.go
- errors.go
- explain.go
- fields.go
- json.go
- jsonschema.go
- key.go
- keyof.go
- keys.go
- native.go
- options.go
- path.go
- print.go
- provenance.go
- provider_func.go
- redact.go
- reload_options.go
- reload_tags.go
- reloader.go
- render.go
- schema.go
- secrets.go
- shadow.go
- snapshot.go
- source_validation.go
- template.go
- text_values.go
- type_text.go
- usage.go
- view.go
- watch.go