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 ¶
- Constants
- Variables
- func Bind(src contracts.Source, p contracts.Parser) contracts.Provider
- func DefaultSecretRedactor(_ string, _ any) any
- func Eval[T any, V any](m *Manager[T], key string, ctx feature.EvalContext, def V) V
- func LookupCodec(name string) (contracts.Codec, bool)
- func RegisterCodec(name string, c contracts.Codec)
- func RegisterCodecExt(ext, codec string)
- func RegisterProviderFactory(name string, f ProviderFactory)
- func RegisteredProviderNames() []string
- func Sub[T any, M any](s *State[T], extract func(*T) *M) *M
- func Subscribe[T any, M any](m *Manager[T], extract func(*T) *M, fn func(old, new *M)) (cancel func())
- type AuditSink
- type AuditSinkFunc
- type CoalesceProfile
- type Defaulter
- type DiffEvent
- type DiffReporter
- type DiffReporterFunc
- type DiffReporterMetricsSink
- type EvalContext
- type FeatureRule
- type FieldSpec
- type HierarchicalOpts
- type Introspection
- type JSONAuditSink
- type K8sOpts
- type LayerKind
- type Manager
- func (m *Manager[T]) Close() error
- func (m *Manager[T]) Errors() <-chan ReloadError
- func (m *Manager[T]) Get() *T
- func (m *Manager[T]) Plan() *PlanBuilder[T]
- func (m *Manager[T]) Reload(ctx context.Context, opts ...ReloadOption) error
- func (m *Manager[T]) Replay() *Replay[T]
- func (m *Manager[T]) Snapshot() *State[T]
- func (m *Manager[T]) Watcher() *Watcher[T]
- type MetricsSink
- type MigrationApplier
- type MigrationFunc
- type Option
- func PresetHierarchical(p HierarchicalOpts) Option
- func PresetK8s(p K8sOpts) Option
- func PresetSidecar(p SidecarOpts) Option
- func PresetTesting(p TestingOpts) Option
- func WithAuditSink(sink AuditSink) Option
- func WithCoalesceMaxLag(d time.Duration) Option
- func WithCoalesceProfile(p coalesce.Profile) Option
- func WithCoalesceQuiet(d time.Duration) Option
- func WithCoalesceSwapHint(d time.Duration) Option
- func WithCodecBridge(b codecBridge) Option
- func WithDefaultProfile(p string) Option
- func WithDefaulterFunc[T any](fn func(*T)) Option
- func WithDiffReporter(r DiffReporter) Option
- func WithDiffReporterQueueCap(n int) Option
- func WithDir(dir string) Option
- func WithDotEnvAuto(prefix string) Option
- func WithFS(f fs.FS) Option
- func WithFeatureRules[T any](extract func(*T) map[string]feature.Rule) Option
- func WithGenerator(g contracts.Generator) Option
- func WithHistory(n int) Option
- func WithLogger(l *slog.Logger) Option
- func WithMergeKeys(keys map[string]string) Option
- func WithMetrics(m MetricsSink) Option
- func WithMigrations(run func(map[string]any) error) Option
- func WithMultiAxisOverlays(axes ...OverlayAxis) Option
- func WithPolicy[T any](p policy.Policy[T]) Option
- func WithProfile(p string) Option
- func WithProfileEnv(name string) Option
- func WithProfileExpr(expr string) Option
- func WithProfiles(p ...string) Option
- func WithProvenance(level ProvenanceLevel) Option
- func WithProvider(p contracts.Provider) Option
- func WithProviderByName(name string, cfg map[string]any) Option
- func WithProviderOrdered(ps ...contracts.Provider) Option
- func WithProviderRegistry(r *ProviderRegistry) Option
- func WithRawMapAccess(fn func(root map[string]any)) Option
- func WithSecretRedactor(r SecretRedactor) Option
- func WithSecretResolver(r SecretResolver) Option
- func WithSource(src contracts.Source, p contracts.Parser) Option
- func WithStrict(strict bool) Option
- func WithStructDefaults[T any]() Option
- func WithTracer(t Tracer) Option
- func WithTransformers(t ...Transformer) Option
- func WithTypedHook(h decoder.TypedHook) Option
- func WithValidator[T any](v func(*T) error) Option
- func WithWatch(enabled bool) Option
- func WithWatchPaths(paths ...string) Option
- func WithoutDefaultTypedHooks() Option
- type Origin
- type OriginIndex
- type OverlayAxis
- type PlanBuilder
- type PlanResult
- type PolicyError
- type ProvenanceLevel
- type ProviderFactory
- type ProviderMetricsSink
- type ProviderRegistry
- type ReloadCause
- type ReloadError
- type ReloadOption
- type RenderMetricsSink
- type Replay
- type SecretRedactor
- type SecretRef
- type SecretResolver
- type SecretResolverFunc
- type SidecarOpts
- type SourceRef
- type Span
- type StageMetricsSink
- type State
- func (s *State[T]) Diff(other *State[T]) []string
- func (s *State[T]) Explain(path string) []Origin
- func (s *State[T]) FeatureRules() map[string]feature.Rule
- func (s *State[T]) Introspect() *Introspection
- func (s *State[T]) Lookup(path string) []Origin
- func (s *State[T]) LookupStrict(path string) ([]Origin, error)
- func (s *State[T]) MarshalYAML(redactor SecretRedactor) ([]byte, error)
- func (s *State[T]) Origins() *OriginIndex
- func (s *State[T]) Redact(redactor SecretRedactor) map[string]any
- func (s *State[T]) Redacted() map[string]any
- type TenantManager
- func (tm *TenantManager[T]) Add(ctx context.Context, id string, opts ...Option) (*Manager[T], error)
- func (tm *TenantManager[T]) Close() error
- func (tm *TenantManager[T]) Get(id string) (*Manager[T], error)
- func (tm *TenantManager[T]) Has(id string) bool
- func (tm *TenantManager[T]) Remove(id string) error
- func (tm *TenantManager[T]) Tenants() []string
- type TestingOpts
- type Tracer
- type Transformer
- type ValidatorReport
- type Watcher
Examples ¶
Constants ¶
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.
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.
const ( BridgeJSON = bridgeJSON BridgeYAML = bridgeYAML )
BridgeJSON and BridgeYAML are the exported aliases for use with WithCodecBridge.
const ( ProfileK8s = coalesce.ProfileK8s ProfileLocalDev = coalesce.ProfileLocalDev )
CoalesceProfile values mirroring the internal/coalesce constants.
const DefaultSidecarHistoryCap = 16
DefaultSidecarHistoryCap is the history ring capacity used by PresetSidecar when SidecarOpts.HistoryN is not set.
Variables ¶
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.
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).
var ErrHistoryDisabled = errors.New("fastconf: history disabled")
ErrHistoryDisabled is returned when history APIs are called but WithHistory was not used.
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).
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).
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.
var ErrUnknownGeneration = errors.New("fastconf: unknown generation")
ErrUnknownGeneration is returned by Rollback when the requested generation is not in the in-memory history ring.
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 ¶
DefaultSecretRedactor replaces the value with "***REDACTED***".
func Eval ¶
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 ¶
LookupCodec returns the codec registered under name (case-insensitive).
func RegisterCodec ¶
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 ¶
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
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 ¶
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 ¶
DiffReporterFunc adapts a function into a 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 ¶
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 )
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 ¶
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 ¶
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 ¶
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 ¶
Snapshot returns the full immutable State[T] snapshot used for diagnostics and fingerprint comparisons.
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 ¶
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 ¶
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 ¶
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 ¶
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
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
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
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
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 ¶
WithDefaultProfile sets the fallback profile when no explicit profile exists.
func WithDefaulterFunc ¶
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 ¶
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 WithDotEnvAuto ¶
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 WithFeatureRules ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
WithProfile sets the active overlay profile explicitly.
func WithProfileEnv ¶
WithProfileEnv sets the environment variable used to resolve the profile.
func WithProfileExpr ¶
WithProfileExpr appends a global match expression to every overlay.
func WithProfiles ¶
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 ¶
WithProvider registers an external provider merged after file layers.
func WithProviderByName ¶
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 ¶
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 ¶
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
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 ¶
WithStrict enables strict file and merge validation.
func WithStructDefaults ¶
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 ¶
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 ¶
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 ¶
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 WithWatchPaths ¶
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:
- If EnvVar is non-empty and the environment variable is set to a non-empty value, that value is used.
- If EnvVar is non-empty and the environment variable is explicitly empty, the axis is skipped (operator opt-out).
- If the environment variable is absent (not set at all) and DefaultFromHostname is true, os.Hostname() is used as the axis value.
- If EnvVar is empty ("") and DefaultFromHostname is true, os.Hostname() is used unconditionally (no env var override is possible — hostname-only axis).
- 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 ¶
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 ¶
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 ¶
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 ¶
Replay is the time-travel sub-API. Created via Manager.Replay().
func (*Replay[T]) List ¶
List returns up to cap previously committed snapshots, oldest first. Returns an empty slice if WithHistory(n) was not configured.
func (*Replay[T]) Rollback ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ValidatorReport is one row in PlanResult.Validators.
type Watcher ¶
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.
Source Files
¶
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. |
|
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. |