Documentation
¶
Overview ¶
Package fastconf 提供基于 Go 1.26 泛型的强类型、无锁化、Kustomize 风格的 动态配置加载叠加框架。
核心思想 ¶
- Manager[T] 把业务配置结构体 T 作为类型参数,所有读路径返回 *T,无 reflect、零分配;
- State[T] 通过 atomic.Pointer 维护,单写多读无锁;
- 加载流程为 Discover → Decode → Merge → (Patch) → Validate → AtomicSwap, 任意一步失败均不会污染当前状态。
File-by-file Phase pointer table ¶
Each entry below records the originating Phase for a file's overall shape. Detailed release history lives in docs/plans/status.md and docs/plans/phase-plan.md.
manager.go — Phase 32 SPEC-32: manager core + Read API reload_loop.go — Phase 32 SPEC-32: single-writer reload provider_watch.go — Phase 5 + Phase 25: subscribe/Resumable + backoff pipeline.go — Phase 32 SPEC-32: assemble/commit glue pipeline_stages.go — Phase 31 SPEC-31: Stage list + dryRun pipeline_plan.go — Phase 22: Manager.Plan API pipeline_helpers.go — Phase 40 SPEC-40: hash + layers + codec registry state.go / state_*.go — Phase 7 + Phase 11: snapshot, provenance, history reflection (secret/ — Phase 8, 12a, 53: secret tag scan, default tag, defaults/watch) shared typeinfo cache tenant.go — Phase 24: TenantManager presets.go — Phase 33 SPEC-33: Preset K8s/Sidecar/Testing registry.go — Phase 38 SPEC-38: Provider Factory Registry options.go / opt_*.go — Phase 41 SPEC-41 + Phase 51 SPEC-51 consolidation observability.go / — Phase 21 + Phase 28 + Phase 52 SPEC-52 obs_otel_enrich*.go errors.go — Phase 34 SPEC-34: ErrFastConf parent chain
详细计划参见 docs/plans/phase-plan.md 与 docs/plans/status.md。
Package fastconf — observability surface.
This file groups three sink-like extension points the reload pipeline fans out to:
── AUDIT SECTION ── AuditSink, JSONAuditSink, WithAuditSink ── METRICS SECTION ── MetricsSink + ProviderMetricsSink + StageMetricsSink, metricsBridge ── TRACER SECTION ── Tracer, Span, noopTracer, WithTracer
Per-stage OTel attribute enrichment is gated by -tags fastconf_otel and lives in the obs_otel_enrich{,_default}.go pair (single tiny function `enrichSpan`).
Package fastconf — Options consolidated. Every fastconf.WithXxx builder lives in this file (paired with its options-struct field) so the public Option surface is discoverable in a single godoc page. Tracer- and AuditSink-related options (WithTracer / WithAuditSink) live with their interface definitions in observability.go.
Plan / dry-run API.
Plan runs the same staged pipeline as reload (assemble → merge → migration → transform → decode → defaults → validate → policy) but does NOT swap the atomic state pointer, push history, fire watches, or invoke audit sinks. The result is a fully populated *State[T] you can diff against the live snapshot, plus the validator outcomes — exactly what a `fastconfctl plan` or PR-bot needs to preview a change before merge.
Plan now reuses pipelineCtx + runStages from pipeline.go, so commit() and Plan() can never drift.
Package fastconf — secret tagging and redaction.
Mark struct fields as sensitive with `fastconf:"secret"` (or include the `secret` token in a comma-separated tag list). FastConf will collect their dotted JSON paths during type registration and use the configured SecretRedactor to mask their values when callers ask for a redacted view.
The default redactor replaces the value with a fixed sentinel string.
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/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 (ExternalProvider) ¶
Example_externalProvider demonstrates plugging a third-party provider into fastconf.
package main
import (
"context"
"fmt"
"testing/fstest"
"github.com/fastabc/fastconf/fastconf"
"github.com/fastabc/fastconf/fastconf/contracts"
)
type externalProviderExampleConfig 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"`
}
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_externalProvider demonstrates plugging a third-party provider into fastconf.
func main() {
provider := &staticExampleProvider{
name: "demo-static",
priority: contracts.PriorityKV,
data: map[string]any{
"server": map[string]any{"addr": ":9090"},
"feature": map[string]any{"betaEnabled": true},
},
}
mgr, err := fastconf.New[externalProviderExampleConfig](context.Background(),
fastconf.WithFS(fstest.MapFS{
"conf.d/base/.keep": &fstest.MapFile{Data: []byte("")},
}),
fastconf.WithBytes("seed", "yaml", []byte("server:\n addr: \":8080\"\n")),
fastconf.WithProvider(provider),
)
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/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, err := mgr.History()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%s %d\n", mgr.Get().HTTP.Addr, len(history))
}
Output: :8651 1
Index ¶
- Constants
- Variables
- func DefaultSecretRedactor(_ string, _ any) any
- 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 WatchTyped[T any, M any](m *Manager[T], extract func(*T) *M, fn func(old, neu *M)) (cancel func())
- type AuditSink
- type AuditSinkFunc
- type JSONAuditSink
- type K8sOpts
- type LayerKind
- type Manager
- func (m *Manager[T]) Close() error
- func (m *Manager[T]) Get() *T
- func (m *Manager[T]) History() ([]*State[T], error)
- func (m *Manager[T]) IsWatchPaused() bool
- func (m *Manager[T]) PauseWatch()
- func (m *Manager[T]) Plan(ctx context.Context) (*PlanResult[T], error)
- func (m *Manager[T]) Redactor() SecretRedactor
- func (m *Manager[T]) Reload(ctx context.Context) error
- func (m *Manager[T]) ResumeWatch()
- func (m *Manager[T]) Rollback(gen uint64) error
- func (m *Manager[T]) Snapshot() *State[T]
- func (m *Manager[T]) Watch(module string, fn func(old, new any)) (cancel func())
- type MetricsSink
- type Migrationdeprecated
- type MigrationApplier
- type MigrationFunc
- type Option
- func PresetK8s(p K8sOpts) Option
- func PresetSidecar(p SidecarOpts) Option
- func PresetTesting(p TestingOpts) Option
- func WithAuditSink(sink AuditSink) Option
- func WithBytes(name, codec string, data []byte) Option
- func WithCLIProvider(data map[string]any) Option
- func WithCodecBridge(b codecBridge) Option
- func WithDebounceInterval(d time.Duration) Option
- func WithDefaultProfile(p string) Option
- func WithDir(dir string) Option
- func WithEnvProvider(prefix string) Option
- func WithFS(f fs.FS) Option
- func WithHistory(n int) Option
- func WithLogger(l *slog.Logger) Option
- func WithMetrics(m MetricsSink) Option
- func WithMigrations(run func(map[string]any) error) 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 WithSecretRedactor(r SecretRedactor) Option
- func WithSlog(h slog.Handler) Option
- func WithSource(s contracts.Source) Option
- func WithStrict(strict bool) Option
- func WithStructDefaults[T any]() Option
- func WithTracer(t Tracer) Option
- func WithTransformers(t ...Transformer) Option
- func WithValidator[T any](v func(*T) error) Option
- func WithWatch(enabled bool) Option
- func WithWatchPaths(paths ...string) Option
- type Origin
- type OriginIndex
- type PlanResult
- type PolicyError
- type ProvenanceLevel
- type ProviderFactory
- type ProviderMetricsSink
- type ReloadCause
- type SecretRedactor
- type SidecarOpts
- type SourceRef
- type Span
- type Stage
- 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]) Lookup(path string) []Origin
- func (s *State[T]) LookupStrict(path string) ([]Origin, error)
- func (s *State[T]) Origins() *OriginIndex
- func (s *State[T]) Redact(redactor SecretRedactor) 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
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 ( BridgeJSON = bridgeJSON BridgeYAML = bridgeYAML )
BridgeJSON and BridgeYAML are the exported aliases for use with WithCodecBridge.
const DefaultDebounceInterval = 500 * time.Millisecond
DefaultDebounceInterval is the file-watcher debounce window. Events arriving within this window are coalesced into a single reload.
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) { ... }
SPEC-34 (v0.5): 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 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 DefaultSecretRedactor ¶
DefaultSecretRedactor replaces the value with "***REDACTED***".
func LookupCodec ¶
LookupCodec returns the codec registered under name (case-insensitive). Mainly useful for tests and diagnostics; production code should not need to introspect the registry.
func RegisterCodec ¶
RegisterCodec installs a third-party Codec under the given name. After registration, files with a matching extension (see RegisterCodecExt) and in-memory layers (WithBytes / WithSource) referencing the same codec name will be decoded by c.
The registration is process-global and survives multiple Manager instances. Re-registering an existing name overwrites the previous codec; this makes test helpers ergonomic and lets advanced users monkey-patch built-in codecs (e.g. swap the YAML implementation).
Typical use from a third-party package:
func init() {
fastconf.RegisterCodec("toml", tomlCodec{})
fastconf.RegisterCodecExt("toml", "toml")
}
Passing nil panics — registering nil would silently break the next decode and is always a programming error.
func RegisterCodecExt ¶
func RegisterCodecExt(ext, codec string)
RegisterCodecExt maps a file extension (with or without leading dot, case-insensitive) to a previously-registered codec name. It is the counterpart of RegisterCodec for the file-discovery path: discovery looks at the extension to pick a codec name, then RegisterCodec supplies the implementation.
func RegisterProviderFactory ¶
func RegisterProviderFactory(name string, f ProviderFactory)
RegisterProviderFactory adds a named factory. It is safe to call from init() across packages. Re-registering an existing name overwrites the previous factory; this is intentional so that tests can substitute fakes.
func RegisteredProviderNames ¶
func RegisteredProviderNames() []string
RegisteredProviderNames returns the sorted list of registered factory names. Useful for help output.
func WatchTyped ¶
func WatchTyped[T any, M any](m *Manager[T], extract func(*T) *M, fn func(old, neu *M)) (cancel func())
WatchTyped is the type-safe wrapper around Watch. Use it when the module path corresponds to a known struct field on *T.
cancel := fastconf.WatchTyped(cfg,
func(c *AppConfig) *DBConfig { return &c.Database },
func(old, neu *DBConfig) { reconnect(neu) },
)
defer cancel()
Internally it relies on the global Hash, not PartHashes, because extract can return any pointer; this fires on every committed state change but the callback can compare old/new itself.
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 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.
fix: BUG-208 — 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)
Debounce time.Duration
}
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 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.WithEnvProvider("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.
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]) History ¶
History returns up to cap previously committed snapshots, oldest first. Returns ErrHistoryDisabled if WithHistory was not configured.
func (*Manager[T]) IsWatchPaused ¶
IsWatchPaused reports the current pause state.
func (*Manager[T]) PauseWatch ¶
func (m *Manager[T]) PauseWatch()
PauseWatch stops the manager from honouring file/provider events until ResumeWatch is called. Manual Reload() still works. Pausing is best-effort: events that arrived before the pause may still be processed.
func (*Manager[T]) Plan ¶
func (m *Manager[T]) Plan(ctx context.Context) (*PlanResult[T], error)
Plan computes a dry-run preview without mutating Manager state.
func (*Manager[T]) Redactor ¶
func (m *Manager[T]) Redactor() SecretRedactor
Redactor returns the redactor configured via WithSecretRedactor, or DefaultSecretRedactor when none was provided.
func (*Manager[T]) Reload ¶
Reload triggers a synchronous reload. On failure the previous state is preserved.
func (*Manager[T]) ResumeWatch ¶
func (m *Manager[T]) ResumeWatch()
func (*Manager[T]) Rollback ¶
Rollback atomically swaps the active state to the snapshot with the supplied Generation, provided it is still retained in the history ring. The new active state keeps its original Generation; subsequent reloads continue advancing the generation counter from the rolled- back value's generation + 1 to keep monotonicity per restart.
func (*Manager[T]) Snapshot ¶
Snapshot returns the full immutable State[T] snapshot used for diagnostics and fingerprint comparisons.
func (*Manager[T]) Watch ¶
Watch registers a callback that fires whenever the named module's per-field hash changes between two committed states.
The callback receives the old and new field values as `any` (the actual type is *T's matching struct field). For type-safe access, prefer WatchTyped[T,M].
The returned cancel removes the subscription. Calling cancel after Close() is a no-op.
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 under the `fastconf_metrics` build tag (see internal/metrics/).
type Migration
deprecated
type Migration = MigrationApplier
Migration is the legacy name for MigrationApplier. It exists to keep v0.7 callers compiling unchanged.
Deprecated: use MigrationApplier. This alias is scheduled for removal in v1.0.
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 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 WithCLIProvider ¶
WithCLIProvider is a shorthand for WithProvider(provider.NewCLI(data)).
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 WithDebounceInterval ¶
WithDebounceInterval sets the watch debounce window. Default: DefaultDebounceInterval.
func WithDefaultProfile ¶
WithDefaultProfile sets the fallback profile when no explicit profile exists.
func WithEnvProvider ¶
WithEnvProvider is a shorthand for WithProvider(provider.NewEnv(prefix)).
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. Default: slog.Default().
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 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.
Returns an Option that records the resolution error inside options; New() surfaces that error before starting any goroutine.
func WithSecretRedactor ¶
func WithSecretRedactor(r SecretRedactor) Option
WithSecretRedactor installs the secret redactor used by dumps and snapshots.
func WithSource ¶
WithSource injects a self-described source value.
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 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.
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 PlanResult ¶
type PlanResult[T any] struct { Proposed *State[T] Diff []string Validators []ValidatorReport }
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 returns a registered factory and whether it existed. 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 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
// RequestID, when non-empty, is propagated from ManualReload
// (or set by an AuditSink) so a human-driven action can be
// correlated end-to-end across logs and metrics.
RequestID string
// Tenant, when non-empty, identifies which logical tenant this
// commit belongs to (Phase 24). For single-tenant deployments
// this is always "".
Tenant 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, manual ManualReload) that drove it.
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 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 配置源的稳定标识:文件层为绝对路径,env/cli 等为 "env://APP_*" 之类的伪 URI。
Path string
// Kind 见 LayerKind 常量定义。
Kind LayerKind
// Profile 当前 overlay 名;base 层为空字符串。
Profile string
// Priority 数字越大越后合并(越高优先级)。
Priority int
// Codec 解码用的编解码器名称:"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. Phase 12b.
Revision string
// Stale flags a degraded provider snapshot (best-effort cache). Phase 12b.
Stale bool
}
SourceRef 描述参与一次合并的某一层配置的元信息。 调用方可以从 Snapshot 中获取以做诊断。
type Span ¶
Span is the per-stage handle returned by Tracer.Start. End MUST be called exactly once per Start. RecordError marks the span as failed; SetAttribute attaches diagnostic metadata.
type Stage ¶
type Stage[T any] interface { Name() string Run(ctx context.Context, m *Manager[T], pc *pipelineCtx[T]) error }
Stage is one step in the reload pipeline.
pipelineCtx remains package-private, so external packages can reference Stage values but cannot implement new stages outside fastconf.
type StageMetricsSink ¶
StageMetricsSink is the Phase 21/28 optional extension. Sinks that want per-stage histograms (assemble, merge, migration, transform, decode, validate, commit) implement this; sinks that don't are transparently ignored.
type State ¶
type State[T any] struct { // Value 强类型业务结构体。Get() 直接返回该指针。 Value *T // Hash 是 *T 的全局 SHA-256 指纹(基于 canonical JSON)。 Hash [32]byte // PartHashes 子模块指纹。Phase 5 启用;Phase 1 暂为空。 PartHashes map[string][32]byte // LoadedAt 状态生成的 unix 纳秒时间戳。 LoadedAt int64 // Sources 本次合并参与的所有 layer 元信息。 Sources []SourceRef // Generation 单调递增版本号。每次成功 reload +1。 Generation uint64 // Cause records why this state was committed (Phase 19): 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 是配置在某一时刻的不可变快照。Manager 通过 atomic.Pointer[State[T]] 串行替换以提供无锁读。
调用方拿到的 *State[T] 指针指向只读对象,禁止任何写入。
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.
func (*State[T]) Explain ¶
Explain is a shortcut for s.Origins().Explain(path); returns nil when provenance is off or the path is unknown.
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 or when the path was never written.
func (*State[T]) LookupStrict ¶
LookupStrict behaves like Lookup but distinguishes "no provenance" from "path unknown" via an error. Phase 20 BUG-206.
func (*State[T]) Origins ¶
func (s *State[T]) Origins() *OriginIndex
Origins returns the per-field origin index; nil when provenance is disabled.
type TenantManager ¶
type TenantManager[T any] struct { // contains filtered or unexported fields }
TenantManager[T] is the Phase 24 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.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package bus provides a small message-bus abstraction for FastConf.
|
Package bus provides a small message-bus abstraction for FastConf. |
|
contracts
module
|
|
|
pkg
|
|
|
debounce
Package debounce provides a single-writer trailing-edge debouncer used by the watcher subsystem to coalesce bursty filesystem events into one reload trigger.
|
Package debounce provides a single-writer trailing-edge debouncer used by the watcher subsystem to coalesce bursty filesystem events into one reload trigger. |
|
decoder
Package decoder 把不同编码(yaml/json/...)的字节流解码为统一的 map[string]any 中间表示。
|
Package decoder 把不同编码(yaml/json/...)的字节流解码为统一的 map[string]any 中间表示。 |
|
discovery
Package discovery 扫描配置目录并产出按优先级排序的 layer 流。
|
Package discovery 扫描配置目录并产出按优先级排序的 layer 流。 |
|
fingerprint
Package fingerprint hosts reusable fingerprinting primitives for FastConf.
|
Package fingerprint hosts reusable fingerprinting primitives for FastConf. |
|
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. |
|
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, ...). |
|
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. |
|
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). |
|
validate
Package validate hosts reusable validation primitives for FastConf.
|
Package validate hosts reusable validation primitives for FastConf. |
|
watcher
Package watcher subscribes to filesystem changes and triggers reloads.
|
Package watcher subscribes to filesystem changes and triggers reloads. |
|
Package policy defines the Phase 23 policy interface.
|
Package policy defines the Phase 23 policy interface. |
|
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, ...). |