fastconf

package module
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: May 17, 2026 License: MIT Imports: 38 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: pre-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. Codec & bridge
  11. Transformers & migration
  12. Watch, Subscribe, and Plan
  13. Provenance, history & rollback
  14. Observability
  15. Multi-tenant & presets
  16. Sub-module ecosystem
  17. Extension guide
  18. CLI tools
  19. Performance
  20. Development
  21. License

Quick start

package main

import (
    "context"
    "log"

    "github.com/fastabc/fastconf"
)

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.WithProfileEnv("APP_PROFILE"),
        fastconf.WithDefaultProfile("dev"),
        fastconf.WithWatch(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

With APP_PROFILE=prod, FastConf merges base/* first, then overlays/prod/*. The default decode bridge does a JSON round-trip, so if your structs only carry yaml tags either add json tags or pass fastconf.WithCodecBridge(fastconf.BridgeYAML) explicitly.

Three recommended entry points
Scenario Recommended combo Read next
Local file config, single service New + WithDir + Get ExampleNew / docs/cookbook/introspect.md
Kubernetes hot-reload PresetK8s + Subscribe + Errors docs/cookbook/k8s.md / docs/cookbook/reload-policy.md
Remote source / GitOps WithProvider + Plan + Provenance docs/cookbook/vault.md / docs/cookbook/consul.md / docs/cookbook/plan.md

For unit tests use PresetTesting; for sidecars PresetSidecar; for region / zone / host axis overlays see PresetHierarchical and WithMultiAxisOverlays.


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 policy-based mergeKeys strategic merge for lists of objects.
  • Opt-in extensions. Providers, transformers, secret resolvers, validators, policies, metrics, and tracing are all optional.
  • Boundary-honest interface surface. Public contracts live under contracts/; reusable primitives live under pkg/*; private helpers under internal/*; CI enforces dependency direction.

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/policy/cue@latest
go get github.com/fastabc/fastconf/policy/opa@latest
go get github.com/fastabc/fastconf/providers/s3@latest
go get github.com/fastabc/fastconf/providers/s3events@latest
go get github.com/fastabc/fastconf/providers/nats@latest
go get github.com/fastabc/fastconf/providers/redisstream@latest
go get github.com/fastabc/fastconf/validate/cue/cuelang@latest
go get github.com/fastabc/fastconf/validate/playground@latest

Command-line tools (Go ≥ 1.26):

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

Each GitHub Release also ships prebuilt binaries for linux/{amd64,arm64}, darwin/{amd64,arm64}, and windows/amd64 with SHA256SUMS.


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
Source layout
.                       (repo root — package fastconf)
  manager.go            Manager[T]: New / Get / Close / Reload / Snapshot
  pipeline.go           runStages[T] + Plan dry-run entry
  pipeline_stages.go    Merge / Assemble / Migrate / Transform / Decode / Validate stages
  options.go            All WithXxx options + public types
  state.go              State[T] + ReloadCause + Origins/Explain/Lookup + history
  watch.go / watcher.go Subscribe, fsnotify, symlink handling
  provider_watch.go     Provider event subscription (exponential backoff + drop-on-full)
  presets.go            PresetK8s / PresetSidecar / PresetTesting / PresetHierarchical
  registry.go           RegisterProviderFactory / WithProviderByName
  defaults.go           fastconf:"default=…" struct tag + built-in hooks
  secret.go             fastconf:"secret" + SecretRedactor
  feature.go            FeatureRule / Eval / Sub
  field_meta.go         range / enum / required field-meta checks
  obs_audit.go / obs_metrics.go / obs_tracer.go   sinks
  tenant.go             TenantManager[T]

contracts/              Public stable interfaces: Provider / Codec / Event / Snapshot / Source / Priority
pkg/                    Reusable primitives — importable by third-party authors
  decoder/              YAML / JSON / TOML codec registry
  discovery/            conf.d scanning + _meta.yaml parsing
  feature/              feature-flag rule + EvalContext
  flog/                 zerolog-style fluent wrapper over *slog.Logger
  generator/            contracts.Generator helpers
  mappath/              dotted-path Get/Set/Delete utilities
  merger/               Kustomize-style map[string]any layering
  migration/            Chain + Step (From/To/Apply)
  profile/              profile expression compiler (&, |, !, ())
  provider/             built-in Env / CLI / Bytes / File / Labels providers
  transform/            Defaults / SetIfAbsent / EnvSubst / DeletePaths / Aliases
  validate/             Validator + ValidatorReport
internal/               Private helpers (debounce / obs / typeinfo / watcher)
providers/              First-party providers (consul / http / vault in root module; nats / redisstream / s3 as sub-modules)
integrations/           bus / render / log / openfeature adapters
observability/          metrics/prometheus + otel (independent sub-modules)
policy/                 Policy interface; cue/opa backends as sub-modules
validate/               cue/cuelang + playground (independent sub-modules)
cmd/                    fastconfd (root module); fastconfctl / fastconfgen (sub-modules)
Dependency direction (CI-enforced)
fastconf  →  pkg/{discovery,decoder,flog,merger,provider,validate}
          →  internal/watcher
          →  contracts

pkg/* MUST NOT depend on each other except via this whitelist
(kept in sync with tools/check-deps.sh):
  pkg/discovery → pkg/profile
  pkg/generator → pkg/mappath
  pkg/provider  → pkg/decoder
  pkg/provider  → pkg/mappath
  pkg/transform → pkg/mappath
internal/* MUST NOT depend on each other; only the standard library.

Manager API

type Manager[T any] struct { /* unexported */ }

// 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

// Write path. ctx controls both enqueue/wait AND the pipeline itself:
// cancelling it aborts provider.Load / secret resolvers / transformers
// and surfaces as ctx.Err().
func (m *Manager[T]) Reload(ctx context.Context, opts ...ReloadOption) error

// Dry-run — never updates the live pointer; collects every ValidatorReport
func (m *Manager[T]) Plan() *PlanBuilder[T] // .WithHostname(...).Run(ctx) → *PlanResult[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

// Sub-system accessors (zero-cost namespaces)
func (m *Manager[T]) Watcher() *Watcher[T]  // .Pause() / .Resume() / .Paused()
func (m *Manager[T]) Replay()  *Replay[T]   // .List() / .Rollback(*State[T])

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

Package-level generics — anything that derives a subtree M from *T lives at the package level:

// 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; type-mismatch returns def.
func Eval[T, V any](m *Manager[T], key string, ctx feature.EvalContext, def V) V

// Read-only subtree alias.
func Sub[T, M any](s *State[T], extract func(*T) *M) *M
State[T] — immutable snapshot
type State[T any] struct {
    Value      *T             // strongly typed config; Get() returns this
    Hash       [32]byte       // global SHA-256 fingerprint
    LoadedAt   int64          // unix nanoseconds
    Sources    []SourceRef    // every layer that contributed
    Generation uint64         // monotonic version
    Cause      ReloadCause    // why this reload ran + provider revisions
}

func (s *State[T]) Explain(path string) []Origin             // oldest → newest override chain
func (s *State[T]) Lookup(path string) []Origin              // alias of Explain
func (s *State[T]) LookupStrict(path string) ([]Origin, error)
func (s *State[T]) Origins() *OriginIndex
func (s *State[T]) Introspect() *Introspection               // Keys / Settings / At
func (s *State[T]) Redacted() map[string]any                 // applies the SecretRedactor
func (s *State[T]) MarshalYAML(redactor SecretRedactor) ([]byte, error)
func (s *State[T]) Diff(other *State[T]) []string
func (s *State[T]) FeatureRules() map[string]feature.Rule

Suggested reading order on pkg.go.dev: NewGetSubscribe / ErrorsPlanReplay. Runnable examples: ExampleNew, ExampleSubscribe, ExampleManager_Errors, ExampleManager_Plan, ExampleReplay_Rollback.


Options reference

All WithXxx options return Option and may be composed in any order when passed to New[T]. Later calls win for duplicates.

Filesystem
Option Purpose Default
WithDir(dir string) Config root directory "conf.d"
WithFS(fs.FS) Alternate fs.FS (testing)
WithStrict(bool) Error on unknown fields false
WithLogger(*slog.Logger) Inject a logger io.Discard (opt-in)
WithCodecBridge(BridgeJSON | BridgeYAML) Decode bridge BridgeJSON
WithMultiAxisOverlays(axes ...OverlayAxis) Multi-axis overlays (region / zone / host)
WithRawMapAccess(fn) Read-only hook over the merged map before decode
Watch
Option Purpose Default
WithWatch(bool) Enable fsnotify false
WithCoalesceQuiet(d) Quiet window after which a per-dir burst fires 30ms
WithCoalesceMaxLag(d) Hard upper bound on burst lifetime 250ms
WithCoalesceSwapHint(d) Tightened window once a K8s ..data swap is detected 5ms
WithCoalesceProfile(p) Apply a preset: ProfileK8s (default) or ProfileLocalDev ProfileK8s
WithWatchPaths(paths...) Additional watch paths

The watcher debounces fsnotify events per parent directory rather than globally, so independent ConfigMaps (or watched dirs) never block each other. When a K8s atomic-swap commit (..data rename/create) is observed, the coalescer tightens the window to swapHint (5ms) instead of waiting the full quiet window — typical reload latency drops from ~500ms (the prior global debouncer default) to ~5–35ms.

Profile
Option Purpose
WithProfile(p string) Explicit single profile
WithProfiles(p ...string) Multi-profile mode (overlays match via _meta.yaml.match)
WithProfileEnv(name string) Read profile from an environment variable
WithDefaultProfile(p string) Fallback when the env var is empty
WithProfileExpr(expr string) Global profile-matching expression
Source × Parser × Provider

FastConf splits the extension surface in two:

  • Source (pkg/source) — a byte-stream contributor (file, http, inline bytes). Paired with a Parser (pkg/parser) at the call site, koanf-style, so the codec is named where the layer is declared.
  • Provider (pkg/provider) — an already-structured contributor (env, cli, KV with one key per setting). No Parser needed.
Option Purpose
WithSource(src, parser) Bind a byte-blob Source with a Parser. Pass nil Parser to auto-pick via content-type hint
WithProvider(p) Register an already-structured provider
WithProviderOrdered(p...) Auto-assigns CLI+100, +101, ... in call order; errors if input has non-zero priority
WithProviderByName(name, cfg) Construct via factory registry (resolved after all options applied)
WithProviderRegistry(r) Manager-local *ProviderRegistry — local wins, then global default
WithGenerator(g) Synthesise a []RawLayer in the assemble stage (e.g. BuildInfo)
WithDotEnvAuto(prefix) Auto-discover a .env file under WithDir

pkg/source and pkg/parser factory functions:

import (
    "github.com/fastabc/fastconf"
    "github.com/fastabc/fastconf/pkg/parser"
    "github.com/fastabc/fastconf/pkg/provider"
    "github.com/fastabc/fastconf/pkg/source"
    "github.com/fastabc/fastconf/pkg/transform"
)

fastconf.New[Cfg](ctx,
    // Byte-blob layers — explicit Source × Parser pairing:
    fastconf.WithSource(source.NewFile("/etc/app/config.yaml"), parser.YAML()),
    fastconf.WithSource(source.NewHTTP("https://kv/config"), parser.JSON()),
    fastconf.WithSource(source.NewBytes("inline", "yaml", data), nil), // nil = auto-bind by content-type

    // Structured providers — no Parser slot:
    fastconf.WithProvider(provider.NewEnv("APP_")),                              // APP_DATABASE__DSN → database.dsn
    fastconf.WithProvider(provider.NewEnvReplacer("APP_", provider.DotReplacer)),// APP_DATABASE_DSN → database.dsn
    fastconf.WithProvider(provider.NewCLI(cliMap)),                              // parsed CLI flag map
    fastconf.WithProvider(provider.NewDotEnv("APP_", ".env")),                   // explicit .env paths
    fastconf.WithProvider(provider.NewLabels(labels, provider.LabelOptions{})),  // Traefik / Docker labels
    fastconf.WithTransformers(transform.ExpandLabels(at, to, opts)),
)
Pipeline enhancers
Option Purpose
WithMigrations(func) Schema migration callback (before transformers)
WithTransformers(t...) Post-merge, pre-decode transformation chain
WithSecretResolver(r) Decrypt leaf secrets after transform, before decode
WithTypedHook(h) Rewrite leaves before decode (built-in: time.Duration)
WithoutDefaultTypedHooks() Disable built-in typed hooks
WithStructDefaults[T]() Populate zero values via fastconf:"default=..."
WithDefaulterFunc[T](fn) Custom defaulter for *T
WithMergeKeys(map) Strategic merge for lists of objects
WithValidator[T](fn) Typed validation after decode; failure preserves old state
WithPolicy[T](p) Policy evaluation after validate; SeverityError aborts reload
WithFeatureRules[T](extract) Attach a feature.Rule table to State for Eval
Observability
Option Purpose
WithMetrics(MetricsSink) Metrics sink (also supports ProviderMetricsSink / StageMetricsSink / RenderMetricsSink)
WithAuditSink(AuditSink) Callback on every successful reload (multi-sink fan-out)
WithDiffReporter(DiffReporter) Async push on non-empty diff; each reporter has its own bounded worker; drop-on-full emits EventDropped("diff-reporter")
WithDiffReporterQueueCap(n int) Per-reporter queue depth (default 64)
WithTracer(Tracer) OTel-compatible span tracer
WithProvenance(level) ProvenanceOff / ProvenanceTopLevel / ProvenanceFull
WithHistory(n) Keep the last n successful states (history ring)
WithSecretRedactor(r) Redact secrets in logs and snapshots (paired with WithSecretResolver)
ReloadOption (passed to Manager.Reload)
Option Purpose
WithSourceOverride(map) Inject a one-shot override layer
WithReloadReason(s) Override the default "manual" reason for audit

