fastconf

package module
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: May 20, 2026 License: MIT Imports: 26 Imported by: 0

README

FastConf — strongly typed, lock-free, Kustomize-style configuration for Go

Language: English · 中文

fastconf layers YAML / JSON / TOML files, environment variables, CLI flags, remote KV stores, and on-the-fly generators into a single strongly typed Go struct. A single-writer reload loop publishes new snapshots atomically via atomic.Pointer; the hot read path is one atomic.Pointer.Load().

Go Reference CI Release

Status: first-public. The API still moves where semantics demand it. pkg.go.dev and this README track the current truth of the codebase.


Table of contents

  1. Quick start
  2. Why FastConf
  3. Installation
  4. Core model
  5. Manager API
  6. Options reference
  7. Reload pipeline
  8. Profiles & overlays
  9. Provider system
  10. Transformers & migration
  11. Watch, Subscribe, and Plan
  12. Provenance, history & rollback
  13. Observability
  14. Multi-tenant & presets
  15. Sub-module ecosystem
  16. CLI tools
  17. Performance
  18. Development
  19. License

Quick start

package main

import (
    "context"
    "log"

    "github.com/fastabc/fastconf"
    "github.com/fastabc/fastconf/pkg/provider"
)

type AppConfig struct {
    Server struct {
        Addr string `json:"addr" yaml:"addr"`
    } `json:"server" yaml:"server"`
    Database struct {
        DSN  string `json:"dsn"  yaml:"dsn"`
        Pool int    `json:"pool" yaml:"pool"`
    } `json:"database" yaml:"database"`
}