Reload pipeline

Triggers
                          ┌── fsnotify events → debounce 500ms ──┐
                          │                                       │
Reload(ctx, opts...) ─────┤    reloadCh chan reloadRequest       ├──► reloadLoop
                          │                                       │    (single writer)
provider.Watch events ────┘── backoff + drop-on-full ──────────┘
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)       [optional]
  ├─ stageTransform:  for each transformer: t.Transform(merged)
  ├─ stageDecode:     json.Marshal(merged) → json.Unmarshal(→ *T)
  │                   apply fastconf:"default=…" struct tags
  ├─ stageFieldMeta:  range / enum / required checks
  ├─ stageValidate:   for each validator: v(*T)
  ├─ stagePolicy:     for each policy:    p.Evaluate(ctx, *T, reason, tenant)
  │
  └─ commit:
       canonicalHashBytes(mergedJSON) → SHA-256 dedup
       atomic.Pointer.Store(newState)
       history.push(newState)
       for each AuditSink: Audit(ctx, cause)
       fireWatches(oldPartHashes, newPartHashes)
Failure-safe semantics

When any stage returns a non-nil error:

  • atomic.Pointer is not updated; Get() keeps returning the old value.
  • Generation is not incremented.
  • The error is returned synchronously from Reload(ctx); the same event is also broadcast asynchronously on Errors().
  • No AuditSink fires — audit only triggers after a successful commit.
  • MetricsSink.ReloadFinished(ok=false, dur) is called.
Context propagation

The ctx passed to Reload(ctx) does more than control enqueue/wait — it threads into the running pipeline:

  • assemble short-circuits on ctx.Err().
  • Each provider.Load(ctx) shares the same ctx; slow providers bail out immediately on cancel.
  • Cancellation errors propagate as context.Canceled / context.DeadlineExceeded (not wrapped in ErrDecode), so callers can errors.Is(err, context.Canceled) precisely.

Filesystem and provider watcher loops have no caller ctx; the framework uses context.Background() for those paths to preserve event-driven reload semantics.


Profiles & overlays

Layout
conf.d/
  base/                   # shared defaults for every profile
    00-defaults.yaml
    10-feature-flags.yaml
  overlays/
    dev/                  # applied when profile == "dev"
      50-dev.yaml
    prod/
      50-prod.yaml
      _meta.yaml          # profile match expression
      _patch.json         # RFC 6902 patch
    staging/
      50-staging.yaml
      _meta.yaml
_meta.yaml fields
schemaVersion: "1"
profileEnv: "APP_PROFILE"     # env var to read profile (overridden by WithProfileEnv)
defaultProfile: "dev"         # fallback profile
appendSlices: true            # slices append instead of overwrite
match: "prod | staging"       # boolean profile expression (&, |, !, () supported)

match is compiled by pkg/profile:

Syntax Meaning
prod profile set contains "prod"
prod | staging contains prod or staging
prod & !debug prod and not debug
(eu-west | eu-east) & !debug composite
RFC 6902 JSON Patch

Drop a _patch.json into any overlay directory; FastConf applies it after the layer's files merge:

[
  { "op": "replace", "path": "/server/addr",      "value": ":8443" },
  { "op": "add",     "path": "/feature/darkMode", "value": true },
  { "op": "remove",  "path": "/legacy/key" }
]
Multi-profile mode
mgr, err := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithProfiles("prod", "eu-west", "canary"),
)

WithProfiles and WithProfile are mutually exclusive. In multi-profile mode each overlay's _meta.yaml.match decides whether it applies.


Provider system

Built-in byte-blob sources (pkg/source)

Pair each Source with a Parser via WithSource(src, parser). Passing nil Parser auto-binds via the content-type hint (file extension, HTTP Content-Type header, or ContentType ctor argument).

Source Constructor Notes
File source.NewFile(path) Reads the file at load time; content-type from extension
HTTP source.NewHTTP(url) Conditional GET with ETag short-circuit; content-type from Content-Type header
Bytes source.NewBytes(name, contentType, data) In-memory layer (most common in tests)
Built-in parsers (pkg/parser)
Parser Content-types claimed
parser.YAML() yaml / .yaml / .yml / application/yaml / application/x-yaml / text/yaml
parser.JSON() json / .json / application/json / text/json
parser.TOML() toml / .toml / application/toml / text/toml

Third-party parsers register their content-types via parser.Register.

Built-in structured providers (pkg/provider)

These contribute map[string]any directly — no Parser needed.

Provider Constructor Notes
Env provider.NewEnv("APP_") APP_FOO__BARfoo.bar (double underscore separator)
EnvReplacer provider.NewEnvReplacer("APP_", provider.DotReplacer) Viper-style single underscore → dot
CLI provider.NewCLI(map[string]any) Parsed CLI flag map
DotEnv provider.NewDotEnv("APP_", paths...) Explicit .env paths
Labels provider.NewLabels(labels, provider.LabelOptions{}) Traefik / Docker-style key=value strings
LabelMap provider.NewLabelMap(labels, provider.LabelOptions{}) Kubernetes annotation-style map[string]string
First-party KV providers in the root module (providers/{vault,consul,http})
import (
    vault    "github.com/fastabc/fastconf/providers/vault"
    consul   "github.com/fastabc/fastconf/providers/consul"
    httpprov "github.com/fastabc/fastconf/providers/http"
)

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{})

Trim them out at build time:

go build -tags no_provider_vault,no_provider_consul,no_provider_http ./...
First-party providers as separate sub-modules

Sub-modules don't ship in the root go.mod; go get them only when needed. All implement contracts.Provider.

// AWS S3 — load with ETag short-circuit, explicit static credentials.
import s3prov "github.com/fastabc/fastconf/providers/s3"

sp, err := s3prov.New(s3prov.Config{
    Region:    "us-east-1",
    Bucket:    "my-configs",
    Key:       "prod/app.yaml",        // codec inferred from ".yaml"
    AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
    SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
    // VersionID: "abc...",            // pin to a specific object version
    // Endpoint:  "http://minio:9000", PathStyle: true,  // for MinIO/LocalStack
})
if err != nil {
    log.Fatal(err)
}
mgr, _ := fastconf.New[AppConfig](ctx, fastconf.WithProvider(sp))

The S3 provider remembers the last ETag and sends If-None-Match on every subsequent Load; AWS returns 304 when the object is unchanged and the provider serves the cached map without re-decoding. That makes repeated Reload() calls cheap and matches the no-spurious-reload contract enforced by providers/http.

For "provider address as a config field" patterns, use the URL helper:

cfg, _ := s3prov.FromURL(
    "s3://my-configs/prod/app.yaml?region=us-east-1",
    s3prov.Credentials{
        AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
        SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
    },
)
sp, _ := s3prov.New(cfg)

FromURL accepts region, codec, endpoint, path_style, version_id, and priority query parameters. Credentials are passed separately so secrets never appear in URLs that may be logged.

For change-driven reloads, compose with providers/s3events (S3 → EventBridge → SQS):

import (
    s3prov   "github.com/fastabc/fastconf/providers/s3"
    s3events "github.com/fastabc/fastconf/providers/s3events"
)

loader, _ := s3prov.New(s3prov.Config{ /* ... */ })
notifier, _ := s3events.New(s3events.Config{
    Region:    "us-east-1",
    QueueURL:  "https://sqs.us-east-1.amazonaws.com/123/cfg-events",
    Bucket:    "my-configs",
    KeyPrefix: "prod/",                // optional filter
    AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
    SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
})

mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithProvider(loader),
    fastconf.WithProvider(notifier),   // watch-only; Load returns empty map
)

The notifier polls SQS with long-poll, filters EventBridge envelopes by bucket and key prefix, deletes the matched messages, and emits a contracts.Event that drives a Manager reload. The loader's ETag short-circuit then makes the re-read free when the event fires for an unrelated key in the same bucket.

NATS JetStream (providers/nats) and Redis Streams (providers/redisstream) are event-driven providers that inject your existing nats.Conn / Redis client adapter through a tiny interface — they pull in no upstream client library.

Provider capability matrix

Pick the right module in 30 seconds. "Watch" describes the native change-notification mechanism; "Resumable" means the provider implements contracts.Resumable.WatchFrom and survives reconnects without losing events. "Codec" indicates whether the provider needs you to choose one.

Provider Module Watch model Resumable Codec Auth model Build tag
pkg/provider.Env / EnvReplacer root load-only n/a env-var prefix n/a
pkg/provider.CLI root load-only n/a n/a (in-memory) n/a
pkg/provider.File root load-only inferred from ext filesystem n/a
pkg/provider.Bytes root load-only explicit n/a (in-memory) n/a
pkg/provider.DotEnv root load-only n/a filesystem n/a
pkg/provider.Labels / LabelMap root load-only n/a n/a (in-memory) n/a
providers/http root ETag + body-hash poll required static headers (Bearer, …) no_provider_http
providers/consul root blocking query (X-Consul-Index) optional (Mode KV/Blob) ACL token no_provider_consul
providers/vault root metadata-version poll (JSON, built-in) static token / WithAuth no_provider_vault
providers/nats sub-module JetStream subscribe yes required inject nats.Conn adapter (sub-module)
providers/redisstream sub-module XREAD BLOCK yes required inject redis.Client adapter (sub-module)
providers/s3 sub-module load + ETag short-circuit inferred from key ext or explicit static AWS creds no_provider_s3
providers/s3events sub-module SQS long-poll (EventBridge) n/a (watch-only) static AWS creds no_provider_s3events

Notes:

  • Load-only providers contribute a layer at every Reload(ctx) but do not push change events. Pair them with a Manager-level trigger (mgr.Watcher(), fsnotify, an external scheduler) or a sibling event-source provider when you need change-driven reloads.
  • Resumable providers re-attach from the last observed Event.Revision on reconnect; non-resumable Watch providers cold-start on every reconnect (still correct, just chattier under network churn).
  • Build tags strip a provider from the binary entirely; sub-modules achieve the same via go.mod exclusion (don't go get them).
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) // (nil, nil) → no native notifications
}
Priority constants

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

Constant Value Use
PriorityDotEnv 5 .env fallback (lowest)
PriorityStatic 10 Static / file layers
PriorityOverlay 20 Overlay providers
PriorityKV 30 Vault / Consul / HTTP / S3 / NATS / Redis Streams
PriorityK8s 40 Kubernetes ConfigMap / Secret
PriorityEnv 50 Process environment variables
PriorityCLI 60 Command-line flag provider (highest)

If picking a priority feels arbitrary, use WithProviderOrdered(p1, p2, p3): each provider receives PriorityCLI+100, +101, +102 ... in call order; later wins. A non-zero explicit priority on an input is rejected to avoid silent override.

Resumable (continuation)
type Resumable interface {
    // Empty lastRev acts like Watch (cold subscribe).
    // Non-empty: deliver events strictly after that revision.
    // If the revision was compacted, return ErrResumeUnsupported and the
    // framework falls back to a cold Watch.
    WatchFrom(ctx context.Context, lastRev string) (<-chan Event, error)
}

The framework remembers each provider's last observed Event.Revision and passes it back into WatchFrom on reconnect.

Provider factory registry
// Register at init or in TestMain.
fastconf.RegisterProviderFactory("vault", func(cfg map[string]any) (contracts.Provider, error) {
    addr, _ := cfg["addr"].(string)
    path, _ := cfg["path"].(string)
    token, _ := cfg["token"].(string)
    return vault.New(addr, path, token)
})

// Use — provider config can now come from YAML.
mgr, err := fastconf.New[AppConfig](ctx,
    fastconf.WithProviderByName("vault", map[string]any{
        "addr":  "https://vault.svc",
        "path":  "kv/data/myapp",
        "token": os.Getenv("VAULT_TOKEN"),
    }),
)

For multi-tenant / per-test isolation use a Manager-local registry:

local := fastconf.NewProviderRegistry()
local.Register("scoped", func(cfg map[string]any) (contracts.Provider, error) {
    return myProvider(cfg)
})

mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithProviderRegistry(local),
    fastconf.WithProviderByName("scoped", map[string]any{...}),
)

Local registry wins on name collision; global names remain resolvable.


Codec & bridge

YAML, JSON, and TOML are registered at init by pkg/decoder. You do not need to call RegisterCodec for these formats — they are immediately available to the discovery layer and to providers that take a Codec.

The decode bridge controls how the merged map[string]any becomes *T:

fastconf.WithCodecBridge(fastconf.BridgeJSON) // default — uses encoding/json
fastconf.WithCodecBridge(fastconf.BridgeYAML) // uses gopkg.in/yaml.v3

Use BridgeYAML when your struct fields only carry yaml tags. Use BridgeJSON (the default) for structs with json tags or anything that also goes through encoding/json elsewhere.

To register a custom codec (e.g. HCL, JSON5) at runtime:

fastconf.RegisterCodec("hcl", hclCodec{})
fastconf.RegisterCodecExt("hcl", "hcl") // .hcl files now route to that codec

Transformers & migration

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

Transformers run after merge and before decode; they receive the merged map[string]any and may safely mutate the tree.

Built-in transformers (pkg/transform)
import "github.com/fastabc/fastconf/pkg/transform"

fastconf.WithTransformers(
    transform.Defaults(map[string]any{                 // recursive merge — does not overwrite
        "server": map[string]any{"timeout": "30s"},
    }),
    transform.SetIfAbsent("server.timeout", "30s"),    // single-path default
    transform.EnvSubst(),                              // ${VAR} / ${VAR:-default}
    transform.DeletePaths("internal.debug"),
    transform.Aliases(map[string]string{               // old path → new path
        "db.url":      "database.dsn",
        "server.port": "server.addr",
    }),
)
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"`
}

mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithStructDefaults[AppConfig](),
    fastconf.WithSecretRedactor(fastconf.DefaultSecretRedactor),
)

fastconf:"default=…" runs after decode and before validate, only populating zero values. Field-meta tags (range=, enum=, required) are checked in the same stage.

Migration
import "github.com/fastabc/fastconf/pkg/migration"

chain := migration.NewChain(
    migration.Step{From: "1", To: "2", Apply: migrateV1toV2},
    migration.Step{From: "2", To: "3", Apply: migrateV2toV3},
)
fastconf.WithMigrations(chain.Migrate)

Or inline:

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

Watch, Subscribe, and Plan

Filesystem watch
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithWatch(true),
    // Defaults to ProfileK8s (quiet=30ms / maxLag=250ms / swapHint=5ms).
    // Switch presets, or tweak one knob:
    fastconf.WithCoalesceQuiet(50*time.Millisecond),
)
// Kubernetes ConfigMap ..data symlink atomic swaps are handled correctly
// by watching the parent directory; swap-commit detection tightens the
// burst window to swapHint (5ms), and per-dir keying prevents multiple
// ConfigMaps from blocking each other.
Field-level Subscribe
cancel := fastconf.Subscribe(mgr,
    func(app *AppConfig) *DatabaseConfig { return &app.Database },
    func(old, neu *DatabaseConfig) {
        if old != nil && *old == *neu { return } // caller-side diff
        reconnect(neu.DSN)
    },
)
defer cancel()

Subscribe callbacks fire synchronously on the reload goroutine (a recover() shields the loop from a panicking subscriber). For long work, spawn a goroutine yourself.

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"},
    }),
)
Pause / Resume
mgr.Watcher().Pause()
applyBatchUpdate()
mgr.Watcher().Resume()
Plan (dry-run)
result, err := mgr.Plan().WithHostname("ci-runner-7").Run(ctx)
if err != nil {
    log.Fatal("plan failed:", err)
}
for _, r := range result.Validators {
    if r.Err != nil {
        log.Printf("validator %s failed: %v", r.Name, r.Err)
    }
}
for _, v := range result.Policies {
    log.Printf("[%s] %s @ %s — %s", v.Severity, v.Rule, v.Path, v.Message)
}

Plan never updates the atomic pointer; SeverityError policy violations are downgraded to warnings in dry-run mode so CI can collect every problem in a single pass.


Provenance, history & rollback

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

origins := mgr.Snapshot().Explain("server.addr")
for _, o := range origins {
    fmt.Printf("layer=%s priority=%d value=%v\n", o.Source.Name, o.Source.Priority, o.Value)
}

// Strict lookup — distinguishes "provenance not enabled" from "path not found".
origins, err := mgr.Snapshot().LookupStrict("database.dsn")
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, with each layer's raw value
History & rollback
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithHistory(10),
)

history := mgr.Replay().List()        // []*State[T], oldest → newest
target  := history[len(history)-2]    // previous version
_ = mgr.Replay().Rollback(target)

Rollback re-publishes a historic *State[T] to the atomic pointer; it does not re-run the pipeline and does not bump Generation, but it does fire Subscribe callbacks (filter on the caller side if you care).

Errors stream
go func() {
    for re := range mgr.Errors() {
        slog.Error("reload failed", "reason", re.Reason, "err", re.Err, "when", re.When)
    }
}()

Buffer 16, drop-on-full. The "keep old state on failure" contract is unchanged regardless of whether anyone reads this channel.


Observability

AuditSink
type AuditSink interface {
    Audit(ctx context.Context, cause ReloadCause) error
}

sink := fastconf.NewJSONAuditSink(os.Stderr) // built-in JSON-lines sink
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithAuditSink(sink),
    fastconf.WithAuditSink(remoteSink), // multiple sinks fan out
)
// Output: {"reason":"watcher","at":"2026-05-14T08:00:00Z","revisions":{"vault":"42"}}
MetricsSink
type MetricsSink interface {
    ReloadStarted()
    ReloadFinished(ok bool, dur time.Duration)
    // Optional extensions: ProviderMetricsSink / StageMetricsSink / RenderMetricsSink
}

A Prometheus implementation lives in a separate sub-module:

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

mgr, _ := fastconf.New[AppConfig](ctx, fastconf.WithMetrics(prommetrics.New()))
Tracer (OpenTelemetry)

Default is no-op. OTel SDK integration lives in a sub-module:

import fastconfotel "github.com/fastabc/fastconf/observability/otel"

tracer := fastconfotel.NewTracer(otel.GetTracerProvider())
mgr, _ := fastconf.New[AppConfig](ctx, fastconf.WithTracer(tracer))

Build with -tags fastconf_otel to enable enriched span attributes.

DiffReporter
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDiffReporter(fastconf.DiffReporterFunc(
        func(ctx context.Context, ev fastconf.DiffEvent) error {
            return slack.Post(ctx, ev.Diff) // runs async; never blocks reload
        },
    )),
    fastconf.WithDiffReporterQueueCap(128), // default 64
)

Each reporter has its own bounded-queue worker:

  • Enqueue is non-blocking; reload never waits on a slow reporter.
  • Queue full → event dropped, MetricsSink.EventDropped("diff-reporter") fires.
  • Manager.Close() drains workers via bgWG.Wait() — no leaks.
Policy
import "github.com/fastabc/fastconf/policy"

mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithPolicy(policy.Func[AppConfig]{
        N: "deny-debug-in-prod",
        Fn: func(_ context.Context, in policy.Input[AppConfig]) ([]policy.Violation, error) {
            if in.Config.Env == "prod" && in.Config.Debug {
                return []policy.Violation{{
                    Rule:     "deny-debug-in-prod",
                    Path:     "debug",
                    Message:  "debug mode must be false in prod",
                    Severity: policy.SeverityError, // aborts reload
                }}, nil
            }
            return nil, nil
        },
    }),
)

CUE and OPA implementations live in policy/cue and policy/opa.

Severity Plan behaviour Reload behaviour
SeverityWarning logged, continues logged, continues
SeverityError downgraded to warning (dry-run collects everything) aborts reload; old state preserved

Multi-tenant & presets

TenantManager[T]
tm := fastconf.NewTenantManager[AppConfig]()

mgrA, _ := tm.Add(ctx, "tenant-a",
    fastconf.WithDir("/etc/config/tenant-a"),
    fastconf.WithProfileEnv("TENANT_A_PROFILE"),
)
mgrB, _ := tm.Add(ctx, "tenant-b",
    fastconf.WithDir("/etc/config/tenant-b"),
    fastconf.WithProvider(tenantBVaultProvider),
)

app, err := tm.Get("tenant-a") // *AppConfig, error (fastconf.ErrUnknownTenant)
_ = tm.Remove("tenant-a")      // calls the underlying Manager.Close()
tm.Close()

Each tenant is fully isolated; AuditSink receives Cause.Tenant = id.

Presets
// Standard Kubernetes ConfigMap deployment.
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.PresetK8s(fastconf.K8sOpts{
        Dir: "/etc/config", ProfileEnv: "APP_PROFILE", Default: "default", Watch: true,
    }),
    fastconf.WithStrict(false), // override the preset's strict=true
)

// fastconfd sidecar.
fastconf.PresetSidecar(fastconf.SidecarOpts{
    Dir: "/etc/fastconfd", HistoryN: 16, Watch: true, Strict: false,
})

// Test fixture: an in-process fs.FS for a known profile.
fastconf.PresetTesting(fastconf.TestingOpts{
    FS:      memFS,        // fs.FS
    Profile: "testing",
})

// Region / zone / host axis overlays.
fastconf.PresetHierarchical(fastconf.HierarchicalOpts{ /* ... */ })

Sub-module ecosystem