func main() {
    mgr, err := fastconf.New[AppConfig](context.Background(),
        fastconf.WithDir("conf.d"),
        fastconf.WithProfile(fastconf.ProfileOptions{
            EnvVar:  "APP_PROFILE",
            Default: "dev",
        }),
        fastconf.WithProvider(provider.NewEnv("APP_")),
        fastconf.WithWatch(fastconf.WatchOptions{Enabled: true}),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer mgr.Close()

    cfg := mgr.Get() // *AppConfig — lock-free, O(1), zero-alloc
    log.Println(cfg.Server.Addr, cfg.Database.Pool)
}

Directory layout:

conf.d/
  base/
    00-app.yaml
  overlays/
    prod/
      50-overrides.yaml
      _patch.json
# conf.d/base/00-app.yaml
server:
  addr: ":8080"
database:
  dsn: "postgres://localhost/app"
  pool: 10

Run with an environment override:

APP_PROFILE=prod APP_DATABASE_POOL=20 go run .

APP_DATABASE_POOL=20 maps to database.pool (single _ separator, Viper/Spring Boot style). With APP_PROFILE=prod, FastConf merges base/* first, then overlays/prod/*.

Recommended entry points
Scenario Recommended combo Read more Runnable example
Local file config New + WithDir + Get Quickstart examples/basic
Kubernetes hot-reload PresetK8s + Subscribe + Errors k8s cookbook examples/sidecar
Remote source / GitOps WithProvider + Plan + Provenance Vault / Consul examples/external_source

Why FastConf

  • Strong typing on the read path. mgr.Get().Server.Addr is checked by the compiler. No dotted-path strings, no reflection, no interface{}.
  • Lock-free hot reads. Get() is an atomic.Pointer.Load() — O(1), zero-alloc, safe from any number of goroutines.
  • Fail-safe reload. Any pipeline stage that errors out keeps the old *State[T] live; a broken config never reaches your read path.
  • Kustomize-style layering. base / overlays, RFC 6902 patches, and strategic merge for lists of objects.
  • Opt-in extensions. Providers, transformers, secret resolvers, validators, policies, metrics, and tracing are all optional.

Installation

go get github.com/fastabc/fastconf@latest

# Optional sub-modules:
go get github.com/fastabc/fastconf/observability/otel@latest
go get github.com/fastabc/fastconf/observability/metrics/prometheus@latest
go get github.com/fastabc/fastconf/cue@latest
go get github.com/fastabc/fastconf/policy/opa@latest
go get github.com/fastabc/fastconf/providers/s3@latest

Command-line tools (Go ≥ 1.22):

go install github.com/fastabc/fastconf/cmd/fastconfd@latest
go install github.com/fastabc/fastconf/cmd/fastconfctl@latest
go install github.com/fastabc/fastconf/cmd/fastconfgen@latest
Compatibility
Item Supported
Go toolchain 1.22, 1.23, 1.24, 1.25, 1.26 (no toolchain pin in go.mod)
OS / arch linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 (binaries published on each tag)
Module form one root module + independent sub-modules (cue, policy/opa, validate/playground, observability/{otel,metrics/prometheus}, providers/s3, integrations/{cli/pflag,log/phuslu,log/zerolog}, cmd/{fastconfctl,fastconfd,fastconfgen})
Pre-release contract semantic-version tags follow vMAJOR.MINOR.PATCH. The current line (v0.18) is the first public release and the rename / bucketed-Options boundary is locked in — see migration-v0.18.md.
Versioning
  • Tags follow vMAJOR.MINOR.PATCH. The root module and every sub-module receive the same tag through tools/tag-release.sh vX.Y.Z.
  • Major-version 0 is reserved for the pre-1.0 cycle. Breaking changes may still land between minor versions until v1.0, but each release ships with an explicit migration recipe under docs/cookbook/ so the call-site delta is mechanical.
  • The internal package set under internal/* is implementation detail and not covered by the SemVer contract — root re-exports (type aliases or wrappers) are the only stable surface.
  • The reusable primitives under pkg/* keep a unidirectional dependency shape (see the whitelist in CLAUDE.md); tools/check-deps.sh statically enforces it in CI so consumers can pull in a single pkg/* subpackage without dragging in hidden lateral dependencies.
  • When sub-modules tag independently the tag is module-path-prefixed (e.g. cue/vX.Y.Z); the README mostly hides this because a single release pushes the same version across the root and every sub-module.
  • Before each release we run make test plus seven guard scripts under tools/{check-layout,check-deps,check-doc-symbols,audit-phase-comments, check-cjk-comments,loc-budget,total-loc-budget}.sh, so directory layout, dependency direction, public symbols, comment archaeology and LOC budgets are all enforced before a tag is pushed.

Core model

sources / generators / providers
              │
              ▼
       assemble preflight
              │
              ▼
 merge → migration → transform → secret → typed-hooks
      → decode → field-meta → validate → policy
              │
      fail ───┴─── keep old State[T]
              │
           success
              ▼
 canonical hash → atomic swap → history → audit → subscribers
Property What it means
Typed read path mgr.Get().Server.Addr, checked by the compiler
Single-writer reload fsnotify, provider events, and manual Reload all serialize through one writer
Fail-safe Any stage error keeps the old *State[T]; bad config never reaches business code
Kustomize-style layering base / overlay, RFC 6902 patches, strategic merge with mergeKeys
Opt-in extensions providers, transformers, secret resolvers, policies, metrics, tracer

Manager API

// Construction (first reload runs synchronously)
func New[T any](ctx context.Context, opts ...Option) (*Manager[T], error)

// Read path — lock-free, O(1), zero-alloc
func (m *Manager[T]) Get() *T

// Trigger a reload; ctx controls the full pipeline.
func (m *Manager[T]) Reload(ctx context.Context, opts ...ReloadOption) error

// Dry-run — never updates the live pointer
func (m *Manager[T]) Plan() *PlanBuilder[T]

// Current snapshot (State[T] + Sources + Origins)
func (m *Manager[T]) Snapshot() *State[T]

// Async failure stream — buffered 16, drop-on-full, closed by Close()
func (m *Manager[T]) Errors() <-chan ReloadError

func (m *Manager[T]) Watcher() *Watcher[T]  // .Pause() / .Resume()
func (m *Manager[T]) Replay()  *Replay[T]   // .List() / .Rollback(*State[T])
func (m *Manager[T]) Close() error

Package-level generics:

// Per-field subscribe; fires on every successful reload.
func Subscribe[T, M any](m *Manager[T], extract func(*T) *M, fn func(old, new *M)) (cancel func())

// Typed feature-flag evaluation.
func Eval[T, V any](m *Manager[T], key string, ctx feature.EvalContext, def V) V

State[T] carries Value *T, Hash [32]byte, Generation uint64, Sources []SourceRef, and provenance helpers (Explain, Diff, Redacted).


Options reference

All WithXxx options return Option and may be composed in any order. The full reference is in docs/readme/02-core-model.md.

Key options
Option Purpose Default
WithDir(dir) Config root directory "conf.d"
WithFS(fs.FS) Alternate fs.FS (testing)
WithWatch(WatchOptions{...}) Enable fsnotify; bundles Enabled / Paths / Coalesce / CoalesceProfile Enabled:false
WithProfile(ProfileOptions{...}) Profile selection bundle: Single, Multi, Expr, EnvVar, Default
WithCoalesce(CoalesceOptions{...}) Tune watcher Quiet / MaxLag / SwapHint independently of WithWatch
WithProvider(p) Register a structured provider
WithSource(src, parser) Byte-blob source + parser
WithMigrations(fn) Schema migration callback
WithTransformers(t...) Post-merge transform chain
WithSecretResolver(r) Decrypt leaves before decode
WithValidator[T](fn) Typed validation after decode
WithPolicy[T](p) Policy evaluation after validate
WithHistory(n) Keep last n successful states
WithProvenance(level) Off / TopLevel / Full Off
WithMetrics(sink) Metrics sink
WithAuditSink(sink) Audit on each successful reload
WithTracer(tracer) OTel-compatible tracer
WithLogger(*slog.Logger) Inject a logger io.Discard
WithStructDefaults[T]() Populate zero values via fastconf:"default=…" tags

Reload pipeline

Stage sequence
reloadCh.recv(req)
  ├─ stageMerge:      discovery.Scan(dir) → decode files → merger.Merge(layers)
  │                   apply _meta.yaml (appendSlices / profileEnv / match)
  │                   apply _patch.json (RFC 6902)
  ├─ stageAssemble:   for each provider: Load(ctx) → merge by Priority
  ├─ stageMigrate:    opts.migrationRun(merged)
  ├─ stageTransform:  for each transformer: t.Transform(merged)
  ├─ stageDecode:     json.Marshal(merged) → json.Unmarshal(→ *T)
  ├─ stageFieldMeta:  range / enum / required checks
  ├─ stageValidate:   for each validator: v(*T)
  ├─ stagePolicy:     for each policy: p.Evaluate(ctx, *T, reason, tenant)
  └─ commit:
       canonical SHA-256 dedup
       atomic.Pointer.Store(newState) → history → audit → subscribers

When any stage errors: atomic.Pointer is not updated, Generation is not incremented, the error surfaces on Errors(), no AuditSink fires.


Profiles & overlays

conf.d/
  base/                     # applied for every profile
    00-defaults.yaml
  overlays/
    prod/
      50-prod.yaml
      _meta.yaml            # profile match expression
      _patch.json           # RFC 6902 patch
_meta.yaml
schemaVersion: "1"
profileEnv: "APP_PROFILE"
defaultProfile: "dev"
appendSlices: true
match: "prod | staging"     # &, |, !, () supported
RFC 6902 JSON Patch
[
  { "op": "replace", "path": "/server/addr",      "value": ":8443" },
  { "op": "add",     "path": "/feature/darkMode", "value": true    },
  { "op": "remove",  "path": "/legacy/key"                         }
]

Multi-profile mode: WithProfile(ProfileOptions{Multi: []string{"prod", "eu-west", "canary"}}) — each overlay's _meta.yaml.match decides whether it applies.


Provider system

Built-in structured providers (pkg/provider)
Provider Constructor Notes
Env provider.NewEnv("APP_") APP_FOO_BARfoo.bar; chain .WithReplacer, .At, .WithCoerce
CLI provider.NewCLI(map) Pass only explicitly changed flags; files/env stay authoritative
DotEnv provider.NewDotEnv("APP_", paths...) .env fallback; process env wins
Labels provider.NewDottedLabels(labels, opts) / NewRoutingLabels(labels, opts) Config and routing DSL labels
K8s Downward k8s.NewDefault() /etc/podinfo/{labels,annotations}

First-party KV providers (root module, trim via build tag):

vp, _ := vault.New("https://vault.svc", "kv/data/myapp", os.Getenv("VAULT_TOKEN"))
cp, _ := consul.New("http://consul.svc:8500", "config/myapp")
hp, _ := httpprov.New("remote", "https://example.com/cfg.yaml", yamlCodec{})
// Build tag to exclude: -tags no_provider_vault,no_provider_consul,no_provider_http

Sub-module providers (go get as needed): S3 (providers/s3), NATS (providers/nats), Redis Streams (providers/redisstream).

Priority constants

Merge order follows Priority() ascending — higher values overwrite lower:

Constant Value Use
PriorityDotEnv 5 .env fallback
PriorityStatic 10 Static / file layers
PriorityKV 30 Vault / Consul / HTTP / S3
PriorityK8s 40 Kubernetes ConfigMap / Secret
PriorityEnv 50 Process environment variables
PriorityCLI 60 Command-line flags (highest)

Use WithProviderOrdered(p1, p2, p3) to auto-assign priorities in call order.

contracts.Provider interface
type Provider interface {
    Name()     string
    Priority() int
    Load(ctx context.Context) (map[string]any, error)
    Watch(ctx context.Context) (<-chan Event, error)
}

Transformers & migration

Built-in transformers (pkg/transform)
fastconf.WithTransformers(
    transform.Defaults(map[string]any{"server": map[string]any{"timeout": "30s"}}),
    transform.SetIfAbsent("server.timeout", "30s"),
    transform.EnvSubst(),                           // ${VAR} / ${VAR:-default}
    transform.DeletePaths("internal.debug"),
    transform.Aliases(map[string]string{"db.url": "database.dsn"}),
)
Struct tags
type AppConfig struct {
    Server struct {
        Addr    string        `json:"addr"    fastconf:"default=:8080"`
        Timeout time.Duration `json:"timeout" fastconf:"default=30s"`
    } `json:"server"`
    Database struct {
        DSN string `json:"dsn" fastconf:"secret"` // redacted in logs/snapshots
    } `json:"database"`
}
Migration
fastconf.WithMigrations(func(root map[string]any) error {
    if v, ok := root["db_url"]; ok {
        db, _ := root["database"].(map[string]any)
        if db == nil { db = map[string]any{}; root["database"] = db }
        if _, has := db["dsn"]; !has { db["dsn"] = v }
        delete(root, "db_url")
    }
    return nil
})

For multi-step schema migrations use pkg/migration.NewChain.


Watch, Subscribe, and Plan

Field-level Subscribe
cancel := fastconf.Subscribe(mgr,
    func(app *AppConfig) *DatabaseConfig { return &app.Database },
    func(old, neu *DatabaseConfig) {
        reconnect(neu.DSN)
    },
)
defer cancel()
Manual reload with one-shot override
err := mgr.Reload(ctx,
    fastconf.WithReloadReason("admin-cli"),
    fastconf.WithSourceOverride(map[string]any{
        "server": map[string]any{"addr": ":9999"},
    }),
)
Plan (dry-run)
result, err := mgr.Plan().WithHostname("ci-runner-7").Run(ctx)
// result.Validators — validation errors
// result.Policies   — policy violations (SeverityError downgraded to warning in dry-run)
Pause / Resume
mgr.Watcher().Pause()
applyBatchUpdate()
mgr.Watcher().Resume()

Provenance, history & rollback

Provenance
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithProvenance(fastconf.ProvenanceFull),
)

origins := mgr.Snapshot().Explain("server.addr")
// each Origin: Source.Name, Source.Priority, Value
Level Cost What you can trace
ProvenanceOff zero nothing
ProvenanceTopLevel O(top-level keys) which layer set each top-level field
ProvenanceFull O(leaves) full override chain per leaf
History & rollback
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithHistory(10),
)
history := mgr.Replay().List()     // []*State[T], oldest → newest
_ = mgr.Replay().Rollback(history[len(history)-2])
Errors stream
go func() {
    for re := range mgr.Errors() {
        slog.Error("reload failed", "reason", re.Reason, "err", re.Err)
    }
}()

Observability

// JSON-lines audit on each successful reload
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithAuditSink(fastconf.NewJSONAuditSink(os.Stderr)),
    fastconf.WithDiffReporter(fastconf.DiffReporterFunc(
        func(ctx context.Context, ev fastconf.DiffEvent) error {
            return slack.Post(ctx, ev.Diff) // async, never blocks reload
        },
    )),
)

Prometheus metrics and OpenTelemetry tracing live in sub-modules:

import prommetrics "github.com/fastabc/fastconf/observability/metrics/prometheus"
import fastconfotel "github.com/fastabc/fastconf/observability/otel"

fastconf.WithMetrics(prommetrics.New())
fastconf.WithTracer(fastconfotel.NewTracer(otel.GetTracerProvider()))

Policy violations abort reload at SeverityError; SeverityWarning logs and continues. CUE and OPA implementations in cue/policy and policy/opa.


Multi-tenant & presets

// Multi-tenant: each tenant is a fully isolated Manager[T]
tm := fastconf.NewTenantManager[AppConfig]()
mgrA, _ := tm.Add(ctx, "tenant-a", fastconf.WithDir("/etc/config/tenant-a"))
app, err := tm.Get("tenant-a")  // fastconf.ErrUnknownTenant if absent
tm.Close()
// Presets
fastconf.PresetK8s(fastconf.K8sOpts{Dir: "/etc/config", Watch: true})
fastconf.PresetSidecar(fastconf.SidecarOpts{Dir: "/etc/fastconfd", HistoryN: 16})
fastconf.PresetTesting(fastconf.TestingOpts{FS: memFS, Profile: "testing"})

Sub-module ecosystem

Shipped with the root module
Package Path
contracts contracts — public interfaces
reusable primitives pkg/{decoder,discovery,feature,flog,generator,merger,migration,provider,transform,validate}
http / vault / consul providers/{http,vault,consul} — build tags: no_provider_{http,vault,consul}
policy policyFunc adapter
sidecar service cmd/fastconfd
Independent sub-modules (go get as needed)
Sub-module Path Primary dependency
validate/playground validate/playground go-playground/validator
prometheus observability/metrics/prometheus prometheus/client_golang
otel observability/otel OpenTelemetry SDK
cue (validation + policy) cue cuelang.org/go
opa-policy policy/opa open-policy-agent/opa
cli/pflag integrations/cli/pflag spf13/pflag
nats provider providers/nats root module (inject nats.Conn)
redis-streams provider providers/redisstream root module (inject redis client)
s3 provider providers/s3 AWS SDK v2
openfeature integrations/openfeature root module
fastconfctl cmd/fastconfctl root module
fastconfgen cmd/fastconfgen yaml.v3

Tag all sub-modules at once: ./tools/tag-release.sh vX.Y.Z [--push]


CLI tools

fastconfd — sidecar service
fastconfd --dir=/etc/config --profile=prod --addr=:8081
Endpoint Method Description
/healthz GET {"status":"ok","generation":N}
/config GET Current config JSON (secrets redacted)
/reload POST Trigger a manual reload
/events GET SSE stream of ReloadCause on each successful reload
fastconfctl — admin CLI
fastconfctl snapshot --addr=:8081
fastconfctl reload   --addr=:8081 --request-id=deploy-123
fastconfctl rollback --addr=:8081 --generation=42
fastconfgen — code generator
fastconfgen generate --input=conf.d/base/00-app.yaml --pkg=config --out=config/config_gen.go

Performance

Most recent benchmark run: Apple M2 / darwin-arm64 / Go 1.26.2.

Benchmark median
BenchmarkGet 0.52 ns/op
BenchmarkReloadNoop 15.1 µs/op
BenchmarkReloadCommitSmall 16.5 µs/op
BenchmarkReloadManySubscribers/50 17.5 µs/op

Full baseline: docs/design/perf.md.


Development

go mod tidy
make build
make test        # go test -race -count=1 ./...
make test-all    # includes sub-modules
make lint        # requires golangci-lint

go test ./... -run '^Example' -v
go test -bench=BenchmarkGet -benchmem ./...

Documentation

Doc Purpose
docs/readme/ In-depth chapters: core model, pipeline, extensions, operations
docs/cookbook/README.md Ready recipes ordered by user journey
docs/design/spec.md Runtime model, concurrency, module boundaries
docs/cookbook/migration-v0.18.md v0.18 rename / bucketed-Options migration table
GitHub Releases Release notes and prebuilt CLI binaries
pkg.go.dev godoc and runnable examples

Common recipes: k8s · vault · consul · secrets · features · policy · otel · tenant · sidecar · plan


License

MIT License, See LICENSE.

Copyright (c) 2026 FastAbc

Documentation

Overview

Package fastconf provides a strongly typed, lock-free, Kustomize-style configuration loader built for Go 1.22+.

Start here

A typical application reads FastConf in this order:

  • Build a Manager[T] with New (or MustNew for one-line main / init).
  • Read the live typed snapshot with Manager.Get.
  • React to successful commits with Subscribe and failed reloads with Manager.Errors.
  • Preview a future commit with Manager.Plan before calling Manager.Reload.
  • Inspect provenance through Manager.Snapshot and recover retained states through Manager.Replay when WithHistory was enabled.

The package examples mirror that path: ExampleNew, ExampleMustNew, ExampleSubscribe, ExampleManager_Errors, ExampleManager_Plan, and ExampleReplay_Rollback.

Core ideas

  • Manager[T] takes the business config struct T as a type parameter; the hot read path returns *T with no reflection or allocations.
  • State[T] is published through atomic.Pointer: one serialized writer, many lock-free readers.
  • A reload first assembles file, generator, and provider layers, then runs the canonical stages Merge → Migration → Transform → Secret → TypedHooks → Decode → FieldMeta → Validate → Policy before atomically publishing. Any failure preserves the previous *State[T].

Reading by need

  • Constructors: New, MustNew.
  • Preset bundles: PresetK8s, PresetSidecar, PresetTesting, PresetHierarchical.
  • Loading and overlays: Option, WithProvider, WithProfile, WithWatch, WithCoalesce, WithMultiAxisOverlays, WithDir, WithFS.
  • Runtime reaction: Subscribe, Manager.Errors, Manager.Watcher, DiffReporter.
  • Inspection and recovery: Manager.Snapshot, State.Introspect, State.Explain, State.Dump, Manager.Plan, Manager.Replay.
  • Extension points: Transformer, WithTypedHook, WithSecretResolver, WithValidator, WithPolicy, AuditSink, MetricsSink, Tracer.

Module layout

The main API package lives at the repository root (github.com/fastabc/fastconf). Independent modules with their own go.mod files are:

cmd/fastconfctl, cmd/fastconfgen
cue (unified: cue/cuelang + cue/policy)
integrations/cli/pflag, integrations/log/phuslu, integrations/log/zerolog
observability/metrics/prometheus, observability/otel
policy/opa
providers/s3
validate/playground

Subpackages that share the root module version include: contracts, integrations/{bus,openfeature,render}, providers/{consul,http,nats,redisstream,vault,k8s}, providers/s3/s3events, pkg/*, policy/ (root), cmd/fastconfd, and cmd/internal/cli.

Key files

manager.go   — Manager[T] facade + New + Subscribe + Eval
state.go     — State[T], ReloadCause, Origins/Explain/Lookup facades
options.go   — WithXxx option builders
aliases.go   — codec, secret, field-meta, and replay public facades
errors.go    — public sentinel errors and ReloadError stream
obs.go       — metrics, tracer, audit-sink facades
defaults.go  — WithStructDefaults + DefaulterFunc
feature.go   — FeatureRule extraction + Eval[T,V]
presets.go   — PresetK8s, PresetSidecar, PresetTesting
registry.go  — RegisterProviderFactory + WithProviderByName
bind.go      — WithSource content-type helpers
doc.go       — package-level godoc

Index

Examples

Constants

View Source
const (
	// DefaultDir is the configuration root directory used when WithDir is
	// not supplied. It follows the conf.d convention from /etc/conf.d.
	DefaultDir = iopts.DefaultDir

	// DefaultProfileEnv is the environment variable FastConf reads when
	// neither WithProfile(ProfileOptions{Single}) nor
	// WithProfile(ProfileOptions{EnvVar}) sets one explicitly.
	DefaultProfileEnv = iopts.DefaultProfileEnv
)

Default configuration values. These constants define the out-of-the-box behaviour of FastConf. All WithXxx options override these values on a per-Manager basis. See the individual option docs for semantics.

View Source
const (
	DefaultCoalesceQuiet    = iopts.DefaultCoalesceQuiet
	DefaultCoalesceMaxLag   = iopts.DefaultCoalesceMaxLag
	DefaultCoalesceSwapHint = iopts.DefaultCoalesceSwapHint
)

Default coalescer windows for the file-system watcher. Events on a single watched parent directory are collapsed into a single reload using these timings. See the internal/coalesce package; runtime overrides go through WithCoalesce(CoalesceOptions{...}).

View Source
const (
	ProfileK8s      = coalesce.ProfileK8s
	ProfileLocalDev = coalesce.ProfileLocalDev
)

CoalesceProfile values mirroring the internal/coalesce constants.

View Source
const (
	// DumpYAML emits deterministic YAML with map keys sorted
	// lexicographically. Two snapshots whose merged values are equal
	// produce byte-identical output, so YAML diffs do not flake.
	DumpYAML = istate.DumpYAML
	// DumpJSON emits indented JSON (two-space indent). Use when piping
	// to jq or another structured-data tool.
	DumpJSON = istate.DumpJSON
	// DumpTOML emits canonical TOML via BurntSushi/toml. Top-level
	// values must be representable as TOML — strings, numbers, bools,
	// tables, and arrays — or the encoder returns an error.
	DumpTOML = istate.DumpTOML
)
View Source
const (
	LayerUnknown   = istate.LayerUnknown
	LayerMerge     = istate.LayerMerge
	LayerPatch     = istate.LayerPatch
	LayerProvider  = istate.LayerProvider
	LayerSecret    = istate.LayerSecret
	LayerGenerator = istate.LayerGenerator
)
View Source
const (
	ProvenanceOff      = provenance.Off
	ProvenanceTopLevel = provenance.TopLevel
	ProvenanceFull     = provenance.Full
)
View Source
const (
	DiffAdded    = istate.DiffAdded
	DiffRemoved  = istate.DiffRemoved
	DiffModified = istate.DiffModified
)
View Source
const DefaultSidecarHistoryCap = iopts.DefaultSidecarHistoryCap

DefaultSidecarHistoryCap is the history ring capacity used by PresetSidecar when SidecarOpts.HistoryN is not set.

Variables

View Source
var (
	ErrNoSources  = fcerr.ErrNoSources
	ErrValidation = fcerr.ErrValidation
	ErrDecode     = fcerr.ErrDecode
	ErrMerge      = fcerr.ErrMerge
	ErrPatch      = fcerr.ErrPatch
	ErrClosed     = fcerr.ErrClosed
	ErrValidator  = fcerr.ErrValidator
	ErrTransform  = fcerr.ErrTransform
	ErrNoOrigin   = fcerr.ErrNoOrigin
)
View Source
var ErrFastConf = fcerr.ErrFastConf
View Source
var ErrHistoryDisabled = imanager.ErrHistoryDisabled
View Source
var ErrParserUnknown = errors.New("fastconf: no parser for source content-type")

ErrParserUnknown is returned by a bound Source/Parser composite when no Parser was supplied to Bind and the Source's content-type hint did not match any registered Parser. The error is observed at the first Load() call, not at Bind time, because content-types may be runtime-discovered (e.g. HTTP Content-Type header).

View Source
var ErrPolicyDenied = fcerr.ErrPolicyDenied
View Source
var ErrTenantExists = itenant.ErrTenantExists
View Source
var ErrUnknownGeneration = imanager.ErrUnknownGeneration
View Source
var ErrUnknownTenant = itenant.ErrUnknownTenant

Functions

func Bind added in v0.16.0

Bind composes a byte-stream Source with a Parser into a contracts.Provider that the reload pipeline can consume. The returned Provider forwards Name/Priority/Watch to the Source and runs Source.Read + Parser.Decode on Load.

If parser is nil, the framework attempts to resolve a Parser from the parser registry using the content-type hint returned by Source.Read. The lookup is deferred to Load() so that Sources whose content-type is only known at runtime (HTTP Content-Type response header, magic-byte detection, ...) work without ceremony. If neither an explicit Parser nor a registry match resolves a Parser by Load time, Load returns ErrParserUnknown.

func DefaultSecretRedactor

func DefaultSecretRedactor(path string, value any) any

func Eval

func Eval[T any, V any](m *Manager[T], key string, ctx feature.EvalContext, def V) V

func Extract added in v0.18.0

func Extract[T any, M any](s *State[T], extract func(*T) *M) *M

Extract returns the sub-tree of s.Value selected by extract. It is the one-shot, type-safe counterpart to Subscribe:

  • Extract is a synchronous view of the current snapshot.
  • Subscribe streams (oldView, newView) pairs to a callback after every commit and returns a cancel func.

Extract is nil-safe: when any of s, s.Value, or extract is nil it returns the zero value of *M (nil) without invoking the extractor.

dbView := fastconf.Extract(mgr.Snapshot(), func(c *Cfg) *DBSection {
    return &c.Database
})

func FormatDiff added in v0.18.0

func FormatDiff(entries []DiffEntry) []string

FormatDiff renders a DiffEntry sequence as the human-readable line list earlier FastConf revisions returned from State.Diff. The output format is intended for operator-facing surfaces (logs, fastconfctl, PR-bot summaries) and is not covered by the SemVer contract — machine consumers should walk DiffEntry fields directly.

func LookupCodec

func LookupCodec(name string) (contracts.Codec, bool)

func RegisterCodec

func RegisterCodec(name string, c contracts.Codec)

func RegisterCodecExt

func RegisterCodecExt(ext, codec string)

func RegisterProviderFactory

func RegisterProviderFactory(name string, f ProviderFactory)

func RegisteredProviderNames

func RegisteredProviderNames() []string

func SourcePriorityBand added in v0.18.0

func SourcePriorityBand(s SourceRef) string

SourcePriorityBand translates a SourceRef's framework-internal Priority into a human-readable band label suitable for audit sinks, fastconfctl explain, or other operator-facing surfaces.

The returned label has one of these shapes:

"file:base"            base file layer        (1000–1999)
"file:overlay:<prof>"  single/multi-axis      (2000–3999)
"generator:<name>"     generator              (7000 + RawLayer.Priority; default 7070)
"provider:<name>"      provider               (8000 + Provider.Priority())
"unknown:<n>"          any value not in a known band

func Subscribe

func Subscribe[T any, M any](m *Manager[T], extract func(*T) *M, fn func(old, new *M)) (cancel func())
Example

ExampleSubscribe demonstrates reacting to a typed subtree after a successful commit while keeping the caller in charge of what counts as "changed".

package main

import (
	"context"
	"fmt"
	"testing/fstest"

	"github.com/fastabc/fastconf"
)

type apiExampleConfig struct {
	Server struct {
		Addr string `json:"addr" yaml:"addr"`
	} `json:"server" yaml:"server"`
}

func main() {
	mgr, err := fastconf.New[apiExampleConfig](context.Background(),
		fastconf.PresetTesting(fastconf.TestingOpts{
			FS: fstest.MapFS{
				"conf.d/base/00-app.yaml": &fstest.MapFile{
					Data: []byte("server:\n  addr: \":8080\"\n"),
				},
			},
		}),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	cancel := fastconf.Subscribe(mgr,
		func(c *apiExampleConfig) *string { return &c.Server.Addr },
		func(old, next *string) {
			if old != nil && next != nil && *old != *next {
				fmt.Printf("%s -> %s\n", *old, *next)
			}
		},
	)
	defer cancel()

	_ = mgr.Reload(context.Background(), fastconf.WithSourceOverride(map[string]any{
		"server": map[string]any{"addr": ":9090"},
	}))
}
Output:
:8080 -> :9090

Types

type AuditSink

type AuditSink = iobs.AuditSink

type AuditSinkFunc

type AuditSinkFunc = iobs.AuditSinkFunc

type CoalesceOptions added in v0.18.0

type CoalesceOptions struct {
	// Quiet is the no-event silence window after which a burst of
	// fsnotify events is delivered as a single reload.
	Quiet time.Duration
	// MaxLag is the upper bound on how long a reload may be deferred
	// regardless of Quiet — protects against pathological streams.
	MaxLag time.Duration
	// SwapHint accelerates the ConfigMap atomic-rename detection so
	// Kubernetes deployments do not need to wait the full Quiet window
	// to publish.
	SwapHint time.Duration
}

CoalesceOptions tunes the file-watcher event coalescer. All three fields are optional — zero means "use the default for that field". See DefaultCoalesceQuiet / DefaultCoalesceMaxLag / DefaultCoalesceSwapHint.

type CoalesceProfile added in v0.16.0

type CoalesceProfile = coalesce.Profile

CoalesceProfile is the public re-export of the coalescer preset selector. Use ProfileK8s in production and ProfileLocalDev when iterating against editors that write via unlink-rename cascades.

type CodecBridge added in v0.18.0

type CodecBridge uint8

CodecBridge selects the bytes-to-struct decoder used in the typed pipeline stage. BridgeJSON (default) round-trips through encoding/json so canonical-hash caching can reuse the marshalled bytes; BridgeYAML honours yaml struct tags directly. See WithCodecBridge for the user trap when *T has only yaml tags.

const (
	BridgeJSON CodecBridge = iota
	BridgeYAML
)

type Defaulter

type Defaulter interface {
	Defaults()
}

Defaulter is an optional interface for strongly-typed config structs. When *T implements Defaulter, FastConf calls Defaults() once per reload AFTER decoding the merged map into *T and AFTER applying struct-tag defaults (WithStructDefaults), but BEFORE running validators. This allows computed defaults, path normalization, and any logic that cannot be expressed in struct tags.

Example:

type AppConfig struct { Port int; DataDir string }

func (c *AppConfig) Defaults() {
    if c.Port == 0 { c.Port = 8080 }
    if c.DataDir == "" { c.DataDir = "/var/lib/myapp" }
}

type DiffChange added in v0.18.0

type DiffChange = istate.DiffChange

DiffChange classifies one DiffEntry as an add, removal, or in-place modification. See State.Diff.

type DiffEntry added in v0.18.0

type DiffEntry = istate.DiffEntry

DiffEntry is one structured per-path difference between two State snapshots, returned by State.Diff and embedded in DiffEvent and PlanResult. Use FormatDiff to render a human-readable line list.

type DiffEvent

type DiffEvent = istate.DiffEvent

type DiffReporter

type DiffReporter = istate.DiffReporter

type DiffReporterFunc

type DiffReporterFunc = istate.DiffReporterFunc

type DiffReporterMetricsSink

type DiffReporterMetricsSink = iobs.DiffReporterMetricsSink

type DumpFormat added in v0.18.0

type DumpFormat = istate.DumpFormat

DumpFormat selects the encoding used by State.Dump. The zero value is DumpYAML.

type EvalContext

type EvalContext = feature.EvalContext

type FeatureRule

type FeatureRule = feature.Rule

type FieldSpec

type FieldSpec = pipeline.FieldSpec

func FieldMetaFor added in v0.18.0

func FieldMetaFor(t reflect.Type) []FieldSpec

func ParseFieldTag added in v0.18.0

func ParseFieldTag(tag string) FieldSpec

type HierarchicalOpts

type HierarchicalOpts struct {
	Dir       string // config root directory (default DefaultDir)
	RegionEnv string // env var for region axis (default "REGION")
	ZoneEnv   string // env var for zone axis (default "ZONE")
	HostEnv   string // env var for host axis (default "HOST")
	Watch     bool   // enable fsnotify hot-reload
	// CoalesceProfile selects watcher event-burst windows. Zero value
	// is ProfileK8s.
	CoalesceProfile CoalesceProfile
}

HierarchicalOpts captures the common knobs for deployments that use the base + regions/<r> + zones/<z> + hosts/<h> directory layout driven by environment variables.

type Introspection

type Introspection = istate.Introspection

type JSONAuditSink

type JSONAuditSink = iobs.JSONAuditSink

func NewJSONAuditSink

func NewJSONAuditSink(w io.Writer) *JSONAuditSink

type K8sOpts

type K8sOpts struct {
	Dir        string // ConfigMap mount path (default "/etc/config")
	ProfileEnv string // env var to read profile from (default DefaultProfileEnv)
	Default    string // default profile if env empty (default "default")
	Watch      bool   // enable fsnotify (recommended)
	// CoalesceProfile selects watcher event-burst windows. Zero value
	// is ProfileK8s, which matches ConfigMap atomic-swap latencies.
	CoalesceProfile CoalesceProfile
}

K8sOpts captures the common knobs for a Kubernetes deployment that reads ConfigMaps mounted at a known directory and selects a profile from an environment variable populated by the Pod spec.

type LayerKind

type LayerKind = istate.LayerKind

type Manager

type Manager[T any] struct {
	// contains filtered or unexported fields
}

Manager is the strongly-typed, lock-free configuration manager.

func MustNew added in v0.18.0

func MustNew[T any](ctx context.Context, opts ...Option) *Manager[T]

MustNew is the panic variant of New. It is intended for top-level program initialisation (main / init), where the only sensible response to a configuration-load failure is to abort startup with a loud, deterministic panic:

var Config = fastconf.MustNew[AppConfig](context.Background(),
    fastconf.WithDir("conf.d"),
    fastconf.WithProvider(provider.NewEnv("APP_")),
)

Long-running servers / daemons should continue to use New so they can decide whether to fall back to built-in defaults or keep serving the previous snapshot. MustNew deliberately omits MustGet / MustReload variants:

  • Manager.Get on a successfully constructed manager never returns nil — New runs the initial reload before returning, so the snapshot is always populated.
  • Manager.Reload failures are runtime events; panicking on a network blip would violate the framework's failure-safe contract.
  • Extract is nil-safe by design and cannot fail.

The panic message wraps the underlying error so `recover` / panic reporters surface the original cause.

Example

ExampleMustNew demonstrates the one-line top-level initialisation pattern. MustNew panics when the initial reload fails, so it is intended for main / init in command-line tools and tests — not for long-running daemons that should degrade gracefully.

package main

import (
	"context"
	"fmt"
	"testing/fstest"

	"github.com/fastabc/fastconf"
)

type apiExampleConfig struct {
	Server struct {
		Addr string `json:"addr" yaml:"addr"`
	} `json:"server" yaml:"server"`
}

func main() {
	mgr := fastconf.MustNew[apiExampleConfig](context.Background(),
		fastconf.PresetTesting(fastconf.TestingOpts{
			FS: fstest.MapFS{
				"conf.d/base/00-app.yaml": &fstest.MapFile{
					Data: []byte("server:\n  addr: \":8080\"\n"),
				},
			},
		}),
	)
	defer mgr.Close()

	fmt.Println(mgr.Get().Server.Addr)
}
Output:
:8080

func New

func New[T any](ctx context.Context, opts ...Option) (*Manager[T], error)

New constructs a Manager and runs the first reload synchronously.

For one-line initialisation in main / init see MustNew.

Example

ExampleNew demonstrates the shortest typed entry path: construct a manager, read the live value, and close it when the owner shuts down.

package main

import (
	"context"
	"fmt"
	"testing/fstest"

	"github.com/fastabc/fastconf"
)

type apiExampleConfig struct {
	Server struct {
		Addr string `json:"addr" yaml:"addr"`
	} `json:"server" yaml:"server"`
}

func main() {
	mgr, err := fastconf.New[apiExampleConfig](context.Background(),
		fastconf.PresetTesting(fastconf.TestingOpts{
			FS: fstest.MapFS{
				"conf.d/base/00-app.yaml": &fstest.MapFile{
					Data: []byte("server:\n  addr: \":8080\"\n"),
				},
			},
		}),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	fmt.Println(mgr.Get().Server.Addr)
}
Output:
:8080

func (*Manager[T]) Close

func (m *Manager[T]) Close() error

func (*Manager[T]) Errors

func (m *Manager[T]) Errors() <-chan ReloadError
Example

ExampleManager_Errors demonstrates the asynchronous failure stream that lets services centralize reload error handling without blocking the writer.

package main

import (
	"context"
	"fmt"
	"testing/fstest"

	"github.com/fastabc/fastconf"
)

type apiExampleConfig struct {
	Server struct {
		Addr string `json:"addr" yaml:"addr"`
	} `json:"server" yaml:"server"`
}

func main() {
	mgr, err := fastconf.New[apiExampleConfig](context.Background(),
		fastconf.PresetTesting(fastconf.TestingOpts{
			FS: fstest.MapFS{
				"conf.d/base/00-app.yaml": &fstest.MapFile{
					Data: []byte("server:\n  addr: \":8080\"\n"),
				},
			},
		}),
		fastconf.WithValidator(func(c *apiExampleConfig) error {
			if c.Server.Addr == "" {
				return fmt.Errorf("server.addr is required")
			}
			return nil
		}),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	_ = mgr.Reload(context.Background(), fastconf.WithSourceOverride(map[string]any{
		"server": map[string]any{"addr": ""},
	}))
	re := <-mgr.Errors()
	fmt.Println(re.Reason, re.Err != nil)
}
Output:
override true

func (*Manager[T]) Get

func (m *Manager[T]) Get() *T

func (*Manager[T]) Plan

func (m *Manager[T]) Plan() *PlanBuilder[T]
Example

ExampleManager_Plan demonstrates previewing a file-backed change before it becomes the live snapshot.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/fastabc/fastconf"
)

type apiExampleConfig struct {
	Server struct {
		Addr string `json:"addr" yaml:"addr"`
	} `json:"server" yaml:"server"`
}

func main() {
	root := mustExampleTempDir("example-plan-")
	defer os.RemoveAll(root)

	confDir := filepath.Join(root, "conf.d")
	configPath := filepath.Join(confDir, "base", "00-app.yaml")
	mustWriteExampleFile(configPath, "server:\n  addr: \":8080\"\n")

	mgr, err := fastconf.New[apiExampleConfig](context.Background(),
		fastconf.WithDir(confDir),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	mustWriteExampleFile(configPath, "server:\n  addr: \":9090\"\n")
	plan, err := mgr.Plan().Run(context.Background())
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(len(plan.Diff), plan.Proposed.Value.Server.Addr, mgr.Get().Server.Addr)
}

func mustExampleTempDir(pattern string) string {
	dir, err := os.MkdirTemp(".", pattern)
	if err != nil {
		panic(err)
	}
	abs, err := filepath.Abs(dir)
	if err != nil {
		panic(err)
	}
	return abs
}

func mustWriteExampleFile(path, content string) {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		panic(err)
	}
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		panic(err)
	}
}
Output:
1 :9090 :8080

func (*Manager[T]) Reload

func (m *Manager[T]) Reload(ctx context.Context, opts ...ReloadOption) error

func (*Manager[T]) Replay

func (m *Manager[T]) Replay() *Replay[T]

func (*Manager[T]) Snapshot

func (m *Manager[T]) Snapshot() *State[T]

func (*Manager[T]) Watcher

func (m *Manager[T]) Watcher() *Watcher[T]

type MetricsSink

type MetricsSink = iobs.MetricsSink

type MigrationApplier

type MigrationApplier interface {
	Migrate(map[string]any) error
}

MigrationApplier is the root-facade contract for the version migration stage. Migrate is invoked once per reload on the merged raw map, before any transformer and before decoding into *T. Use MigrationFunc to adapt a plain `func(map[string]any) error` value.

type MigrationFunc

type MigrationFunc func(map[string]any) error

MigrationFunc adapts a plain function value to the MigrationApplier contract.

func (MigrationFunc) Migrate

func (fn MigrationFunc) Migrate(root map[string]any) error

Migrate implements MigrationApplier.

type Option

type Option = iopts.Option

func PresetHierarchical

func PresetHierarchical(p HierarchicalOpts) Option

PresetHierarchical returns options for the standard multi-axis deployment pattern: base layer always loaded, then regions (if $REGION is set), then zones (if $ZONE is set), then hosts (if $HOST is set or hostname matches a subdirectory). Providers still override all file layers.

The hosts axis uses DefaultFromHostname: true, so it automatically activates based on os.Hostname() when the host env var is not set. Set the env var explicitly to an empty string to disable host-specific overlays.

Example directory layout:

config/
├── base/           <- always loaded (priority 1000-1999)
├── regions/
│   └── eu-west/    <- loaded when $REGION=eu-west (priority 3000-3099)
├── zones/
│   └── az1/        <- loaded when $ZONE=az1       (priority 3100-3199)
└── hosts/
    └── web-01/     <- loaded when $HOST=web-01 or hostname=web-01 (priority 3200-3299)

func PresetK8s

func PresetK8s(p K8sOpts) Option

PresetK8s returns the canonical option bundle for K8s side-by-side ConfigMap deployments: directory load, profile from env, watch on, strict mode (fail loud on unknown fields).

func PresetSidecar

func PresetSidecar(p SidecarOpts) Option

PresetSidecar returns options tuned for a sidecar daemon: bigger history ring (so /events SSE consumers can replay), watch on by default, less strict so unknown fields warn instead of fail.

func PresetTesting

func PresetTesting(p TestingOpts) Option

PresetTesting returns options tuned for hermetic tests. Watch is always disabled; strict is always on.

func WithAuditSink

func WithAuditSink(sink AuditSink) Option

func WithCoalesce added in v0.18.0

func WithCoalesce(c CoalesceOptions) Option

WithCoalesce overrides just the coalescer windows without touching the Watch enabled flag or paths. Useful when a Preset already enabled Watch with a profile-based timing set and the caller wants to fine- tune one knob:

fastconf.PresetK8s(K8sOpts{Watch: true, CoalesceProfile: ProfileK8s}),
fastconf.WithCoalesce(CoalesceOptions{Quiet: 75*time.Millisecond}),

func WithCodecBridge

func WithCodecBridge(b CodecBridge) Option

WithCodecBridge selects the bytes-to-struct decoder for the typed stage. See CodecBridge for the BridgeJSON / BridgeYAML semantics.

Troubleshooting

The default BridgeJSON round-trips through encoding/json so the canonical-hash cache can reuse the marshalled bytes. It honours `json:` and `fastconf:` struct tags only. Symptoms that indicate the default is mis-matched to your struct:

  • snake_case keys in your YAML are silently dropped — the field is left at its zero value (e.g. `db_pool: 50` ignored when *T only declares `yaml:"db_pool"`). FastConf emits a one-time warn log at New() to surface this; switch to BridgeYAML or add `json:` tags.
  • nested structs deserialize as nil — yaml's anchor / merge keys are normalized to map[string]any by the decoder but json's struct decoder reads field names, not tags, when no `json:` tag is present.
  • time.Time fields fail to parse — yaml's native time type encodes as `2006-01-02T15:04:05Z` strings; the json bridge accepts those only with a `time.Time`-aware typed hook.

When in doubt, set BridgeYAML for YAML-tagged configs.

func WithDefaults added in v0.18.0

func WithDefaults[T any](fn func(*T)) Option

WithDefaults installs a post-decode defaults function for cases where *T cannot implement the Defaulter interface (e.g., third-party types or when modifying the struct definition is not possible). It is the function-form counterpart to Defaulter and runs at the same point in the pipeline.

Defaults precedence (each step only fills zero / unset fields left by the previous step):

  1. WithStructDefaults — `fastconf:"default=..."` struct tags
  2. Defaulter interface — *T.Defaults() if implemented
  3. WithDefaults — explicit fn, last to run

All three run BEFORE validators, so WithValidator sees the populated value.

func WithDiffReporter

func WithDiffReporter(r DiffReporter) Option

func WithDiffReporterQueueCap

func WithDiffReporterQueueCap(n int) Option

func WithDir

func WithDir(dir string) Option

func WithDotEnvAuto

func WithDotEnvAuto(prefix string) Option

func WithFS

func WithFS(f fs.FS) Option

func WithFeatureRules

func WithFeatureRules[T any](extract func(*T) map[string]feature.Rule) Option

func WithGenerator

func WithGenerator(g contracts.Generator) Option

func WithHistory

func WithHistory(n int) Option

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger overrides the default slog logger. Passing nil records a deferred error so a misconfigured logger fails loudly at New(), rather than silently routing every log line into the default backend.

func WithMergeKeys

func WithMergeKeys(keys map[string]string) Option

func WithMetrics

func WithMetrics(m MetricsSink) Option

WithMetrics installs a MetricsSink. Passing nil records a deferred error so a missing sink fails loudly at New() rather than silently dropping every metric.

func WithMigrations

func WithMigrations(run func(map[string]any) error) Option

func WithMultiAxisOverlays

func WithMultiAxisOverlays(axes ...OverlayAxis) Option

WithMultiAxisOverlays adds multi-axis overlay layers (region, tier, hostname, ...). Each OverlayAxis resolves at assemble time to a concrete extra overlay directory via its EnvVar / DefaultFromHostname rules. Append-only across calls.

func WithPolicy

func WithPolicy[T any](p policy.Policy[T]) Option

func WithProfile

func WithProfile(p ProfileOptions) Option

WithProfile installs the supplied ProfileOptions. A zero value is valid (loads base only).

func WithProvenance

func WithProvenance(level ProvenanceLevel) Option

func WithProvider

func WithProvider(p contracts.Provider) Option

func WithProviderByName

func WithProviderByName(name string, cfg map[string]any) Option

func WithProviderOrdered

func WithProviderOrdered(ps ...contracts.Provider) Option

func WithProviderRegistry

func WithProviderRegistry(r *ProviderRegistry) Option

func WithRawMapAccess

func WithRawMapAccess(fn func(root map[string]any)) Option

func WithSecretRedactor

func WithSecretRedactor(r SecretRedactor) Option

func WithSecretResolver

func WithSecretResolver(r SecretResolver) Option

func WithSource added in v0.16.0

func WithSource(src contracts.Source, p contracts.Parser) Option

func WithStrict

func WithStrict(strict bool) Option

func WithStructDefaults

func WithStructDefaults[T any]() Option

WithStructDefaults installs a transformer that populates zero-valued fields of *T from `fastconf:"default=..."` struct tags. It runs once per reload, immediately before validation, so user-supplied YAML / patch / provider values always win over the tag default.

func WithTracer

func WithTracer(t Tracer) Option

WithTracer installs a Tracer. Passing nil records a deferred error so a missing tracer fails loudly at New() rather than silently dropping every span.

func WithTransformers

func WithTransformers(t ...Transformer) Option

WithTransformers appends raw-map transformers that run in declared order after merge and before the typed decoder. Implementations satisfy the root Transformer interface (Name + Transform).

func WithTypedHook

func WithTypedHook(h decoder.TypedHook) Option

func WithValidator

func WithValidator[T any](v func(*T) error) Option

func WithWatch

func WithWatch(w WatchOptions) Option

WithWatch installs the file-watcher with the supplied WatchOptions. A zero WatchOptions{} disables the watcher (same as omitting this Option). The CoalesceProfile selector applies before the per-field Coalesce values, so Coalesce overrides anything the profile set.

func WithoutDefaultTypedHooks

func WithoutDefaultTypedHooks() Option

type Origin

type Origin = provenance.Origin

type OriginIndex

type OriginIndex = provenance.Index

type OverlayAxis

type OverlayAxis struct {
	Dir                 string
	EnvVar              string
	Priority            int
	DefaultFromHostname bool
}

OverlayAxis describes one multi-axis overlay layer. Resolution order:

  1. EnvVar present + non-empty → use that value
  2. EnvVar present + empty → skip axis (operator opt-out)
  3. EnvVar absent + DefaultFromHostname → fall back to os.Hostname()
  4. otherwise → skip axis

Priority should be a value in or above the contracts.BandExtraOverlay (3000) range to win over file-base / single-profile overlays. The Generator (7000) and Provider (8000) bands stay higher.

type PlanBuilder

type PlanBuilder[T any] struct {
	// contains filtered or unexported fields
}

func (*PlanBuilder[T]) Run

func (b *PlanBuilder[T]) Run(ctx context.Context) (*PlanResult[T], error)

func (*PlanBuilder[T]) WithHostname

func (b *PlanBuilder[T]) WithHostname(host string) *PlanBuilder[T]

type PlanResult

type PlanResult[T any] struct {
	Proposed   *State[T]
	Diff       []DiffEntry
	Validators []ValidatorReport
	Policies   []policy.Violation
}

type PolicyError

type PolicyError = fcerr.PolicyError

type ProfileOptions added in v0.18.0

type ProfileOptions struct {
	// Single is the active profile name for the legacy single-profile
	// path. Set this when you want one overlay subdirectory selected
	// by name. Mutually exclusive with Multi.
	Single string
	// Multi enables expression-based overlay matching by populating
	// the active profile set. Each overlay's `_meta.yaml.match`
	// predicate (or, lacking _meta.yaml, the subdirectory name) is
	// evaluated against this set.
	Multi []string
	// Expr is an additional global expression that must hold for any
	// overlay to be selected. AND-ed with each overlay's per-meta
	// match. Empty disables the global filter.
	Expr string
	// EnvVar names the environment variable read when Single / Multi
	// are both empty. Empty falls back to DefaultProfileEnv
	// ("APP_PROFILE").
	EnvVar string
	// Default is the profile name used when EnvVar is unset / empty.
	Default string
}

ProfileOptions bundles the profile-selection knobs. Single and Multi are mutually exclusive: when Multi is non-empty it takes precedence and Single is ignored. Expr is the global expression AND-ed with each overlay's `_meta.yaml.match` predicate. EnvVar / Default control the fallback chain when neither Single nor Multi is set:

  1. ProfileOptions.Single (when non-empty)
  2. ProfileOptions.Multi (when non-empty; turns on expression matching)
  3. $EnvVar / $DefaultProfileEnv
  4. ProfileOptions.Default
  5. _meta.yaml's spec.defaultProfile

type ProvenanceLevel

type ProvenanceLevel = provenance.Level

type ProviderFactory

type ProviderFactory = registry.Factory

func LookupProviderFactory

func LookupProviderFactory(name string) (ProviderFactory, bool)

type ProviderMetricsSink

type ProviderMetricsSink = iobs.ProviderMetricsSink

type ProviderRegistry

type ProviderRegistry = registry.Registry

func NewProviderRegistry

func NewProviderRegistry() *ProviderRegistry

type ReloadCause

type ReloadCause = istate.ReloadCause

type ReloadError

type ReloadError = fcerr.ReloadError

type ReloadOption

type ReloadOption = imanager.ReloadOption

func WithReloadReason

func WithReloadReason(reason string) ReloadOption

func WithSourceOverride

func WithSourceOverride(override map[string]any) ReloadOption

type RenderMetricsSink

type RenderMetricsSink = iobs.RenderMetricsSink

type Replay

type Replay[T any] struct {
	// contains filtered or unexported fields
}

func (*Replay[T]) List

func (r *Replay[T]) List() []*State[T]

func (*Replay[T]) Rollback

func (r *Replay[T]) Rollback(target *State[T]) error
Example

ExampleReplay_Rollback demonstrates recovering a retained prior snapshot without rerunning the reload pipeline.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/fastabc/fastconf"
)

type apiExampleConfig struct {
	Server struct {
		Addr string `json:"addr" yaml:"addr"`
	} `json:"server" yaml:"server"`
}

func main() {
	root := mustExampleTempDir("example-replay-")
	defer os.RemoveAll(root)

	confDir := filepath.Join(root, "conf.d")
	configPath := filepath.Join(confDir, "base", "00-app.yaml")
	mustWriteExampleFile(configPath, "server:\n  addr: \":8080\"\n")

	mgr, err := fastconf.New[apiExampleConfig](context.Background(),
		fastconf.WithDir(confDir),
		fastconf.WithHistory(2),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	mustWriteExampleFile(configPath, "server:\n  addr: \":9090\"\n")
	if err := mgr.Reload(context.Background()); err != nil {
		fmt.Println(err)
		return
	}
	liveAfterReload := mgr.Get().Server.Addr
	history := mgr.Replay().List()
	if err := mgr.Replay().Rollback(history[0]); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(liveAfterReload, mgr.Get().Server.Addr)
}

func mustExampleTempDir(pattern string) string {
	dir, err := os.MkdirTemp(".", pattern)
	if err != nil {
		panic(err)
	}
	abs, err := filepath.Abs(dir)
	if err != nil {
		panic(err)
	}
	return abs
}

func mustWriteExampleFile(path, content string) {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		panic(err)
	}
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		panic(err)
	}
}
Output:
:9090 :8080

type SecretRedactor

type SecretRedactor = secret.Redactor

type SecretRef

type SecretRef = secret.Ref

type SecretResolver

type SecretResolver = secret.Resolver

type SecretResolverFunc

type SecretResolverFunc = secret.ResolverFunc

type SidecarOpts

type SidecarOpts struct {
	Dir      string
	HistoryN int  // history ring capacity (default DefaultSidecarHistoryCap)
	Watch    bool // typically true for sidecars
	Strict   bool
}

SidecarOpts captures the common knobs for cmd/fastconfd-style deployments where the manager is hosted by an in-cluster process that exposes the config over HTTP/SSE.

type SourceRef

type SourceRef = istate.SourceRef

type Span

type Span = iobs.Span

type StageMetricsSink

type StageMetricsSink = iobs.StageMetricsSink

type State

type State[T any] istate.State[T]

func (*State[T]) Diff

func (s *State[T]) Diff(other *State[T]) []DiffEntry

func (*State[T]) Dump added in v0.18.0

func (s *State[T]) Dump(format DumpFormat, redactor SecretRedactor) ([]byte, error)

func (*State[T]) Explain

func (s *State[T]) Explain(path string) []provenance.Origin

func (*State[T]) FeatureRules

func (s *State[T]) FeatureRules() map[string]feature.Rule

func (*State[T]) Introspect

func (s *State[T]) Introspect() *Introspection

func (*State[T]) Lookup

func (s *State[T]) Lookup(path string) []provenance.Origin

func (*State[T]) LookupStrict

func (s *State[T]) LookupStrict(path string) ([]provenance.Origin, error)

func (*State[T]) Origins

func (s *State[T]) Origins() *provenance.Index

func (*State[T]) Redact

func (s *State[T]) Redact(redactor SecretRedactor) map[string]any

func (*State[T]) Redacted

func (s *State[T]) Redacted() map[string]any

type TenantManager

type TenantManager[T any] struct {
	// contains filtered or unexported fields
}

func NewTenantManager

func NewTenantManager[T any]() *TenantManager[T]

func (*TenantManager[T]) Add

func (tm *TenantManager[T]) Add(ctx context.Context, id string, opts ...Option) (*Manager[T], error)

func (*TenantManager[T]) Close

func (tm *TenantManager[T]) Close() error

func (*TenantManager[T]) Get

func (tm *TenantManager[T]) Get(id string) (*Manager[T], error)

func (*TenantManager[T]) Has

func (tm *TenantManager[T]) Has(id string) bool

func (*TenantManager[T]) Remove

func (tm *TenantManager[T]) Remove(id string) error

func (*TenantManager[T]) Tenants

func (tm *TenantManager[T]) Tenants() []string

type TestingOpts

type TestingOpts struct {
	FS      fs.FS
	Profile string
}

TestingOpts captures the common knobs for hermetic unit/integration tests: pass an fs.FS (often testing/fstest.MapFS), pin a profile, disable watch, and force strict so tests catch typos eagerly.

type Tracer

type Tracer = iobs.Tracer

type Transformer

type Transformer interface {
	Name() string
	Transform(map[string]any) error
}

Transformer is the root-facade contract for the pre-decode raw-map transformation stage. Implementations get the merged map[string]any AFTER all source layers fold together and BEFORE the typed decoder runs, so they can rewrite keys, inject computed values, or normalise vendor-specific layouts without touching *T.

The same shape is reused inside pkg/transform for the built-in transformers (Aliases, KeyMap, DropPrefix, EnvReplacer, …); third parties only need to satisfy this root interface.

type ValidatorReport

type ValidatorReport = imanager.ValidatorReport

type WatchOptions added in v0.18.0

type WatchOptions struct {
	Enabled         bool
	Paths           []string
	Coalesce        CoalesceOptions
	CoalesceProfile coalesce.Profile
}

WatchOptions bundles the file-watcher knobs. Enabled defaults to false; set it explicitly to opt into reload-on-change. Paths and Coalesce / CoalesceProfile only apply when Enabled is true.

type Watcher

type Watcher[T any] struct {
	// contains filtered or unexported fields
}

func (*Watcher[T]) Pause

func (w *Watcher[T]) Pause()

func (*Watcher[T]) Paused

func (w *Watcher[T]) Paused() bool

func (*Watcher[T]) Resume

func (w *Watcher[T]) Resume()

Directories

Path Synopsis
cmd
fastconfctl command
Command fastconfctl is a CLI companion to FastConf for CI / ops:
Command fastconfctl is a CLI companion to FastConf for CI / ops:
fastconfd command
fastconfd is the sidecar daemon.
fastconfd is the sidecar daemon.
fastconfgen command
fastconfgen reads a YAML or JSON configuration sample and emits an equivalent Go struct definition.
fastconfgen reads a YAML or JSON configuration sample and emits an equivalent Go struct definition.
internal/cli
Package cli centralises the FastConf command-line flag set so every cmd/* binary registers -dir / -profile / -strict / -watch with identical defaults and semantics, and constructs the Manager via a single canonical path.
Package cli centralises the FastConf command-line flag set so every cmd/* binary registers -dir / -profile / -strict / -watch with identical defaults and semantics, and constructs the Manager via a single canonical path.
Package contracts is the **public, stable** surface of FastConf interfaces.
Package contracts is the **public, stable** surface of FastConf interfaces.
examples
fastconf
contracts module
integrations
bus
Package bus provides a small message-bus abstraction for FastConf.
Package bus provides a small message-bus abstraction for FastConf.
openfeature
Package openfeature adapts a FastConf Manager into an OpenFeature-shaped provider.
Package openfeature adapts a FastConf Manager into an OpenFeature-shaped provider.
render
Package render plugs FastConf into the long tail of legacy daemons that only consume on-disk configuration files (nginx.conf, envoy.yaml, postgresql.conf, ...).
Package render plugs FastConf into the long tail of legacy daemons that only consume on-disk configuration files (nginx.conf, envoy.yaml, postgresql.conf, ...).
internal
coalesce
Package coalesce collapses bursty fsnotify events into a single trigger per (key, burst) — where "key" is typically the parent directory of the events.
Package coalesce collapses bursty fsnotify events into a single trigger per (key, burst) — where "key" is typically the parent directory of the events.
diffreport
Package diffreport is the bounded-queue worker pool that fans post-reload diff events out to user-installed Reporter implementations.
Package diffreport is the bounded-queue worker pool that fans post-reload diff events out to user-installed Reporter implementations.
obs
pipeline
Package pipeline holds reload-pipeline helpers that are pure functions of *T or the merged map[string]any.
Package pipeline holds reload-pipeline helpers that are pure functions of *T or the merged map[string]any.
provenance
Package provenance owns the per-field "where did this value come from" index that the merger feeds during reload.
Package provenance owns the per-field "where did this value come from" index that the merger feeds during reload.
registry
Package registry holds the provider-factory registry that backs fastconf.WithProviderByName.
Package registry holds the provider-factory registry that backs fastconf.WithProviderByName.
secret
Package secret implements the redaction and resolver primitives that back fastconf's `fastconf:"secret"` tag and WithSecretResolver hook.
Package secret implements the redaction and resolver primitives that back fastconf's `fastconf:"secret"` tag and WithSecretResolver hook.
state
Package state holds snapshot, source, and reload-cause primitives used by the public fastconf facade and internal manager implementation.
Package state holds snapshot, source, and reload-cause primitives used by the public fastconf facade and internal manager implementation.
testutil
Package testutil centralises test helpers shared across the fastconf module.
Package testutil centralises test helpers shared across the fastconf module.
typeinfo
Package typeinfo provides a single, cached reflect.Type walker for FastConf's per-T metadata extraction (secret paths, default tags, top-level field hashers).
Package typeinfo provides a single, cached reflect.Type walker for FastConf's per-T metadata extraction (secret paths, default tags, top-level field hashers).
watcher
Package watcher subscribes to filesystem changes and feeds them into a coalescer.
Package watcher subscribes to filesystem changes and feeds them into a coalescer.
pkg
cliadapter
Package cliadapter converts CLI flag state into a fastconf-compatible nested map containing only the flags the user explicitly set on the command line.
Package cliadapter converts CLI flag state into a fastconf-compatible nested map containing only the flags the user explicitly set on the command line.
decoder
Package decoder turns bytes of various encodings (yaml/json/...) into a uniform map[string]any intermediate representation.
Package decoder turns bytes of various encodings (yaml/json/...) into a uniform map[string]any intermediate representation.
discovery
Package discovery scans a configuration root and produces a stream of priority-ordered layers (base, overlays, extra overlay axes).
Package discovery scans a configuration root and produces a stream of priority-ordered layers (base, overlays, extra overlay axes).
feature
Package feature provides a tiny, allocation-light feature-flag / rollout evaluator that piggybacks on FastConf's strongly-typed configuration.
Package feature provides a tiny, allocation-light feature-flag / rollout evaluator that piggybacks on FastConf's strongly-typed configuration.
flog
Package flog wraps *slog.Logger with a zerolog-style fluent API while preserving slog's handler ecosystem.
Package flog wraps *slog.Logger with a zerolog-style fluent API while preserving slog's handler ecosystem.
generator
Package generator hosts FastConf's built-in contracts.Generator implementations.
Package generator hosts FastConf's built-in contracts.Generator implementations.
mappath
Package mappath provides dotted-path helpers for map[string]any trees.
Package mappath provides dotted-path helpers for map[string]any trees.
merger
Package merger implements Kustomize-style deep merge for map[string]any trees.
Package merger implements Kustomize-style deep merge for map[string]any trees.
migration
Package migration lets FastConf rewrite the merged map from one schema version to another before it is decoded into the strongly typed snapshot.
Package migration lets FastConf rewrite the merged map from one schema version to another before it is decoded into the strongly typed snapshot.
parser
Package parser exposes the koanf-style Parser slot used at the Manager call site (WithSource(file.New(path), yaml.Parser())).
Package parser exposes the koanf-style Parser slot used at the Manager call site (WithSource(file.New(path), yaml.Parser())).
profile
Package profile implements FastConf's tiny boolean profile-expression language.
Package profile implements FastConf's tiny boolean profile-expression language.
provider
Package provider abstracts external configuration sources (env, CLI, KV, Vault, ...).
Package provider abstracts external configuration sources (env, CLI, KV, Vault, ...).
source
Package source provides built-in contracts.Source implementations for the koanf-style WithSource(file/http/bytes, parser) call shape.
Package source provides built-in contracts.Source implementations for the koanf-style WithSource(file/http/bytes, parser) call shape.
transform
Package transform provides composable, post-merge / pre-decode transformations on the merged configuration tree.
Package transform provides composable, post-merge / pre-decode transformations on the merged configuration tree.
typed
Package typed contains scalar-value helpers shared by pkg/provider and pkg/mappath.
Package typed contains scalar-value helpers shared by pkg/provider and pkg/mappath.
validate
Package validate hosts reusable validation primitives for FastConf.
Package validate hosts reusable validation primitives for FastConf.
Package policy defines the policy interface.
Package policy defines the policy interface.
providers
consul
Package consul is a first-party Consul KV provider for FastConf.
Package consul is a first-party Consul KV provider for FastConf.
http
Package http is a first-party HTTP/HTTPS provider for FastConf.
Package http is a first-party HTTP/HTTPS provider for FastConf.
k8s
Package k8s implements first-party FastConf providers for Kubernetes integration.
Package k8s implements first-party FastConf providers for Kubernetes integration.
nats
Package nats implements a FastConf Provider backed by a NATS-style publish/subscribe connection.
Package nats implements a FastConf Provider backed by a NATS-style publish/subscribe connection.
redisstream
Package redisstream implements a FastConf Provider backed by a Redis Streams compatible client.
Package redisstream implements a FastConf Provider backed by a Redis Streams compatible client.
vault
Package vault is a first-party HashiCorp Vault KV v2 provider for FastConf.
Package vault is a first-party HashiCorp Vault KV v2 provider for FastConf.

Jump to

Keyboard shortcuts

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