Shipped with the root module (same version, regular import)
Package Path Notes
contracts contracts Public interfaces: Provider / Codec / Source / Event
pkg/* pkg/{decoder,discovery,feature,flog,generator,mappath,merger,migration,profile,provider,transform,validate} Reusable primitives
internal/* internal/{debounce,obs,typeinfo,watcher} Compile-time API boundary
http providers/http HTTP / SSE provider (build tag no_provider_http)
vault providers/vault HashiCorp Vault KV v2 (build tag no_provider_vault)
consul providers/consul Consul KV (build tag no_provider_consul)
policy policy Policy interface + Func adapter
integrations/bus integrations/bus Configuration change bus
integrations/render integrations/render Template render extension
cmd/fastconfd cmd/fastconfd Sidecar HTTP + SSE service
Independent sub-modules (go get as needed)
Sub-module Path Tag prefix Primary dependency
validate/playground validate/playground validate/playground/vX.Y.Z go-playground/validator
prometheus observability/metrics/prometheus observability/metrics/prometheus/vX.Y.Z prometheus/client_golang
otel observability/otel observability/otel/vX.Y.Z OpenTelemetry SDK
cue-policy policy/cue policy/cue/vX.Y.Z cuelang.org/go
opa-policy policy/opa policy/opa/vX.Y.Z open-policy-agent/opa
cue-validate validate/cue/cuelang validate/cue/cuelang/vX.Y.Z cuelang.org/go
log/phuslu integrations/log/phuslu integrations/log/phuslu/vX.Y.Z phuslu/log
log/zerolog integrations/log/zerolog integrations/log/zerolog/vX.Y.Z rs/zerolog
nats provider providers/nats providers/nats/vX.Y.Z root module only (caller injects nats.Conn)
redis-streams provider providers/redisstream providers/redisstream/vX.Y.Z root module only (caller injects redis client)
s3 provider providers/s3 providers/s3/vX.Y.Z AWS SDK v2 (load + ETag short-circuit, FromURL helper)
s3events provider providers/s3events providers/s3events/vX.Y.Z AWS SDK v2 SQS (EventBridge S3 → SQS watch sibling)
openfeature integrations/openfeature integrations/openfeature/vX.Y.Z OpenFeature SDK
cmd/fastconfctl cmd/fastconfctl cmd/fastconfctl/vX.Y.Z root module only
cmd/fastconfgen cmd/fastconfgen cmd/fastconfgen/vX.Y.Z yaml.v3

Tag every sub-module at once via tools/tag-release.sh:

./tools/tag-release.sh vX.Y.Z          # local tags only
./tools/tag-release.sh vX.Y.Z --push   # push and trigger release.yml
./tools/tag-release.sh vX.Y.Z --force --push

Extension guide

Custom Provider
type RedisProvider struct {
    client *redis.Client
    key    string
    ch     chan contracts.Event
}

func (p *RedisProvider) Name()     string { return "redis:" + p.key }
func (p *RedisProvider) Priority() int    { return contracts.PriorityKV }

func (p *RedisProvider) Load(ctx context.Context) (map[string]any, error) {
    raw, err := p.client.Get(ctx, p.key).Bytes()
    if err != nil { return nil, err }
    var out map[string]any
    return out, json.Unmarshal(raw, &out)
}

func (p *RedisProvider) Watch(ctx context.Context) (<-chan contracts.Event, error) {
    go p.watchLoop(ctx)
    return p.ch, nil
}

func init() {
    fastconf.RegisterProviderFactory("redis", func(cfg map[string]any) (contracts.Provider, error) {
        return NewRedisProvider(cfg["addr"].(string), cfg["key"].(string))
    })
}
Custom Transformer
type PrefixTransformer struct{ Prefix string }

func (t PrefixTransformer) Name() string { return "prefix:" + t.Prefix }
func (t PrefixTransformer) Transform(root map[string]any) error {
    if v, ok := root["app_name"].(string); ok {
        root["app_name"] = t.Prefix + "-" + v
    }
    return nil
}

fastconf.WithTransformers(PrefixTransformer{Prefix: "myorg"})
Custom Codec

YAML, JSON, and TOML are registered automatically. Register a new format like this:

fastconf.RegisterCodec("hcl", hclCodec{})
fastconf.RegisterCodecExt("hcl", "hcl") // .hcl files route to "hcl"
Picking an extension point
Need Use
Add a data source implement contracts.Provider
Rewrite the merged tree implement Transformer
Decrypt leaves before decode implement SecretResolver
Type-rewrite leaves before decode implement decoder.TypedHook
Assert after decode WithValidator / WithPolicy
Act on successful publish AuditSink / DiffReporter
Add a file format implement contracts.Codec + RegisterCodec

CLI tools

fastconfd — sidecar service
fastconfd --dir=/etc/config --profile=prod --addr=:8081
Endpoint Method Description
/healthz GET {"status":"ok","generation":N}
/version GET Current state version (Hash + Generation)
/config GET Current config JSON (secrets redacted)
/reload POST Trigger a manual reload; accepts {"request_id":"…"}
/events GET SSE stream of ReloadCause JSON on every successful reload
fastconfctl — admin CLI
fastconfctl snapshot --addr=:8081
fastconfctl reload   --addr=:8081 --request-id=deploy-123
fastconfctl plan     --addr=:8081
fastconfctl rollback --addr=:8081 --generation=42
fastconfctl sources  --addr=:8081
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
BenchmarkIntrospectCold 1.67 µs/op
BenchmarkExplainDeep 219 ns/op

Full baseline, command lines, and explanation: docs/design/perf.md.

The contract is: hot reads are essentially free; reload may fail but never publishes a half-built state; subscriber fan-out never blocks the read path.


Development

# Dependencies
go mod tidy

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

# Examples
go test ./... -run '^Example' -v

# Benchmarks
go test -bench=BenchmarkGet -benchmem ./...

# CI guards
bash tools/check-layout.sh
bash tools/check-doc-symbols.sh
bash tools/check-deps.sh
bash tools/bench-guard.sh
bash tools/loc-budget.sh
bash tools/total-loc-budget.sh

# Code-review dependency graph
bash tools/code-review-graph.sh

Documentation map

Doc Purpose
docs/cookbook/README.md Single entry point for every recipe
docs/design/spec.md Runtime model, concurrency, module boundaries
docs/design/perf.md Latest benchmark baseline
CHANGELOG.md Release notes
pkg.go.dev godoc and runnable examples

Common recipes:


License

MIT License

Copyright (c) 2026 FastAbc

See LICENSE.

Documentation

Overview

Package fastconf provides a strongly typed, lock-free, Kustomize-style configuration loader built on Go 1.26 generics.

Start here

A typical application reads FastConf in this order:

  • Build a Manager[T] with New.
  • 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, 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

  • Loading and overlays: New, Option, PresetK8s, PresetSidecar, WithProvider, WithProfile, WithMultiAxisOverlays.
  • Runtime reaction: Subscribe, Manager.Errors, Manager.Watcher, DiffReporter.
  • Inspection and recovery: Manager.Snapshot, State.Introspect, State.Explain, 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
integrations/log/phuslu, integrations/log/zerolog, integrations/openfeature
observability/metrics/prometheus, observability/otel
policy/cue, policy/opa
providers/nats, providers/redisstream
validate/cue/cuelang, validate/playground

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

Key files

manager.go            — Manager[T] lifecycle + serialized reload loop
provider_watch.go     — provider event subscription + resume fallback
pipeline.go           — assemble / commit / Plan / codec registry
pipeline_stages.go    — canonical merge→policy stage definitions
state.go              — State[T], provenance, history, diff, watcher views
options.go            — WithXxx option builders
feature.go            — feature-rule extraction + Eval[T,V]
introspect.go         — dotted-key diagnostics + Sub[T,M]
obs_audit.go          — audit sinks and JSON audit output
obs_metrics.go        — metrics extension points and bridge
obs_tracer.go         — tracing extension points and noop tracer
errors.go             — public sentinel errors and reload error stream
watch.go / watcher.go — subscriptions + file-system watcher runtime
Example (Basic)

Example_basic demonstrates loading a profile overlay from a config directory.

package main

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

	"github.com/fastabc/fastconf"
)

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

// Example_basic demonstrates loading a profile overlay from a config directory.
func main() {
	root := mustExampleTempDir("example-basic-")
	defer os.RemoveAll(root)

	confDir := filepath.Join(root, "conf.d")
	mustWriteExampleFile(filepath.Join(confDir, "base", "00-app.yaml"), "server:\n  addr: \":8080\"\ndatabase:\n  pool: 10\n")
	mustWriteExampleFile(filepath.Join(confDir, "overlays", "prod", "10-app.yaml"), "server:\n  addr: \":8443\"\ndatabase:\n  pool: 32\n")

	restoreEnv := mustSetExampleEnv("APP_PROFILE", "prod")
	defer restoreEnv()

	mgr, err := fastconf.New[basicExampleConfig](context.Background(),
		fastconf.WithDir(confDir),
		fastconf.WithProfileEnv("APP_PROFILE"),
		fastconf.WithDefaultProfile("dev"),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	app := mgr.Get()
	fmt.Printf("%s %d %d\n", app.Server.Addr, app.Database.Pool, len(mgr.Snapshot().Sources))
}

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

func mustSetExampleEnv(key, value string) func() {
	old, ok := os.LookupEnv(key)
	if err := os.Setenv(key, value); err != nil {
		panic(err)
	}
	return func() {
		if !ok {
			_ = os.Unsetenv(key)
			return
		}
		_ = os.Setenv(key, old)
	}
}
Output:
:8443 32 2
Example (ExternalSource)

Example_externalSource demonstrates the two complementary extension points for fastconf:

  • WithSource(Source, Parser) for byte-blob layers (file / http / inline bytes) where the decoder is named at the call site;
  • WithProvider for already-structured contributors (env / cli / KV with one key per setting).
package main

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

	"github.com/fastabc/fastconf"
	"github.com/fastabc/fastconf/contracts"
	"github.com/fastabc/fastconf/pkg/parser"
	"github.com/fastabc/fastconf/pkg/source"
)

type externalSourceExampleConfig struct {
	Server struct {
		Addr string `yaml:"addr" json:"addr"`
	} `yaml:"server" json:"server"`
	Feature struct {
		BetaEnabled bool `yaml:"betaEnabled" json:"betaEnabled"`
	} `yaml:"feature" json:"feature"`
}

// staticExampleProvider implements contracts.Provider directly because
// its data is already structured (no bytes → decode required). The Source
// contract is the alternative used by byte-blob layers (see seed below).
type staticExampleProvider struct {
	name     string
	priority int
	data     map[string]any
}

func (p *staticExampleProvider) Name() string  { return p.name }
func (p *staticExampleProvider) Priority() int { return p.priority }

func (p *staticExampleProvider) Load(context.Context) (map[string]any, error) {
	out := make(map[string]any, len(p.data))
	for k, v := range p.data {
		out[k] = v
	}
	return out, nil
}

func (p *staticExampleProvider) Watch(context.Context) (<-chan contracts.Event, error) {
	return nil, nil
}

// Example_externalSource demonstrates the two complementary extension
// points for fastconf:
//
//   - WithSource(Source, Parser) for byte-blob layers (file / http /
//     inline bytes) where the decoder is named at the call site;
//   - WithProvider for already-structured contributors (env / cli /
//     KV with one key per setting).
func main() {
	demo := &staticExampleProvider{
		name:     "demo-static",
		priority: contracts.PriorityKV,
		data: map[string]any{
			"server":  map[string]any{"addr": ":9090"},
			"feature": map[string]any{"betaEnabled": true},
		},
	}

	// Inline byte-blob layer paired with an explicit Parser. To let
	// `demo` (PriorityKV=30) supply the override, push the seed below
	// it via WithPriority.
	seed := source.NewBytes("seed", "yaml",
		[]byte("server:\n  addr: \":8080\"\nfeature:\n  betaEnabled: false\n"),
	).WithPriority(contracts.PriorityStatic)

	mgr, err := fastconf.New[externalSourceExampleConfig](context.Background(),
		fastconf.WithFS(fstest.MapFS{
			"conf.d/base/.keep": &fstest.MapFile{Data: []byte("")},
		}),
		fastconf.WithSource(seed, parser.YAML()),
		fastconf.WithProvider(demo),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	app := mgr.Get()
	fmt.Printf("%s %t\n", app.Server.Addr, app.Feature.BetaEnabled)
}
Output:
:9090 true
Example (Sidecar)

Example_sidecar demonstrates a sidecar-style manager using the preset bundle.

package main

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

	"github.com/fastabc/fastconf"
)

type sidecarExampleConfig struct {
	HTTP struct {
		Addr string `yaml:"addr" json:"addr"`
	} `yaml:"http" json:"http"`
}

// Example_sidecar demonstrates a sidecar-style manager using the preset bundle.
func main() {
	root := mustExampleTempDir("example-sidecar-")
	defer os.RemoveAll(root)

	confDir := filepath.Join(root, "conf.d")
	configPath := filepath.Join(confDir, "base", "00-sidecar.yaml")
	mustWriteExampleFile(configPath, "http:\n  addr: \":8650\"\n")

	mgr, err := fastconf.New[sidecarExampleConfig](context.Background(),
		fastconf.PresetSidecar(fastconf.SidecarOpts{
			Dir:      confDir,
			HistoryN: 2,
			Watch:    false,
			Strict:   true,
		}),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer mgr.Close()

	mustWriteExampleFile(configPath, "http:\n  addr: \":8651\"\n")
	if err := mgr.Reload(context.Background()); err != nil {
		fmt.Println(err)
		return
	}
	history := mgr.Replay().List()
	fmt.Printf("%s %d\n", mgr.Get().HTTP.Addr, len(history))
}
Output:
:8651 1

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 = "conf.d"

	// DefaultProfileEnv is the environment variable FastConf reads when
	// neither WithProfile nor WithProfileEnv is provided.
	DefaultProfileEnv = "APP_PROFILE"
)

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    = coalesce.DefaultQuiet
	DefaultCoalesceMaxLag   = coalesce.DefaultMaxLag
	DefaultCoalesceSwapHint = coalesce.DefaultSwapHint
)

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 and the WithCoalesceQuiet / WithCoalesceMaxLag / WithCoalesceSwapHint options for the runtime overrides.

View Source
const (
	BridgeJSON = bridgeJSON
	BridgeYAML = bridgeYAML
)

BridgeJSON and BridgeYAML are the exported aliases for use with WithCodecBridge.

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

CoalesceProfile values mirroring the internal/coalesce constants.

View Source
const DefaultSidecarHistoryCap = 16

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

Variables

View Source
var (
	// ErrNoSources is returned when discovery + providers produced no layers.
	ErrNoSources = newFCErr("fastconf: no configuration sources discovered")
	// ErrValidation indicates *T failed structural validation.
	ErrValidation = newFCErr("fastconf: validation failed")
	// ErrDecode indicates a layer could not be decoded.
	ErrDecode = newFCErr("fastconf: decode failed")
	// ErrMerge indicates the deep-merge stage rejected an inconsistency.
	ErrMerge = newFCErr("fastconf: merge failed")
	// ErrPatch indicates an RFC 6902 patch failed to apply.
	ErrPatch = newFCErr("fastconf: patch failed")
	// ErrClosed indicates the Manager has been closed.
	ErrClosed = newFCErr("fastconf: manager closed")
	// ErrValidator indicates a WithValidator callback returned an error.
	ErrValidator = newFCErr("fastconf: validator failed")
	// ErrTransform indicates a WithTransformers callback returned an error.
	ErrTransform = newFCErr("fastconf: transform failed")
	// ErrNoOrigin indicates LookupStrict found no provenance for path.
	ErrNoOrigin = newFCErr("fastconf: no origin for path")
)

Package-level sentinels. Every error returned from the public API satisfies errors.Is against one of these AND against ErrFastConf.

View Source
var ErrFastConf = errors.New("fastconf")

ErrFastConf is the umbrella sentinel for every error returned by the FastConf framework. Every public Err* below chains to it via an Is method so callers can write a single catch-all clause:

if errors.Is(err, fastconf.ErrFastConf) { ... }

Centralised error hierarchy. Each sentinel is implemented as *fcErr; errors.Is matches both the specific sentinel (by pointer identity) and ErrFastConf (via fcErr.Is).

View Source
var ErrHistoryDisabled = errors.New("fastconf: history disabled")

ErrHistoryDisabled is returned when history APIs are called but WithHistory was not used.

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 = errors.New("fastconf: policy denied")

ErrPolicyDenied is returned by reload() when one or more SeverityError violations fired. The error message lists every violation; callers can inspect the structured slice via errors.As(err, *PolicyError).

View Source
var ErrTenantExists = errors.New("fastconf: tenant already registered")

ErrTenantExists is returned by Add when the tenant id is already registered. Callers must Remove the prior instance first if they want to swap configuration atomically.

View Source
var ErrUnknownGeneration = errors.New("fastconf: unknown generation")

ErrUnknownGeneration is returned by Rollback when the requested generation is not in the in-memory history ring.

View Source
var ErrUnknownTenant = errors.New("fastconf: unknown tenant")

ErrUnknownTenant is returned by Get/Remove for ids that were never added. Use Has() if you need a check that does not allocate or surface an error.

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(_ string, _ any) any

DefaultSecretRedactor replaces the value with "***REDACTED***".

func Eval

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

Eval looks up a feature rule by key against the live *State[T] feature table, evaluates it under ctx, and returns the rule value if it matches V (the typed default's type). Returns def in any of these cases:

  • m or its current state is nil
  • WithFeatureRules was never configured (no rule table)
  • The rule for key is missing
  • The rule value cannot be type-asserted to V

Eval is zero-allocation on the hot path: one atomic snapshot load, one map lookup, optionally one deterministic hash/compare, then a typed return.

dark := fastconf.Eval[AppConfig, bool](mgr, "darkMode", flagCtx, false)

For integrations that need the raw any-typed return (OpenFeature, etc.), call feature.Eval(state.FeatureRules(), key, ctx, def) directly.

func LookupCodec

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

LookupCodec returns the codec registered under name (case-insensitive).

func RegisterCodec

func RegisterCodec(name string, c contracts.Codec)

RegisterCodec installs a third-party Codec under the given name.

func RegisterCodecExt

func RegisterCodecExt(ext, codec string)

RegisterCodecExt maps a file extension to a previously-registered codec name.

func RegisterProviderFactory

func RegisterProviderFactory(name string, f ProviderFactory)

RegisterProviderFactory adds a named factory to the process-wide default registry. Safe to call from init() across packages. Re-registering an existing name overwrites — useful for test fakes.

For test isolation or multi-tenant setups, prefer NewProviderRegistry + WithProviderRegistry instead of mutating the global.

func RegisteredProviderNames

func RegisteredProviderNames() []string

RegisteredProviderNames returns the sorted list of process-wide factory names. Per-Manager registries are not included; ask the registry instance directly via (*ProviderRegistry).Names() when debugging an isolated setup.

func Sub

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

Sub is a strongly-typed subtree accessor: given an extractor from *T to *M, it returns the live *M pointer from the current State. The returned pointer aliases State.Value and MUST be treated as read-only; mutations leak across goroutines and break the atomic- pointer invariant.

Sub mirrors fastconf.Subscribe and fastconf.Eval: every "from *T, extract M" operation is a package-level generic function. For dynamic (map[string]any) subtree access use state.Introspect().At(path).

func Subscribe

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

Subscribe registers a callback that fires on every successful reload, receiving the extracted M from the previous and new *T. Use it when you want type-safe access to a struct field (or sub-struct) of *T without reaching for reflection.

cancel := fastconf.Subscribe(mgr,
    func(c *AppConfig) *DBConfig { return &c.Database },
    func(old, new *DBConfig) {
        if old != nil && *old == *new {
            return // no real change in DBConfig — caller-side filter
        }
        reconnect(new)
    },
)
defer cancel()

The callback fires unconditionally on every commit; if the extracted M is unchanged the caller is responsible for skipping (typical pattern: compare old and new). This keeps Subscribe O(0) on the reload hot path — no per-field hashing — and lets the caller decide what "changed" means for their type.

Callbacks run synchronously on the reload goroutine. They must return quickly; any blocking I/O (RPC, lock contention, time.Sleep) postpones the next reload. Spawn a goroutine inside the callback if needed.

A panic in fn is recovered and logged; it does not poison the writer or affect other subscribers. The returned cancel removes the subscription; calling it after Close() is a no-op.

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 interface {
	Audit(ctx context.Context, cause ReloadCause) error
}

AuditSink receives a ReloadCause every time the Manager publishes a new state. Implementations MUST be goroutine-safe and SHOULD return quickly — the manager invokes Audit synchronously in the reload goroutine, so a slow sink directly inflates publish latency.

type AuditSinkFunc

type AuditSinkFunc func(context.Context, ReloadCause) error

AuditSinkFunc adapts a free function into an AuditSink.

func (AuditSinkFunc) Audit

func (f AuditSinkFunc) Audit(ctx context.Context, cause ReloadCause) error

Audit implements AuditSink.

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 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 DiffEvent

type DiffEvent struct {
	// Reason mirrors ReloadCause.Reason — "manual", "watcher",
	// "provider:vault://...", "override", etc.
	Reason string
	// PrevGeneration is the generation number of the State that was
	// just replaced; zero on the first reload.
	PrevGeneration uint64
	// NewGeneration is the generation number just published.
	NewGeneration uint64
	// At captures when the reload swap occurred.
	At time.Time
	// Diff is the human-readable list of dotted paths that changed,
	// produced by State.Diff. Empty when the previous state had a
	// different hash but identical field values (which should be rare
	// once canonicalisation has run).
	Diff []string
	// Cause is the full ReloadCause for downstream tooling that needs
	// the audit trail (revisions, tenant, request id, ...).
	Cause ReloadCause
}

DiffEvent is the payload handed to every DiffReporter.

type DiffReporter

type DiffReporter interface {
	Report(ctx context.Context, ev DiffEvent) error
}

DiffReporter receives an event after every successful reload that changed at least one field. Implementations MUST be goroutine-safe. The Report method is invoked on a fresh goroutine so it may block without affecting reload latency, but it SHOULD still bound its own time spent (e.g. with an HTTP timeout).

type DiffReporterFunc

type DiffReporterFunc func(context.Context, DiffEvent) error

DiffReporterFunc adapts a function into a DiffReporter.

func (DiffReporterFunc) Report

func (f DiffReporterFunc) Report(ctx context.Context, ev DiffEvent) error

Report implements DiffReporter.

type DiffReporterMetricsSink

type DiffReporterMetricsSink interface {
	DiffReporterQueueDepth(reporter string, depth, capacity int)
}

DiffReporterMetricsSink is the optional extension implemented by sinks that want to observe the DiffReporter backpressure pool. The framework samples each reporter's (length, capacity) after every successful commit and after each enqueue, so a Prometheus gauge can show "how close are we to dropping events?". Sinks that don't implement it are transparently ignored.

reporter is a stable identifier of the form "diff-reporter:<idx>" matching the EventDropped label used when drop-on-full fires.

type EvalContext

type EvalContext = feature.EvalContext

EvalContext re-exports pkg/feature.EvalContext so callers do not need to import a second package solely for the type.

type FeatureRule

type FeatureRule = feature.Rule

FeatureRule re-exports pkg/feature.Rule for the same reason.

type FieldSpec

type FieldSpec struct {
	Path     string
	Index    []int
	Default  string
	Required bool
	Min      *float64
	Max      *float64
	OneOf    []string
	Desc     string
}

FieldSpec captures the structured metadata parsed from a single `fastconf` tag (independent of the legacy `default=` and `secret` flags which keep their own walkers).

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 struct {
	// contains filtered or unexported fields
}

Introspection is the dotted-key / map[string]any view of a State[T]. Always obtained via state.Introspect(); never zero-value-constructed.

func (*Introspection) At

func (i *Introspection) At(path string) map[string]any

At returns every dotted key strictly underneath path (prefix stripped), as a freshly allocated map. The empty string returns the same shape as Settings().

Example: Settings = {"a.b":1,"a.c.d":2,"x":3}

At("a")   -> {"b":1, "c.d":2}
At("a.c") -> {"d":2}
At("")    -> identical to Settings()

func (*Introspection) Keys

func (i *Introspection) Keys() []string

Keys returns every dotted leaf path of the underlying *T in deterministic (lexicographic) order.

func (*Introspection) Settings

func (i *Introspection) Settings() map[string]any

Settings returns the full dotted-key map as a freshly allocated copy; callers may mutate it without affecting the snapshot.

type JSONAuditSink

type JSONAuditSink struct {
	// contains filtered or unexported fields
}

JSONAuditSink writes each cause as a single JSON line to w. It is safe for concurrent use; writes are serialized through a mutex so individual lines never interleave. The encoder is created once and reused under the lock to avoid one allocation per Audit call.

func NewJSONAuditSink

func NewJSONAuditSink(w io.Writer) *JSONAuditSink

NewJSONAuditSink returns a sink that writes to w (defaults to os.Stderr when w is nil).

func (*JSONAuditSink) Audit

func (s *JSONAuditSink) Audit(_ context.Context, cause ReloadCause) error

Audit implements AuditSink.

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 uint8

LayerKind identifies the merge semantics of a layer.

const (
	// LayerUnknown is the zero-value placeholder.
	LayerUnknown LayerKind = iota
	// LayerMerge is a standard deep-merge layer.
	LayerMerge
	// LayerPatch is an RFC 6902 JSON Patch layer.
	LayerPatch
	// LayerProvider is a layer injected by a Provider (env/cli/kv/...).
	LayerProvider
	// LayerSecret marks a per-field plaintext supplied by a SecretResolver
	// (SOPS / Vault transit / KMS / age).
	LayerSecret
)

func (LayerKind) String

func (k LayerKind) String() string

String returns a human-readable name for the LayerKind.

type Manager

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

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

Typical usage:

cfg, err := fastconf.New[MyConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithProfileEnv("APP_PROFILE"),
    fastconf.WithProvider(provider.NewEnv("APP_")),
    fastconf.WithWatch(true),
)
defer cfg.Close()
app := cfg.Get()

Internally Manager serializes the write path (one reload goroutine) while keeping the read path completely lock-free.

func New

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

New constructs a Manager and runs the first reload synchronously. On failure no goroutine is started.

Once construction succeeds, read with Get, react with Subscribe and Errors, preview future changes with Plan, and recover retained snapshots through Replay when WithHistory was configured.

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

Close shuts the Manager down gracefully. Idempotent. After Close returns, the channel from Errors() is closed; consumers iterating with `for re := range m.Errors()` exit cleanly.

func (*Manager[T]) Errors

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

Errors returns a buffered channel that publishes one ReloadError per failed reload attempt. The channel has a fixed capacity; if the consumer cannot keep up, the oldest pending error is dropped so the reload loop never blocks. Closed by Close.

Note: the synchronous error returned by Reload(ctx, ...) (and Plan() failures) is also published here, so a consumer can centralise error handling without checking both paths.

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

Get returns a pointer to the current snapshot's value. Zero allocation, O(1), lock-free. The returned value MUST be treated as read-only.

func (*Manager[T]) Plan

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

Plan opens a dry-run builder. The actual preview executes when Run is called; nothing happens beforehand.

result, err := m.Plan().
    WithHostname("prod-eu-1").
    Run(ctx)
Example

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

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)
Output:
1 :9090 :8080

func (*Manager[T]) Reload

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

Reload triggers a synchronous reload. On failure the previous state is preserved.

Options:

  • WithSourceOverride(map) injects a one-shot in-memory layer at the top of the priority stack for this reload only. The map is consumed; do not mutate it after the call.
  • WithReloadReason(s) overrides the default "manual" reason tag used for audit / metrics / logging.

func (*Manager[T]) Replay

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

Replay is the sub-namespace accessor that exposes time-travel operations on the Manager's history ring (configured via WithHistory). Returns a zero-cost view; methods short-circuit when history is disabled.

for _, s := range m.Replay().List() {
    fmt.Println(s.Generation, s.Hash)
}
_ = m.Replay().Rollback(prev)

func (*Manager[T]) Snapshot

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

Snapshot returns the full immutable State[T] snapshot used for diagnostics and fingerprint comparisons.

func (*Manager[T]) Watcher

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

Watcher is the sub-namespace accessor for watch-loop control. Returns a zero-cost view onto Manager. Methods are nil-safe-ish: they only operate on the watchPaused atomic, which is always present.

m.Watcher().Pause()
defer m.Watcher().Resume()

type MetricsSink

type MetricsSink interface {
	ReloadStarted()
	ReloadFinished(ok bool, dur time.Duration)
	StateGeneration(gen uint64)
	LayersTotal(n int)
}

MetricsSink is the minimal interface fastconf calls during reload. The default implementation is no-op so that metrics impose zero dependency on the user. A Prometheus implementation is provided in the observability/metrics/prometheus sub-module.

type MigrationApplier

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

MigrationApplier rewrites the merged configuration tree before transformers and decode run. The single-method shape lets a plain function adapt via MigrationFunc.

The reload pipeline invokes Migrate exactly once per reload on the single writer goroutine; implementations therefore do not need to be safe for concurrent calls. Returning an error aborts the reload and preserves the previous *State[T].

type MigrationFunc

type MigrationFunc func(map[string]any) error

MigrationFunc adapts a plain function to MigrationApplier.

func (MigrationFunc) Migrate

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

Migrate implements MigrationApplier.

type Option

type Option func(*options)

Option configures Manager behavior.

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

WithAuditSink installs an AuditSink invoked once per successful reload. May be combined freely with other Options; multiple WithAuditSink calls register multiple sinks (fan-out, in order).

func WithCoalesceMaxLag added in v0.16.0

func WithCoalesceMaxLag(d time.Duration) Option

WithCoalesceMaxLag sets the absolute upper bound on a watcher event burst's lifetime. Once exceeded the burst is force-flushed even if new events keep arriving. Default: DefaultCoalesceMaxLag (250 ms).

func WithCoalesceProfile added in v0.16.0

func WithCoalesceProfile(p coalesce.Profile) Option

WithCoalesceProfile applies all three coalescer windows from a preset. Use ProfileK8s (default) in production and ProfileLocalDev when iterating against editors that write via unlink-rename cascades.

func WithCoalesceQuiet added in v0.16.0

func WithCoalesceQuiet(d time.Duration) Option

WithCoalesceQuiet sets the silent-window after which a watcher event burst on a single parent directory fires a reload. Default: DefaultCoalesceQuiet (30 ms). The window is reset by every new event in the same burst, subject to MaxLag.

func WithCoalesceSwapHint added in v0.16.0

func WithCoalesceSwapHint(d time.Duration) Option

WithCoalesceSwapHint sets the much shorter quiet window applied once a Kubernetes ConfigMap atomic-swap commit is detected (CREATE/RENAME on "..data" or "..data_tmp_*"). Trailing CHMOD events on inner symlinks do not extend the window further. Default: DefaultCoalesceSwapHint (5 ms). Clamped to <= Quiet.

func WithCodecBridge

func WithCodecBridge(b codecBridge) Option

WithCodecBridge selects how the merged map[string]any is round-tripped into *T. The default bridgeJSON pairs with the SHA-256 hash so a reload only marshals the document once. Choose BridgeYAML if your configuration struct only carries yaml tags and you cannot add json tags; this is the v0.6 behaviour and slightly slower.

func WithDefaultProfile

func WithDefaultProfile(p string) Option

WithDefaultProfile sets the fallback profile when no explicit profile exists.

func WithDefaulterFunc

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

WithDefaulterFunc 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 runs at the same point as the interface check: after struct-tag defaults and before validators.

func WithDiffReporter

func WithDiffReporter(r DiffReporter) Option

WithDiffReporter installs a reporter invoked asynchronously after every successful reload that produced a non-empty diff. Multiple reporters can be installed; each runs on its own dedicated worker goroutine fed by a bounded queue.

Backpressure: events are enqueued non-blockingly. When a reporter's queue is full (slow reporter + high reload churn) the event is DROPPED and EventDropped("diff-reporter") is reported to the MetricsSink. Reload throughput is therefore independent of reporter latency.

Tune the per-reporter queue depth with WithDiffReporterQueueCap.

func WithDiffReporterQueueCap

func WithDiffReporterQueueCap(n int) Option

WithDiffReporterQueueCap sets the per-reporter bounded queue depth used for backpressure when fan-out cannot keep up with reload churn. Default is defaultDiffReporterQueueCap (64). n < 1 is clamped to 1.

func WithDir

func WithDir(dir string) Option

WithDir sets the configuration root directory. Default: DefaultDir.

func WithDotEnvAuto

func WithDotEnvAuto(prefix string) Option

WithDotEnvAuto auto-discovers ".env" files in the config directory (WithDir value) and the current working directory, loading them as the lowest-priority provider.

Resolution is deferred to the end of option application so option order no longer matters — WithDotEnvAuto("APP_") placed before WithDir("conf.d") works correctly. The prefix is stashed and resolved once just before New() builds its Manager. This is the one Option whose mechanics cannot be replaced by a single WithProvider call (because it needs the final o.dir value). For structured contributors use WithProvider with provider.NewEnv / NewDotEnv / NewLabels / NewCLI; for byte-blob layers use WithSource with source.NewFile / NewHTTP / NewBytes.

func WithFS

func WithFS(f fs.FS) Option

WithFS sets the fs.FS used to load configs. It overrides WithDir.

func WithFeatureRules

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

WithFeatureRules attaches a per-reload rule extractor to the Manager. The extractor is invoked at the end of every successful reload to derive a map[string]feature.Rule from the freshly committed *T; the result is stamped onto the new State[T] so future Eval() calls are O(1) atomic loads.

Pass a closure that pulls the rules table out of your config struct:

type AppConfig struct {
    Features map[string]feature.Rule `json:"features"`
}
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithFeatureRules[AppConfig](func(c *AppConfig) map[string]feature.Rule {
        return c.Features
    }),
)

Without WithFeatureRules, Manager.Eval always returns the supplied default.

func WithGenerator

func WithGenerator(g contracts.Generator) Option

WithGenerator registers a Source generator that runs during the assemble stage of every reload. Generators synthesise layers dynamically (Kustomize ConfigMapGenerator / SecretGenerator style): inject build info, query a downward-api volume, or shell out for a JSON blob. A failing generator aborts the reload and preserves the previous *State[T]. See contracts.Generator.

func WithHistory

func WithHistory(n int) Option

WithHistory keeps the last n successfully committed states in an in-memory ring buffer so Manager.Rollback / Manager.History can surface them. The default (0) disables history. Each retained state holds one *T plus its sources slice, so size the buffer with care for very large configs.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger injects the logger used by FastConf. The default discards every log line (io.Discard), so callers must opt in to see them. Pass nil to keep the current default.

Any slog.Handler-backed *slog.Logger works: stdlib JSON/Text handlers, the phuslu adapter under integrations/log/phuslu, the zerolog adapter under integrations/log/zerolog, or any third-party Handler. Internally FastConf wraps the logger in pkg/flog for zerolog-style fluent calls, so swapping the backend never affects call-site code.

If you already have an slog.Handler instead of *slog.Logger, wrap it:

fastconf.WithLogger(slog.New(myHandler))

func WithMergeKeys

func WithMergeKeys(keys map[string]string) Option

WithMergeKeys installs Kustomize-style strategic merge keys without requiring a _meta.yaml file. Each entry maps a dotted path in the merged tree to the field name that identifies "the same item" across overlays. Programmatic option values are merged with any _meta.yaml mergeKeys; programmatic entries win on conflict.

func WithMetrics

func WithMetrics(m MetricsSink) Option

WithMetrics injects the metrics sink used by the reload pipeline.

func WithMigrations

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

WithMigrations installs a schema-migration callback that runs after the merged map is assembled but before transformers and decode. It addresses the long-lived-config / evolving-struct mismatch by letting operators rename or restructure ageing keys on the fly.

func WithMultiAxisOverlays

func WithMultiAxisOverlays(axes ...OverlayAxis) Option

WithMultiAxisOverlays registers one or more overlay axes. Each axis maps an environment variable to a subdirectory under the config root. When the environment variable is set to a name that matches an existing subdirectory, that subdirectory's files are loaded as additional file layers at the declared priority level, after base and overlays but before providers.

Axes are loaded in declaration order; assign increasing Priority values to establish a clear override hierarchy (e.g. regions < zones < hosts).

Directories that do not exist are silently skipped.

When DefaultFromHostname is true on an axis, os.Hostname() is used as the axis value if the environment variable is absent (not set). The env var still takes precedence when present, and an explicitly empty env var disables the axis.

Usage:

fastconf.New[Config](ctx,
    fastconf.WithDir("config"),
    fastconf.WithMultiAxisOverlays(
        fastconf.OverlayAxis{Dir: "regions", EnvVar: "REGION", Priority: 3000},
        fastconf.OverlayAxis{Dir: "zones",   EnvVar: "ZONE",   Priority: 3100},
        fastconf.OverlayAxis{Dir: "hosts",   EnvVar: "HOST",   Priority: 3200, DefaultFromHostname: true},
    ),
)

func WithPolicy

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

WithPolicy registers a typed Policy[T] that is evaluated on the reload goroutine after decode + validation but BEFORE the atomic state swap. Multiple WithPolicy calls fan-out (all policies run, findings aggregate). A SeverityError finding aborts the reload and the previous *State[T] remains in place — the failure-safe invariant is preserved.

Use:

mgr, err := fastconf.New[MyApp](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithPolicy(policy.Func[MyApp]{
        N: "deny-debug-in-prod",
        Fn: func(_ context.Context, in policy.Input[MyApp]) ([]policy.Violation, error) {
            if in.Config.Profile == "prod" && in.Config.Debug {
                return []policy.Violation{{Path: "debug", Severity: policy.SeverityError}}, nil
            }
            return nil, nil
        },
    }),
)

func WithProfile

func WithProfile(p string) Option

WithProfile sets the active overlay profile explicitly.

func WithProfileEnv

func WithProfileEnv(name string) Option

WithProfileEnv sets the environment variable used to resolve the profile.

func WithProfileExpr

func WithProfileExpr(expr string) Option

WithProfileExpr appends a global match expression to every overlay.

func WithProfiles

func WithProfiles(p ...string) Option

WithProfiles activates the multi-profile model. When at least one profile is supplied, FastConf evaluates each overlay subdirectory's optional `_meta.yaml.match:` boolean expression against this active set instead of the legacy single-profile lookup. Subdirectories without a match expression fall back to membership: they are included iff their directory name is one of the supplied profiles. WithProfile remains supported for the simple single-tag case and is preserved as a fallback when WithProfiles is not used.

func WithProvenance

func WithProvenance(level ProvenanceLevel) Option

WithProvenance enables field-level origin tracking at the requested level. The default (ProvenanceOff) keeps the reload pipeline allocation-free; ProvenanceTopLevel adds O(top-level keys) work and ProvenanceFull adds O(leaves). Once enabled, Manager.Snapshot().Origins().Explain("a.b.c") returns the chain of layers that wrote to that path, oldest→newest.

func WithProvider

func WithProvider(p contracts.Provider) Option

WithProvider registers an external provider merged after file layers.

func WithProviderByName

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

WithProviderByName resolves a provider through the registry and installs it. It is the dynamic counterpart to WithProvider, useful when the provider list comes from configuration rather than code.

Resolution is deferred until all Options have been applied, so the per-Manager registry (WithProviderRegistry) may appear in any order relative to WithProviderByName.

Missing factory names and factory errors are recorded as deferred option errors; New() surfaces them before starting any goroutine.

func WithProviderOrdered

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

WithProviderOrdered is a let-me-keep-it-simple helper for users who prefer the Viper "last call wins" mental model over FastConf's explicit Priority() integers. It wraps each supplied provider in a thin priorityOverride that assigns a strictly increasing priority starting just above PriorityCLI, so providers later in the argument list always win.

Use it when you have a fixed call order and don't want to think about the priority table. For mixed deployments (file + env + multiple remote providers) the explicit Priority() approach is still clearer.

func WithProviderRegistry

func WithProviderRegistry(r *ProviderRegistry) Option

WithProviderRegistry installs a Manager-local ProviderRegistry. When set, WithProviderByName resolves names against this registry first, then falls back to the process-wide default.

Use cases:

  • Multi-tenant: each tenant has its own factory set without touching the global registry.
  • Tests: install fakes for a single test without race-y mutation of process state.
  • Plugin sandboxing: a sub-system can declare exactly which providers it allows to be wired in by configuration.

func WithRawMapAccess

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

WithRawMapAccess installs a read hook that is called with the fully merged map[string]any immediately after all transformers run and just before the map is decoded into *T via the configured codec bridge.

Downstream adapters use this hook to work around type-mismatch issues that the codec bridge cannot resolve on its own:

  • Extract a sub-tree (e.g. "protocols") as raw data to populate a json.RawMessage field without going through a yaml.Marshal / Unmarshal round-trip that loses type information.
  • Read string-form values (e.g. "30s") that json.Unmarshal cannot convert natively into time.Duration fields, and use them alongside a separate validator or defaulter.

The callback is invoked synchronously on the single reload goroutine. The map argument is the live merged tree — callers MUST NOT retain a reference beyond the call or mutate the map. Use WithTransformers if mutation of the merged tree before decode is required.

Example — capture the raw "protocols" sub-tree so a validator can convert it to json.RawMessage independent of the codec bridge:

var rawProtocols map[string]any
fastconf.New[Config](ctx,
    fastconf.WithRawMapAccess(func(root map[string]any) {
        if p, ok := root["protocols"].(map[string]any); ok {
            rawProtocols = p
        }
    }),
    fastconf.WithValidator(func(cfg *Config) error {
        if rawProtocols != nil {
            b, _ := json.Marshal(rawProtocols)
            cfg.Protocols = b
        }
        return nil
    }),
)

func WithSecretRedactor

func WithSecretRedactor(r SecretRedactor) Option

WithSecretRedactor installs the secret redactor used by dumps and snapshots.

func WithSecretResolver

func WithSecretResolver(r SecretResolver) Option

WithSecretResolver installs a resolver that walks the merged map before decode, replacing every recognised reference with its plaintext. Decryption errors abort the reload (failure-safe).

func WithSource added in v0.16.0

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

WithSource registers a byte-stream Source paired with a Parser. Internally Bind composes them into a Provider, so a Source is a first-class participant of the merge order alongside Provider.

Use this for byte-blob sources (file, http, inline bytes) where the decoder is named at the call site for discoverability:

fastconf.WithSource(file.New("/etc/app/config.yaml"), yaml.Parser())

Pass a nil Parser to defer parser selection to the registry: the content-type hint returned by Source.Read picks the parser automatically (file extension for FileSource, Content-Type header for HTTPSource, the contentType ctor argument for BytesSource).

For already-structured sources (env, cli, kv-with-one-key-per-setting) continue to use WithProvider directly; there is no Parser to attach.

func WithStrict

func WithStrict(strict bool) Option

WithStrict enables strict file and merge validation.

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. The framework opens spans for the reload root plus seven stages: assemble, merge, migration, transform, decode, validate, commit. Pass nil to keep the default no-op tracer.

func WithTransformers

func WithTransformers(t ...Transformer) Option

WithTransformers appends post-merge / pre-decode transformers to the reload pipeline. They run in order, after every layer has been merged/patched but before the merged tree is decoded into the user's strongly-typed *T. A failing transformer aborts the reload and the previous state is preserved.

Transformers are designed to host cross-cutting concerns such as applying defaults, env-var interpolation, key aliases / deprecations, and stripping operator-only fields.

func WithTypedHook

func WithTypedHook(h decoder.TypedHook) Option

WithTypedHook registers an additional decoder hook beyond the default Duration / IP / URL / Regex set. Hooks rewrite merged map leaves into the typed wire form that encoding/json can natively unmarshal into *T's strongly-typed fields ("30s" → int64 nanoseconds, "10.0.0.1" → canonical IP string, etc).

Hooks are evaluated in (defaults ++ extras) order; the first Match wins per field. Use WithoutDefaultTypedHooks to drop the built-in set when a project wants its own end-to-end policy.

func WithValidator

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

WithValidator registers a strongly-typed validator. Runs after the merged document has been decoded into *T but BEFORE the new state is published. If any registered validator returns an error, the reload fails atomically: the previous state is preserved and Get() continues to return the prior value.

Validators are the canonical way to enforce cross-field invariants (e.g. "if mTLS is enabled, certificateFile must be non-empty") that cannot be expressed in struct tags or JSON Schema.

Multiple validators may be registered; they run in registration order and the first error short-circuits the rest.

fastconf.New[AppConfig](ctx,
    fastconf.WithValidator(func(cfg *AppConfig) error {
        if cfg.Server.Addr == "" { return errors.New("server.addr required") }
        return nil
    }),
)

Validators must be deterministic and side-effect-free; they MAY run repeatedly during shadow loads.

func WithWatch

func WithWatch(enabled bool) Option

WithWatch enables file-system driven reloads.

func WithWatchPaths

func WithWatchPaths(paths ...string) Option

WithWatchPaths appends additional paths to watch.

func WithoutDefaultTypedHooks

func WithoutDefaultTypedHooks() Option

WithoutDefaultTypedHooks disables the built-in Duration / IP / URL / Regex hooks. Use it when the application has installed its own typed-hook policy via WithTypedHook and the defaults would conflict.

type Origin

type Origin struct {
	// Path is the dotted JSON path of the field, e.g. "database.dsn".
	Path string
	// Source is the SourceRef that contributed this value.
	Source SourceRef
	// Value is the per-layer value as it appeared in this Source's
	// contribution before downstream layers overrode it. Only populated
	// when ProvenanceFull is enabled and the value is a JSON-leaf
	// (non-map). Map values are intentionally left nil to avoid
	// retaining large subtrees.
	Value any
}

Origin identifies which configuration layer last wrote a particular dotted field path during the merge stage.

Provenance is opt-in via WithProvenance(level): the merger emits an OriginIndex only when level > ProvenanceOff; this keeps the default reload pipeline allocation-free for installations that don't need field-level "where did this come from?" answers.

type OriginIndex

type OriginIndex struct {
	// contains filtered or unexported fields
}

OriginIndex maps dotted JSON paths to the chain of layers that wrote to them, oldest first. The last element wins the merge.

func (*OriginIndex) Explain

func (o *OriginIndex) Explain(path string) []Origin

Explain returns the chain of layers that contributed to the given dotted field path. The chain is oldest→newest; the last element "won" the merge. An unknown path yields nil.

func (*OriginIndex) Format

func (o *OriginIndex) Format(path string) string

Format renders an explain entry as one line per origin.

func (*OriginIndex) Paths

func (o *OriginIndex) Paths() []string

Paths returns every recorded path in deterministic order, useful for CLI listings and tests.

type OverlayAxis

type OverlayAxis struct {
	Dir      string // directory name relative to config root, e.g. "hosts"
	EnvVar   string // environment variable that selects the active subdirectory
	Priority int    // base priority for layers in this axis (suggested: 3000+)
	// DefaultFromHostname controls whether os.Hostname() is used as the axis
	// value when EnvVar is absent from the environment. This is useful for
	// host-specific overlays that should activate automatically based on the
	// machine name without requiring an explicit environment variable.
	//
	// When EnvVar is non-empty: DefaultFromHostname only activates if the
	// variable is absent (not set at all). Setting the variable to an empty
	// string explicitly disables the axis, giving operators a way to opt out.
	//
	// When EnvVar is empty (""): DefaultFromHostname always activates —
	// the axis unconditionally uses os.Hostname() with no env var override.
	DefaultFromHostname bool
}

OverlayAxis describes a single overlay axis: a directory under the config root that contains named subdirectories, where the active subdirectory is determined by an environment variable.

Example:

OverlayAxis{Dir: "hosts", EnvVar: "HOST", Priority: 3200, DefaultFromHostname: true}

With HOST=ua and config root "config/", FastConf loads all files under "config/hosts/ua/" as additional file layers with priority 3200. Files in this axis override base layers (priority 1000-1999) and standard overlays (2000-2999), but are themselves overridden by providers (8000+).

Axis value resolution order:

  1. If EnvVar is non-empty and the environment variable is set to a non-empty value, that value is used.
  2. If EnvVar is non-empty and the environment variable is explicitly empty, the axis is skipped (operator opt-out).
  3. If the environment variable is absent (not set at all) and DefaultFromHostname is true, os.Hostname() is used as the axis value.
  4. If EnvVar is empty ("") and DefaultFromHostname is true, os.Hostname() is used unconditionally (no env var override is possible — hostname-only axis).
  5. Otherwise the axis is skipped.

type PlanBuilder

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

PlanBuilder is the dry-run builder returned by Manager.Plan(). Use the With* chain to tune the preview, then call Run(ctx) to execute.

func (*PlanBuilder[T]) Run

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

Run executes the configured dry-run preview without mutating Manager state.

func (*PlanBuilder[T]) WithHostname

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

WithHostname pins the hostname value used to resolve multi-axis overlay axes that rely on DefaultFromHostname. Use it from fastconfctl plan / PR-bots running on CI runners so the produced diff reflects the target environment instead of "ci-runner-7".

type PlanResult

type PlanResult[T any] struct {
	Proposed   *State[T]
	Diff       []string
	Validators []ValidatorReport
	// Policies holds all policy findings (warnings and errors alike) gathered
	// during the dry-run. Findings with SeverityError would have aborted a real
	// reload; here they are captured for inspection instead.
	Policies []policy.Violation
}

PlanResult describes the outcome of Manager.Plan.

type PolicyError

type PolicyError struct {
	Violations []policy.Violation
}

PolicyError aggregates the violations that aborted a reload. It satisfies errors.Is(ErrPolicyDenied) so callers don't need to know the concrete type to special-case policy failures.

func (*PolicyError) Error

func (e *PolicyError) Error() string

func (*PolicyError) Is

func (e *PolicyError) Is(target error) bool

type ProvenanceLevel

type ProvenanceLevel uint8

ProvenanceLevel controls how aggressively the merger records field origins.

ProvenanceOff       — default; no recording, zero overhead.
ProvenanceTopLevel  — only track top-level keys (cheap).
ProvenanceFull      — track every leaf path (linear in tree size).
const (
	// ProvenanceOff disables origin tracking entirely (default).
	ProvenanceOff ProvenanceLevel = iota
	// ProvenanceTopLevel records only top-level (depth=1) keys.
	ProvenanceTopLevel
	// ProvenanceFull records every leaf path — recommended for CLI
	// "explain" use, but adds O(N) work per reload.
	ProvenanceFull
)

type ProviderFactory

type ProviderFactory func(cfg map[string]any) (contracts.Provider, error)

ProviderFactory builds a Provider from a free-form config map. The map shape is provider-specific; a vault factory might look for "addr" and "path", an HTTP factory for "url" etc. Factories MUST validate the config and return an error rather than panic.

func LookupProviderFactory

func LookupProviderFactory(name string) (ProviderFactory, bool)

LookupProviderFactory consults only the process-wide default registry. Exposed for diagnostic tooling such as `fastconfctl`.

type ProviderMetricsSink

type ProviderMetricsSink interface {
	ProviderError(provider string)
	EventDropped(provider string)
}

ProviderMetricsSink is an optional extension implemented by sinks that also want to observe provider-watch lifecycle counters (provider errors and dropped events). The framework checks for this interface at runtime via a type assertion, so existing MetricsSink implementations remain compatible.

type ProviderRegistry

type ProviderRegistry struct {
	// contains filtered or unexported fields
}

ProviderRegistry is an explicit, instance-scoped map of named ProviderFactory entries. Use NewProviderRegistry to construct one and WithProviderRegistry(r) to attach it to a Manager.

The zero value is NOT usable; always call NewProviderRegistry. Methods are safe for concurrent use.

func NewProviderRegistry

func NewProviderRegistry() *ProviderRegistry

NewProviderRegistry returns an empty registry. Pair with WithProviderRegistry to scope provider lookups to a single Manager (or TenantManager tenant) instead of the process-wide global.

func (*ProviderRegistry) Lookup

func (r *ProviderRegistry) Lookup(name string) (ProviderFactory, bool)

Lookup returns a registered factory and whether it existed.

func (*ProviderRegistry) Names

func (r *ProviderRegistry) Names() []string

Names returns the sorted list of registered factory names. Useful for diagnostic output (e.g. "have: [vault consul http]").

func (*ProviderRegistry) Register

func (r *ProviderRegistry) Register(name string, f ProviderFactory)

Register adds a named factory. Re-registering an existing name overwrites the previous factory; tests rely on this.

type ReloadCause

type ReloadCause struct {
	// Reason mirrors the reloadRequest reason ("initial",
	// "provider:vault://...", "manual", "watcher", ...). Stable string
	// safe for log labels and metric dimensions.
	Reason string
	// At is the wall-clock instant the reload pipeline started.
	At int64
	// Revisions captures every provider's reported revision at the time
	// of assemble (provider name -> revision string). Empty for plain
	// file-only configurations.
	Revisions map[string]string
	// Tenant, when non-empty, identifies which logical tenant this
	// commit belongs to. For single-tenant deployments this is always "".
	Tenant string
	// Key, when non-empty, identifies the watched parent directory whose
	// fsnotify event burst triggered this reload. Populated only for
	// file-system driven reloads (the coalescer keys bursts by parent
	// dir); empty for manual, provider-driven, and initial reloads.
	Key string
}

ReloadCause is the audit-friendly explanation of a successful commit. It is emitted to every AuditSink and surfaced on State[T].Cause so downstream tooling can trace an in-process change back to the event (file change, provider push, Reload) that drove it.

type ReloadError

type ReloadError struct {
	// Err is the wrapped reload error (errors.Is(err, ErrFastConf) → true).
	Err error
	// Reason mirrors the reloadRequest reason ("manual" / "watcher" /
	// "provider:vault" / "override" / ...). Safe for log labels and
	// metric dimensions.
	Reason string
	// When is the wall-clock instant the reload attempt completed.
	When time.Time
}

ReloadError is one entry on the Manager.Errors() channel. Failure-safe is unchanged: on every reload failure the previous *State[T] remains active; this struct is purely a notification carrier.

type ReloadOption

type ReloadOption func(*reloadConfig)

ReloadOption tunes a single Reload invocation.

func WithReloadReason

func WithReloadReason(reason string) ReloadOption

WithReloadReason overrides the default "manual" reason tag stamped on the audit / metric / log lines this reload emits.

func WithSourceOverride

func WithSourceOverride(override map[string]any) ReloadOption

WithSourceOverride attaches a one-shot in-memory layer to this reload, merged above CLI flags. The override map is CONSUMED by the manager; callers MUST NOT mutate map keys, sub-maps, or slice contents after the call. The layer is not remembered: a subsequent Reload reverts to the natural state.

Use cases: targeted integration tests, ad-hoc operator overrides in fastconfctl, "rehearse a change without writing a file". Never use this from production hot paths.

type RenderMetricsSink

type RenderMetricsSink interface {
	RenderError(name string)
}

RenderMetricsSink (SMELL-1210) is the optional extension implemented by sinks that want to observe integrations/render failures. The framework calls RenderError once per failed render attempt; sinks that don't implement this surface are transparently ignored.

type Replay

type Replay[T any] Manager[T]

Replay is the time-travel sub-API. Created via Manager.Replay().

func (*Replay[T]) List

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

List returns up to cap previously committed snapshots, oldest first. Returns an empty slice if WithHistory(n) was not configured.

func (*Replay[T]) Rollback

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

Rollback atomically swaps the active state to the supplied snapshot, provided it is still retained in the history ring. The swap is serialized through the single-writer reloadCh so it cannot race with an in-flight reload pipeline.

Returns ErrHistoryDisabled when WithHistory(n) was not configured, and ErrUnknownGeneration when target is not (or no longer) in the ring.

Example

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

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)
Output:
:9090 :8080

type SecretRedactor

type SecretRedactor func(path string, value any) any

SecretRedactor turns a sensitive value into its display form. It receives the dotted path and the raw decoded value, and returns whatever should be surfaced in dumps, logs and CLI output.

type SecretRef

type SecretRef struct {
	Scheme string
	Body   string
}

SecretRef identifies one opaque secret reference recognised by a SecretResolver. Scheme is the lookup namespace ("sops", "age", "vault", "kms", "fastconf-enc", ...); Body is the scheme-specific payload (cipher text, kms arn, file pointer).

type SecretResolver

type SecretResolver interface {
	Recognize(v string) (SecretRef, bool)
	Resolve(ctx context.Context, ref SecretRef) (string, error)
}

SecretResolver decrypts opaque secret references that appear in the merged map. Implementations may call SOPS, Vault transit, AWS KMS, age, or a local keyring.

Recognize is called on every leaf string in the merged map; returning (SecretRef{}, false) leaves the value untouched. Recognize MUST be pure and side-effect free — the framework may call it many times per reload.

Resolve is called once per recognised reference per reload, on the single reload goroutine, with the original ctx. Returning a non-nil error aborts the reload (failure-safe).

type SecretResolverFunc

type SecretResolverFunc struct {
	RecognizeFn func(string) (SecretRef, bool)
	ResolveFn   func(context.Context, SecretRef) (string, error)
}

SecretResolverFunc adapts a pair of functions into a SecretResolver.

func (SecretResolverFunc) Recognize

func (f SecretResolverFunc) Recognize(v string) (SecretRef, bool)

Recognize implements SecretResolver.

func (SecretResolverFunc) Resolve

func (f SecretResolverFunc) Resolve(ctx context.Context, ref SecretRef) (string, error)

Resolve implements SecretResolver.

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 struct {
	// Path is the stable identifier for the config source: an absolute file path
	// for file layers, or a pseudo-URI like "env://APP_*" for env/cli providers.
	Path string
	// Kind identifies the merge semantics. See LayerKind constants.
	Kind LayerKind
	// Profile is the active overlay name; empty string for base layers.
	Profile string
	// Priority determines merge order: higher values are merged later (higher precedence).
	Priority int
	// Codec is the decoder name: "yaml" | "json" | "" (provider).
	Codec string
	// Revision is the opaque per-provider version string (etcd revision,
	// Vault current_version, Consul ModifyIndex). Empty for file/legacy providers.
	Revision string
	// Stale flags a degraded provider snapshot (best-effort cache).
	Stale bool
}

SourceRef describes the metadata for a single config layer that participated in a merge. Available via State.Sources for diagnostics and tooling.

type Span

type Span = contracts.Span

Span is a type alias for contracts.Span (v0.10.0+). Existing callers that reference fastconf.Span continue to compile without any changes.

type StageMetricsSink

type StageMetricsSink interface {
	StageDuration(stage string, dur time.Duration, ok bool)
}

StageMetricsSink is an optional extension for sinks that want per-stage histograms (assemble, merge, migration, transform, decode, validate, commit). Sinks that don't implement it are transparently ignored.

type State

type State[T any] struct {
	// Value is the strongly-typed configuration struct. Get() returns this pointer directly.
	Value *T
	// Hash is the global SHA-256 fingerprint of *T (based on canonical JSON).
	Hash [32]byte
	// LoadedAt is the Unix nanosecond timestamp when this state was generated.
	LoadedAt int64
	// Sources holds metadata for every layer that participated in this merge.
	Sources []SourceRef
	// Generation is the monotonically increasing version number; incremented on each successful reload.
	Generation uint64

	// Cause records why this state was committed: which event triggered
	// the reload, which provider revisions were observed, and an optional
	// caller-supplied request id.
	Cause ReloadCause
	// contains filtered or unexported fields
}

State is an immutable snapshot of the configuration at a point in time. Manager replaces it atomically via atomic.Pointer[State[T]] to provide lock-free reads.

Callers must treat the *State[T] pointer as read-only.

func (*State[T]) Diff

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

Diff returns the dotted-path differences between two snapshots (typically produced by canonical JSON encoding the *T values). The output is stable and human-readable, suitable for tests and CLI display. Either operand may be nil; nil is treated as an empty configuration so the diff reports every path on the other side.

func (*State[T]) Explain

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

Explain is a shortcut for s.Origins().Explain(path); returns nil when provenance is off, the path is unknown, or the receiver is nil.

func (*State[T]) FeatureRules

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

FeatureRules returns the feature rule table this State carries. Empty when WithFeatureRules was not configured. Pair with feature.Eval when you need the untyped runtime value (e.g. OpenFeature integrations); for compile-time typed evaluation prefer fastconf.Eval[T,V].

func (*State[T]) Introspect

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

Introspect returns the dotted-key / map[string]any introspection sub-API. The strongly-typed hot path is state.Value; Introspect is reserved for diagnostics, CLI dump, diff tooling, and other places where dynamic keys are unavoidable.

The first call materialises the flat view (one json.Marshal + tree walk); subsequent calls reuse a cached snapshot.

func (*State[T]) Lookup

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

Lookup returns every per-layer value recorded for the given dotted path, oldest first. The last entry is the winner (the value the caller would actually observe via Get). Each entry carries its SourceRef and the raw layer value (only populated when ProvenanceFull was enabled). Returns nil when provenance is off, the path was never written, or the receiver is nil.

func (*State[T]) LookupStrict

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

LookupStrict behaves like Lookup but distinguishes "no provenance" from "path unknown" via an error.

func (*State[T]) MarshalYAML

func (s *State[T]) MarshalYAML(redactor SecretRedactor) ([]byte, error)

MarshalYAML returns a deterministic YAML encoding of the State's merged settings. Map keys are emitted in lexicographic order so operator-driven diff tooling produces stable output across reloads.

When redactor is non-nil, every `fastconf:"secret"` field is replaced in the output via redactor(path, value). Pass DefaultSecretRedactor for the standard "***REDACTED***" mask, or a custom redactor for alternative display logic. When redactor is nil the raw values are emitted (callers must redact upstream if sensitivity matters).

func (*State[T]) Origins

func (s *State[T]) Origins() *OriginIndex

Origins returns the per-field origin index; nil when provenance is disabled or when called on a nil receiver.

func (*State[T]) Redact

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

Redact returns a deep copy of v with every secret path replaced according to the redactor (DefaultSecretRedactor when nil).

func (*State[T]) Redacted

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

Redacted returns a map[string]any view of the configuration with every "secret"-tagged field replaced by the configured SecretRedactor (or DefaultSecretRedactor when WithSecretRedactor was not used).

Equivalent to s.Redact(<configured redactor>); use Redact directly when you need to apply a different redactor at call time.

Safe to call on a nil receiver; returns nil in that case.

type TenantManager

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

TenantManager[T] is the multi-tenancy facade. It owns a registry of fully independent Manager[T] instances keyed by tenant id, so different tenants may have different providers, profiles, validators, or feature flags while sharing one process and one reader-side API.

Design choices:

  • Each tenant gets its own goroutine-safe Manager[T]; there is no cross-tenant coupling, which keeps the failure-isolation guarantee per tenant. A bad provider in tenant A cannot stall reloads in tenant B.
  • Get(tenant) is a single map lookup behind a RWMutex; the read side is intentionally O(1) on the steady state.
  • Add returns the underlying Manager so callers can subscribe, plan, or close it directly. Remove is idempotent.

TenantManager does NOT proxy options across tenants — the caller supplies the full options slice for each Add call. This keeps the public API tiny and avoids "spooky action at a distance" where a shared option would surprisingly affect every tenant.

func NewTenantManager

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

NewTenantManager constructs an empty registry. Tenants are added via Add. The zero value is NOT usable — always go through this constructor so future fields can be initialised.

func (*TenantManager[T]) Add

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

Add constructs and registers a Manager[T] for tenant id. The supplied options are passed to New verbatim. The framework automatically wraps the user's AuditSink so every emitted ReloadCause carries Tenant=id, eliminating boilerplate at the call site.

func (*TenantManager[T]) Close

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

Close closes every registered Manager and marks the registry as closed. Subsequent Add calls will fail. Close aggregates errors using errors.Join.

func (*TenantManager[T]) Get

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

Get returns the manager for id; ErrUnknownTenant if absent.

func (*TenantManager[T]) Has

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

Has returns true when id has been Added and not yet Removed.

func (*TenantManager[T]) Remove

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

Remove closes the underlying Manager and de-registers id. It is safe to call Remove for an unknown tenant (returns ErrUnknownTenant without side effects).

func (*TenantManager[T]) Tenants

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

Tenants returns a snapshot of the currently registered ids in unspecified order. The returned slice is safe to mutate.

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 interface {
	Start(ctx context.Context, name string) (context.Context, Span)
}

Tracer is a minimal, dependency-free tracing surface that fastconf calls to mark the boundaries of each reload-pipeline stage. It is inspired by go.opentelemetry.io/otel but expressed without importing it so that the core module stays zero-dependency. Concrete adapters (OTel, Jaeger client, custom logger-as-trace) live in submodules.

The framework guarantees:

  • Start is always paired with Span.End (defer-ed).
  • SetAttribute is called with primitive values: string, int64, bool.
  • Errors flow into Span.RecordError; failed reloads also call End.
  • All calls happen on the single reload goroutine; implementations do NOT need to be safe for concurrent calls on the same Span.

A nil Tracer (or Span returning nil) is always tolerated; the framework checks before dispatch.

type Transformer

type Transformer = transform.Transformer

Transformer mutates the merged configuration tree before it is decoded into the user's strongly typed snapshot.

This is a type alias for transform.Transformer: any value satisfying pkg/transform.Transformer satisfies fastconf.Transformer too — they are the same Go type. The alias keeps fastconf's option surface ergonomic while letting the built-in transformer set (Defaults / SetIfAbsent / EnvSubst / DeletePaths / Aliases) live in its own package.

type ValidatorReport

type ValidatorReport struct {
	Name string
	Err  error
}

ValidatorReport is one row in PlanResult.Validators.

type Watcher

type Watcher[T any] Manager[T]

Watcher is the watch-loop control sub-API. Created via Manager.Watcher().

func (*Watcher[T]) Pause

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

Pause stops the manager from honouring file/provider events until Resume is called. Manual Reload() still works. Pausing is best-effort: events that arrived before the pause may still be processed.

func (*Watcher[T]) Paused

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

Paused reports the current pause state.

func (*Watcher[T]) Resume

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

Resume re-enables file/provider event processing after Pause.

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 Phase 26 sidecar daemon.
fastconfd is the Phase 26 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.
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.
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.
obs
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
decoder
Package decoder 把不同编码(yaml/json/...)的字节流解码为统一的 map[string]any 中间表示。
Package decoder 把不同编码(yaml/json/...)的字节流解码为统一的 map[string]any 中间表示。
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 实现 Kustomize 风格的 map[string]any 深度合并。
Package merger 实现 Kustomize 风格的 map[string]any 深度合并。
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 (Phase 13).
Package profile implements FastConf's tiny boolean profile-expression language (Phase 13).
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.
validate
Package validate hosts reusable validation primitives for FastConf.
Package validate hosts reusable validation primitives for FastConf.
Package policy defines the Phase 23 policy interface.
Package policy defines the Phase 23 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.
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