fastconf

package module
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: MIT Imports: 37 Imported by: 0

README

FastConf — 强类型 · 无锁 · Kustomize 风格配置框架

fastconf 把 YAML / JSON / TOML、环境变量、命令行参数、远程 KV 与生成器 layer 叠加成一个强类型 Go 结构体,并在热更新时用单写者 reload loop + atomic.Pointer 安全发布新快照。业务读路径就是一次 atomic.Pointer.Load()

Go Reference CI Release

Status: pre-public。当前 API 仍以“把语义收准”为第一目标;本文档与 pkg.go.dev 描述的是当前真相。


目录

  1. 先看哪一段
  2. 一分钟上手
  3. 安装
  4. 核心模型
  5. 核心抽象
  6. 公开 API 地图
  7. Option 参考
  8. Reload 流水线
  9. Profile 与 Overlay
  10. Provider 系统
  11. Transformer 与 Migration
  12. Watch、Subscribe 与 Plan
  13. Provenance、History 与 Rollback
  14. 可观测性
  15. 多租户与 Preset
  16. 为什么没有 GetString("a.b.c")
  17. 性能与可靠性
  18. Sub-module 生态矩阵
  19. 扩展指南
  20. CLI 工具
  21. 本地开发
  22. 文档地图
  23. License

先看哪一段

你要做什么 先看这里
第一次把 FastConf 接进 Go 服务 一分钟上手
在 K8s 里读 ConfigMap 并热更新 docs/cookbook/k8s.md
接 Vault / Consul / 远程 provider docs/cookbook/README.md 的 Providers 区
做 dry-run、解释来源、回滚历史 公开 API 地图 + 对应 cookbook
只想查所有 recipe docs/cookbook/README.md

一分钟上手

package main

import (
    "context"
    "log"

    "github.com/fastabc/fastconf"
)

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

func main() {
    mgr, err := fastconf.New[AppConfig](context.Background(),
        fastconf.WithDir("conf.d"),
        fastconf.WithProfileEnv("APP_PROFILE"),
        fastconf.WithDefaultProfile("dev"),
        fastconf.WithWatch(true),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer mgr.Close()

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

目录约定:

conf.d/
  base/
    00-app.yaml
  overlays/
    prod/
      50-overrides.yaml
      _patch.json

APP_PROFILE=prod 时,FastConf 会按 base/*overlays/prod/* 的顺序合并。 默认 decode bridge 走 JSON round-trip,所以现有结构体若只有 yaml tag,请补上 json tag,或显式选择 fastconf.WithCodecBridge(fastconf.BridgeYAML)

三条推荐入口
场景 推荐组合 继续阅读
本地 / 单服务文件配置 New + WithDir + Get ExampleNew / docs/cookbook/introspect.md
K8s 热更新服务 PresetK8s + Subscribe + Errors docs/cookbook/k8s.md / docs/cookbook/reload-policy.md
远程 source / GitOps WithProvider + Plan + Provenance docs/cookbook/vault.md / docs/cookbook/consul.md / docs/cookbook/plan.md

单元测试优先用 PresetTesting;sidecar 优先用 PresetSidecar;需要 region / zone / host 多轴叠加再看 PresetHierarchicalWithMultiAxisOverlays


安装

作为 Go library(请把 @latest 换为你实际锁定的版本):

go get github.com/fastabc/fastconf@latest

# 可选 sub-module(按需):
go get github.com/fastabc/fastconf/observability/otel@latest
go get github.com/fastabc/fastconf/observability/metrics/prometheus@latest
go get github.com/fastabc/fastconf/policy/cue@latest
go get github.com/fastabc/fastconf/policy/opa@latest
go get github.com/fastabc/fastconf/validate/cue/cuelang@latest
go get github.com/fastabc/fastconf/validate/playground@latest

安装 CLI 工具(Go ≥ 1.26):

go install github.com/fastabc/fastconf/cmd/fastconfd@latest
go install github.com/fastabc/fastconf/cmd/fastconfctl@latest
go install github.com/fastabc/fastconf/cmd/fastconfgen@latest

作为预编译二进制:每个 GitHub Release 都附 OS+arch 矩阵 (linux/{amd64,arm64}darwin/{amd64,arm64}windows/amd64) × 3 个 binary,外加 SHA256SUMS


核心模型

sources / generators / providers
              │
              ▼
       assemble preflight
              │
              ▼
 merge → migration → transform → secret → typed-hooks
      → decode → field-meta → validate → policy
              │
      fail ───┴─── keep old State[T]
              │
           success
              ▼
 canonical hash → atomic swap → history → audit → subscribers
设计 含义
强类型读路径 mgr.Get().Server.Addr 由编译器检查,不靠字符串路径
单写者 reload fsnotify、provider 事件、手动 Reload 都串行进入同一条写路径
失败安全 任一 stage 失败都保留旧 State[T],不会把坏配置发布给业务
Kustomize 风格叠加 支持 base / overlay、RFC 6902 patch、mergeKeys 策略合并
可选扩展 provider、transformer、secret resolver、policy、metrics、tracer 都是 opt-in
源码布局
.    (repo root, package fastconf)
  manager.go              Manager[T] 核心:New / Get / Close / Reload / Snapshot
  pipeline.go             runStages[T] + Plan dry-run 入口
  pipeline_stages.go      Stage[T] 实现(Merge / Assemble / Migrate / Transform / Decode / Validate)
  options.go              所有 WithXxx Option + 公开类型
  state.go                State[T] + ReloadCause + Origins/Explain/Lookup + History
  introspect.go           State.Introspect (Keys / Settings / At)
  watch.go / watcher.go   Subscribe、fsnotify、symlink 处理
  provider_watch.go       provider 事件订阅(指数退避 + drop-on-full)
  presets.go              PresetK8s / PresetSidecar / PresetTesting / PresetHierarchical
  registry.go             RegisterProviderFactory / WithProviderByName
  defaults.go             fastconf:"default=…" struct tag + 内置 hook
  secret.go               fastconf:"secret" + SecretRedactor
  feature.go              FeatureRule / Eval / Sub
  field_meta.go           range / enum / required field-meta check
  errors.go               ErrFastConf sentinel + ReloadError
  obs_audit.go / obs_metrics.go / obs_tracer.go   sinks
  tenant.go               TenantManager[T]
  doc.go                  package-level godoc

pkg/                  ← 公开可复用实现原语(可被外部 Provider / Codec 作者 import)
  decoder/            YAML/JSON codec 注册表
  discovery/          conf.d 目录扫描 + _meta.yaml 解析
  feature/            feature flag rule + EvalContext
  flog/               zerolog 风格 fluent wrapper over *slog.Logger
  generator/          contracts.Generator helpers
  mappath/            dotted-path Get/Set/Delete 工具
  merger/             Kustomize 风格 map[string]any 叠加
  migration/          Chain + Step(From/To/Apply)
  profile/            profile 表达式编译器(&/|/!/())
  provider/           内置 Env / CLI / Bytes / File / Labels Provider
  transform/          Defaults / SetIfAbsent / EnvSubst / DeletePaths / Aliases
  validate/           Validator + ValidatorReport

internal/             ← 私有 helper(Go 编译时 API boundary)
  debounce/  obs/  typeinfo/  watcher/

contracts/            ← 稳定接口:Provider / Codec / Source / Event / Snapshot / Priority

providers/            ← 内置 Provider(vault / consul / http;nats / redisstream 独立 sub-module)
integrations/         ← bus / render / log / openfeature 适配
observability/        ← metrics/prometheus、otel(各自独立 sub-module)
policy/               ← Policy 接口;cue、opa 后端为独立 sub-module
validate/             ← cue/cuelang、playground 校验后端(独立 sub-module)
cmd/                  ← fastconfd(主模块)、fastconfctl、fastconfgen
依赖方向(CI 强制)
fastconf  →  pkg/{discovery,decoder,flog,merger,provider,validate}
          →  internal/watcher
          →  contracts

pkg/* 之间不得相互依赖,白名单例外(与 tools/check-deps.sh 同步):
  pkg/discovery → pkg/profile
  pkg/generator → pkg/mappath
  pkg/provider  → pkg/decoder
  pkg/provider  → pkg/mappath
  pkg/transform → pkg/mappath
internal/* 之间不得相互依赖;只允许标准库。

核心抽象

Manager[T] — 配置管理器
type Manager[T any] struct { /* unexported */ }

// 构造(首次 reload 同步执行)
func New[T any](ctx context.Context, opts ...Option) (*Manager[T], error)

// 读路径(lock-free, O(1), zero-alloc)
func (m *Manager[T]) Get() *T

// 写路径(触发 pipeline;等待结果)。ctx 既控制入队/等待,也贯穿 pipeline 自身 ——
// 取消会终止 provider.Load / secret resolver / transformer,并以 ctx.Err() 返回。
func (m *Manager[T]) Reload(ctx context.Context, opts ...ReloadOption) error

// Dry-run(不更新指针;收集全部 ValidatorReport)
func (m *Manager[T]) Plan() *PlanBuilder[T] // .WithHostname(...).Run(ctx) → *PlanResult[T]

// 当前快照(State[T] + Sources + Origins)
func (m *Manager[T]) Snapshot() *State[T]

// 失败事件流(缓冲 16;drop-on-full;Close() 时关闭)
func (m *Manager[T]) Errors() <-chan ReloadError

// 子系统访问器(零成本命名空间)
func (m *Manager[T]) Watcher() *Watcher[T]  // .Pause() / .Resume() / .Paused()
func (m *Manager[T]) Replay()  *Replay[T]   // .List() / .Rollback(*State[T])

// 生命周期
func (m *Manager[T]) Close() error

包级泛型函数(“从 *TM” 一律走包级):

// 字段订阅:每次成功 reload 都触发,回调内自行比较 old/new
func Subscribe[T, M any](m *Manager[T], extract func(*T) *M, fn func(old, new *M)) (cancel func())

// 强类型 feature flag 评估;类型不匹配返回 def
func Eval[T, V any](m *Manager[T], key string, ctx feature.EvalContext, def V) V

// 强类型子树视图(read-only 别名指针)
func Sub[T, M any](s *State[T], extract func(*T) *M) *M
State[T] — 不可变快照
type State[T any] struct {
    Value      *T             // 强类型业务结构体(Get() 直接返回)
    Hash       [32]byte       // 全局 SHA-256 指纹
    LoadedAt   int64          // unix nanoseconds
    Sources    []SourceRef    // 参与本次合并的所有 layer
    Generation uint64         // 单调递增版本号
    Cause      ReloadCause    // 触发原因 + Revisions
    // origins: 字段级来源追踪(ProvenanceTopLevel / ProvenanceFull 时填充)
}

func (s *State[T]) Explain(path string) []Origin             // oldest → newest 覆盖链
func (s *State[T]) Lookup(path string) []Origin              // 同 Explain
func (s *State[T]) LookupStrict(path string) ([]Origin, error)
func (s *State[T]) Origins() *OriginIndex
func (s *State[T]) Introspect() *Introspection               // Keys / Settings / At
func (s *State[T]) Redacted() map[string]any                 // 用构造时的 SecretRedactor
func (s *State[T]) MarshalYAML(redactor SecretRedactor) ([]byte, error)  // redactor 非 nil 时按 fastconf:"secret" 路径脱敏
func (s *State[T]) Diff(other *State[T]) []string
func (s *State[T]) FeatureRules() map[string]feature.Rule
SourceRef — Layer 元信息
type SourceRef struct {
    Name     string    // 文件路径 / provider 名称
    Kind     LayerKind // LayerFile / LayerProvider / LayerBytes / LayerCLI / LayerEnv
    Priority int
    LoadedAt int64
}
ReloadCause — 触发原因审计
type ReloadCause struct {
    Reason    string            // "initial" / "watcher" / "provider:vault://…" / "manual"
    At        int64             // reload pipeline 启动时间(unix ns)
    Revisions map[string]string // 每个 provider 的 revision(Resumable WatchFrom 用)
    Tenant    string            // TenantManager 多租户标识
}

公开 API 地图

需求 主要入口
构造 manager New[T], PresetK8s, PresetSidecar, PresetTesting, PresetHierarchical
文件与 profile WithDir, WithFS, WithProfile, WithProfiles, WithProfileEnv, WithMultiAxisOverlays
接外部 source WithProvider, WithProviderOrdered, WithProviderByName, WithProviderRegistry, WithGenerator, WithDotEnvAuto
业务读取 Manager.Get, Manager.Snapshot, Sub
成功提交后的反应 Subscribe, WithDiffReporter, Manager.Watcher
失败处理 Manager.Errors, ReloadError
预演与诊断 Manager.Plan, State.Introspect, State.Explain, State.LookupStrict
历史与恢复 WithHistory, Manager.Replay, Replay.List, Replay.Rollback
解码与校验 WithTransformers, WithTypedHook, WithSecretResolver, WithStructDefaults, WithDefaulterFunc, WithValidator, WithPolicy
可观测性 WithAuditSink, WithMetrics, WithTracer, WithDiffReporter, WithProvenance
rollout WithFeatureRules, Eval

pkg.go.dev 建议阅读顺序: NewGetSubscribe / ErrorsPlanReplay。可执行示例: ExampleNewExampleSubscribeExampleManager_ErrorsExampleManager_PlanExampleReplay_Rollback


Option 参考

所有 WithXxx 函数都返回 Option,可以任意组合传给 New[T],按调用顺序 last-write-wins 应用。

文件系统
Option 说明 默认值
WithDir(dir string) 配置根目录 "conf.d"
WithFS(fs.FS) 替代 dir 的 fs.FS(测试用)
WithStrict(bool) 未知字段是否报错 false
WithLogger(*slog.Logger) 注入 logger(任何 slog.Handler 后端均可) io.Discard(opt-in 才有日志)
WithCodecBridge(BridgeJSON | BridgeYAML) decode bridge BridgeJSON
WithMultiAxisOverlays(axes ...OverlayAxis) 多轴 Overlay(region / zone / host 等)
WithRawMapAccess(fn) decode 前的只读钩子,访问完整 merged map
Watch
Option 说明 默认值
WithWatch(bool) 启用 fsnotify false
WithDebounceInterval(d) 去抖动窗口 500ms
WithWatchPaths(paths...) 额外监视路径
Profile
Option 说明
WithProfile(p string) 显式单 profile
WithProfiles(p ...string) 多 profile 模式(用 overlay _meta.yaml.match 表达式匹配)
WithProfileEnv(name string) 从环境变量读取 profile
WithDefaultProfile(p string) 环境变量为空时的 fallback
WithProfileExpr(expr string) 全局 profile 匹配表达式(覆盖每个 overlay 的默认 membership 逻辑)
Provider
Option 说明
WithProvider(p) 注册外部 provider(核心入口)
WithProviderOrdered(p...) 按调用顺序自动分配 CLI+100, +101, ...;输入已有非零 Priority 时报错
WithProviderByName(name, cfg) 通过 Factory Registry 按名称构造 provider;解析在所有 Option 应用完之后做(与 WithProviderRegistry 顺序无关)
WithProviderRegistry(r) 注入 Manager-local *ProviderRegistry;解析时先 local,后全局默认,便于多租户/测试隔离
WithGenerator(g) assemble 阶段动态合成 layer(如 BuildInfo)
WithDotEnvAuto(prefix) WithDir 终值上自动发现 .env

pkg/provider 的工厂函数:

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

fastconf.New[Cfg](ctx,
    fastconf.WithProvider(provider.NewEnv("APP_")),                              // APP_DATABASE__DSN → database.dsn
    fastconf.WithProvider(provider.NewEnvReplacer("APP_", provider.DotReplacer)),// APP_DATABASE_DSN → database.dsn
    fastconf.WithProvider(provider.NewCLI(cliMap)),                              // 解析过的 CLI 标志
    fastconf.WithProvider(provider.NewDotEnv("APP_", ".env")),                   // 显式 .env 路径
    fastconf.WithProvider(provider.NewBytes("inline", "yaml", data)),            // 内存 layer
    fastconf.WithProvider(provider.NewLabels(labels, provider.LabelOptions{})),  // Traefik/Docker 标签
    fastconf.WithTransformers(transform.ExpandLabels(at, to, opts)),
)
Pipeline 增强
Option 说明
WithMigrations(func) 模式迁移回调(在 Transformer 之前)
WithTransformers(t...) post-merge / pre-decode 变换链
WithSecretResolver(r) transform 之后、decode 之前解密 leaf 密文
WithTypedHook(h) decode 前重写 leaf(默认含 time.Duration
WithoutDefaultTypedHooks() 关闭内置 typed hook 集
WithStructDefaults[T]() 用 struct tag (fastconf:"default=...") 填零值
WithDefaulterFunc[T](fn) 自定义 *T 默认值填充函数
WithMergeKeys(map) Kustomize 风格策略合并(list-of-object)
WithValidator[T](fn) decode 后的强类型校验;失败保留旧状态
WithPolicy[T](p) validate 后的策略评估;SeverityError 中止 reload
WithFeatureRules[T](extract) feature.Rule 表挂到 State,供 Eval 使用
可观测性
Option 说明
WithMetrics(MetricsSink) 注入 metrics sink(可选扩展 ProviderMetricsSink / StageMetricsSink / RenderMetricsSink
WithAuditSink(AuditSink) 每次成功 reload 后回调(多个 sink fan-out)
WithDiffReporter(DiffReporter) 每次产生非空 diff 时异步推送;每个 reporter 用独立 bounded-queue + worker,满则丢 + EventDropped("diff-reporter")
WithDiffReporterQueueCap(n int) 每个 reporter 的队列深度(默认 64)
WithTracer(Tracer) OTel 兼容 span tracer
WithProvenance(level) ProvenanceOff / ProvenanceTopLevel / ProvenanceFull
WithHistory(n) 保留最近 n 个成功状态(History ring)
WithSecretRedactor(r) 日志和快照中的 secret 脱敏(与 WithSecretResolver 分工:前者只脱敏展示)
ReloadOption(传给 Manager.Reload
Option 说明
WithSourceOverride(map) 注入一次性 override layer
WithReloadReason(s) 覆盖默认 "manual" 原因,便于审计

Reload 流水线

触发源
                          ┌── fsnotify events → debounce 500ms ──┐
                          │                                       │
Reload(ctx, opts...) ─────┤    reloadCh chan reloadRequest       ├──► reloadLoop
                          │                                       │    (single writer)
provider.Watch events ────┘── backoff + drop-on-full ──────────┘
Pipeline 执行序列
reloadCh.recv(req)
  │
  ├─ stageMerge:      discovery.Scan(dir) → decode files → merger.Merge(layers)
  │                   apply _meta.yaml(appendSlices / profileEnv / match)
  │                   apply _patch.json (RFC 6902)
  │
  ├─ stageAssemble:   for each provider: Load(ctx) → merge by Priority
  │
  ├─ stageMigrate:    opts.migrationRun(merged)       [optional]
  ├─ stageTransform:  for each transformer: t.Transform(merged)
  ├─ stageDecode:     json.Marshal(merged) → json.Unmarshal(→ *T)
  │                   apply fastconf:"default=…" struct tags
  ├─ stageFieldMeta:  range / enum / required 检查
  ├─ stageValidate:   for each validator: v(*T)
  ├─ stagePolicy:     for each policy:    p.Evaluate(ctx, *T, reason, tenant)
  │
  └─ commit:
       canonicalHashBytes(mergedJSON) → SHA-256 dedup
       atomic.Pointer.Store(newState)
       history.push(newState)
       for each AuditSink: Audit(ctx, cause)
       fireWatches(oldPartHashes, newPartHashes)
失败保留语义

任意 stage 返回非 nil 错误时:

  • atomic.Pointer 不更新Get() 继续返回旧值;
  • Generation 不递增
  • 错误通过 Reload(ctx).err 同步返回;同一条事件也通过 Errors() 异步广播;
  • AuditSink 不调用(只有 commit 成功才触发 Audit);
  • MetricsSink.ReloadFinished(ok=false, dur) 被调用。
Context 传播

Reload(ctx)ctx 不止控制入队/等待 —— 它会被串到执行中的 pipeline:

  • assemble 入口处 ctx.Err() 早退;
  • 每个 provider.Load(ctx) 共享同一个 ctx,慢 provider 因 ctx 取消而立刻返回;
  • 取消产生的错误以 context.Canceled / context.DeadlineExceeded 原样返回 (ErrDecode 包裹),调用方可以 errors.Is(err, context.Canceled) 做精确判断。

文件系统 watcher 与 provider watcher 自身没有 caller ctx,框架在这两条路径上自动 使用 context.Background(),保持原有"事件驱动 reload 不被外部干涉"的语义。


Profile 与 Overlay

目录结构
conf.d/
  base/                   # 所有 profile 共享的基础值
    00-defaults.yaml
    10-feature-flags.yaml
  overlays/
    dev/                  # 仅当 profile == "dev" 时叠加
      50-dev.yaml
    prod/
      50-prod.yaml
      _meta.yaml          # profile 匹配表达式
      _patch.json         # RFC 6902 patch
    staging/
      50-staging.yaml
      _meta.yaml
_meta.yaml 字段
schemaVersion: "1"
profileEnv: "APP_PROFILE"     # 读取 profile 的环境变量(优先级低于 WithProfileEnv)
defaultProfile: "dev"         # 兜底 profile
appendSlices: true            # slice 字段追加而非覆盖
match: "prod | staging"       # 布尔 profile 表达式(&, |, !, () 均支持)

matchpkg/profile 编译,语法:

语法 含义
prod profile 集合包含 "prod"
prod | staging 包含 prod 或 staging
prod & !debug 包含 prod 且不包含 debug
(eu-west | eu-east) & !debug 复合表达式
RFC 6902 JSON Patch

在任意 overlay 目录下放 _patch.json,FastConf 会在该层文件叠加完成后应用:

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

WithProfilesWithProfile 互斥;多 profile 模式下每个 overlay 的 _meta.yaml.match 用于判断是否包含。


Provider 系统

内置 Provider(pkg/provider
Provider 构造 说明
Env provider.NewEnv("APP_") APP_FOO__BARfoo.bar(双下划线分隔)
EnvReplacer provider.NewEnvReplacer("APP_", provider.DotReplacer) Viper 风格单下划线 → 点
CLI provider.NewCLI(map[string]any) 命令行 flag 解析后的 map
Bytes provider.NewBytes(name, codec, data) 内存 layer;测试 / fixture 最常用
DotEnv provider.NewDotEnv("APP_", paths...) 显式 .env 文件路径
Labels provider.NewLabels(labels, provider.LabelOptions{}) Traefik / Docker 风格 key=value 字符串列表
LabelMap provider.NewLabelMap(labels, provider.LabelOptions{}) K8s annotation 风格 map[string]string
File provider.NewFile(path, codec) 读取单个文件
内置 KV Provider(providers/{vault,consul,http},主模块内)
import (
    vault    "github.com/fastabc/fastconf/providers/vault"
    consul   "github.com/fastabc/fastconf/providers/consul"
    httpprov "github.com/fastabc/fastconf/providers/http"
)

vp, _ := vault.New("https://vault.svc", "kv/data/myapp", os.Getenv("VAULT_TOKEN"))
cp, _ := consul.New("http://consul.svc:8500", "config/myapp")
hp, _ := httpprov.New("remote", "https://example.com/cfg.yaml", yamlCodec{})

编译时裁剪(瘦身二进制):

go build -tags no_provider_vault,no_provider_consul,no_provider_http ./...
contracts.Provider 接口
type Provider interface {
    Name()     string
    Priority() int
    Load(ctx context.Context) (map[string]any, error)
    Watch(ctx context.Context) (<-chan Event, error) // 无能力的 provider 返回 (nil, nil)
}
Priority 常量

合并顺序由 Priority() 数值升序决定 —— 数值越大,越后合并,越能覆盖:

常量 数值 用途
PriorityDotEnv 5 .env 兜底(最低)
PriorityStatic 10 静态 / 文件层默认
PriorityOverlay 20 overlay providers
PriorityKV 30 Vault / Consul / HTTP / NATS / Redis-Streams
PriorityK8s 40 K8s ConfigMap / Secret
PriorityEnv 50 进程环境变量 provider
PriorityCLI 60 命令行 flag provider(最高)

如果不想思考 Priority,用 WithProviderOrdered(p1, p2, p3) — 它把传入的 providers 按调用顺序分配 PriorityCLI+100, +101, +102 …,最后一个传入的赢; 若某个输入 provider 已显式设置非零 Priority,会直接报错,避免静默覆盖。

Resumable(断点续订)
type Resumable interface {
    // lastRev 为空时等价于 Watch(冷订阅)。
    // lastRev 非空时从该 revision 之后的变更开始推送。
    // 若 revision 已被压缩,返回 ErrResumeUnsupported,框架回退到冷订阅。
    WatchFrom(ctx context.Context, lastRev string) (<-chan Event, error)
}

框架自动记忆每个 provider 最后观测到的 Event.Revision,在断线重连时传给 WatchFrom

Provider Factory Registry
// 注册(通常在 init() 或 TestMain 中)
fastconf.RegisterProviderFactory("vault", func(cfg map[string]any) (contracts.Provider, error) {
    addr, _ := cfg["addr"].(string)
    path, _ := cfg["path"].(string)
    token, _ := cfg["token"].(string)
    return vault.New(addr, path, token)
})

// 使用(让 provider 配置自己来自 YAML)
mgr, err := fastconf.New[AppConfig](ctx,
    fastconf.WithProviderByName("vault", map[string]any{
        "addr":  "https://vault.svc",
        "path":  "kv/data/myapp",
        "token": os.Getenv("VAULT_TOKEN"),
    }),
)

Manager-local registry(多租户 / 测试隔离):

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

mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithProviderRegistry(local),
    fastconf.WithProviderByName("scoped", map[string]any{...}),
)
// 全局默认 registry 不被污染;同名 factory local 优先;只在全局存在的名字仍可解析。

Transformer 与 Migration

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

Transformer 在 merge 完成、decode 之前运行,接收 map[string]any,可安全修改树 结构。

内置 Transformer(pkg/transform
import "github.com/fastabc/fastconf/pkg/transform"

fastconf.WithTransformers(
    transform.Defaults(map[string]any{                 // 填默认(递归合并,不覆盖已有)
        "server": map[string]any{"timeout": "30s"},
    }),
    transform.SetIfAbsent("server.timeout", "30s"),    // 单值缺省
    transform.EnvSubst(),                              // 替换 ${VAR} / ${VAR:-default}
    transform.DeletePaths("internal.debug"),
    transform.Aliases(map[string]string{               // 旧路径 → 新路径
        "db.url":      "database.dsn",
        "server.port": "server.addr",
    }),
)
Struct Tag
type AppConfig struct {
    Server struct {
        Addr    string        `json:"addr"    fastconf:"default=:8080"`
        Timeout time.Duration `json:"timeout" fastconf:"default=30s"`
    } `json:"server"`
    Database struct {
        DSN string `json:"dsn" fastconf:"secret"` // 日志 / 快照中自动脱敏
    } `json:"database"`
}

mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithStructDefaults[AppConfig](),                 // 启用 default 标签
    fastconf.WithSecretRedactor(fastconf.DefaultSecretRedactor),
)

fastconf:"default=…"stageDecode 之后、stageValidate 之前应用,只填充零 值字段。Field-meta(range=, enum=, required)在同阶段检查。

Migration(模式迁移)
import "github.com/fastabc/fastconf/pkg/migration"

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

或一次性 inline:

fastconf.WithMigrations(func(root map[string]any) error {
    if v, ok := root["db_url"]; ok {
        db, _ := root["database"].(map[string]any)
        if db == nil { db = map[string]any{}; root["database"] = db }
        if _, has := db["dsn"]; !has { db["dsn"] = v }
        delete(root, "db_url")
    }
    return nil
})

Watch、Subscribe 与 Plan

文件系统 Watch
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithWatch(true),
    fastconf.WithDebounceInterval(500*time.Millisecond),
)
// K8s ConfigMap 的 ..data symlink 原子交换由父目录 fsnotify 正确处理。
字段订阅(Subscribe
cancel := fastconf.Subscribe(mgr,
    func(app *AppConfig) *DatabaseConfig { return &app.Database },
    func(old, neu *DatabaseConfig) {
        if old != nil && *old == *neu { return } // 调用方自行 diff
        reconnect(neu.DSN)
    },
)
defer cancel()

Subscribe 在每次成功 reload 时同步触发(reload goroutine 中执行;recover() 隔 离 panic)。长耗时操作请自行 go func() 异步。

手动触发 & 一次性 override
err := mgr.Reload(ctx,
    fastconf.WithReloadReason("admin-cli"),
    fastconf.WithSourceOverride(map[string]any{
        "server": map[string]any{"addr": ":9999"},
    }),
)
Pause / Resume
mgr.Watcher().Pause()
applyBatchUpdate()
mgr.Watcher().Resume()
Plan — Dry-run
result, err := mgr.Plan().WithHostname("ci-runner-7").Run(ctx)
if err != nil {
    log.Fatal("plan failed:", err)
}
for _, r := range result.Validators {
    if r.Err != nil {
        log.Printf("validator %s failed: %v", r.Name, r.Err)
    }
}
for _, v := range result.Policies {
    log.Printf("[%s] %s @ %s — %s", v.Severity, v.Rule, v.Path, v.Message)
}

Plan 不更新 atomic.PointerSeverityError 的 policy violation 在 dry-run 中降级为警告,便于 CI/CD 把所有问题一次性列完。


Provenance、History 与 Rollback

Provenance(字段来源追踪)
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithProvenance(fastconf.ProvenanceFull),
)

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

// 严格查询(区分"未开启 provenance"和"路径不存在")
origins, err := mgr.Snapshot().LookupStrict("database.dsn")
Level 开销 能追踪什么
ProvenanceOff
ProvenanceTopLevel O(top-level keys) 每个顶层字段最终来自哪个 layer
ProvenanceFull O(leaves) 每个叶子字段的完整覆盖链 + 每层的原始值
History 与 Rollback
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDir("conf.d"),
    fastconf.WithHistory(10),
)

history := mgr.Replay().List()        // []*State[T],从旧到新
target  := history[len(history)-2]    // 上一个版本
_ = mgr.Replay().Rollback(target)

Rollback 把历史 *State[T] 重新发布到 atomic.Pointer:不重新执行 pipeline、 不递增 Generation,但会触发 Subscribe 回调(调用方自行 filter)。

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

缓冲 16,drop-on-full;失败保留旧状态的契约不变。


可观测性

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

sink := fastconf.NewJSONAuditSink(os.Stderr) // 内置 JSON-lines 实现
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithAuditSink(sink),       // 多个 sink fan-out
    fastconf.WithAuditSink(remoteSink),
)
// 输出:{"reason":"watcher","at":"2026-05-14T08:00:00Z","revisions":{"vault":"42"}}
MetricsSink
type MetricsSink interface {
    ReloadStarted()
    ReloadFinished(ok bool, dur time.Duration)
    // 可选扩展:ProviderMetricsSink / StageMetricsSink / RenderMetricsSink
}

Prometheus 实现在独立 sub-module:

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

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

默认 noop;OTel SDK 集成在独立 sub-module:

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

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

-tags fastconf_otel 启用 span 属性的额外 enrich。

DiffReporter
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.WithDiffReporter(fastconf.DiffReporterFunc(
        func(ctx context.Context, ev fastconf.DiffEvent) error {
            return slack.Post(ctx, ev.Diff) // 异步执行,不阻塞 reload
        },
    )),
    fastconf.WithDiffReporterQueueCap(128), // 默认 64
)

每个 reporter 拥有独立的 bounded-queue worker:

  • 入队是非阻塞的;reload 主线程不会被慢 reporter 拖住。
  • 队列满时事件被丢弃(drop-on-full),并触发 MetricsSink.EventDropped("diff-reporter")
  • 调用 Manager.Close() 时 worker 通过 m.closed 信号优雅退出, bgWG.Wait() 保证不留悬挂 goroutine。
  • 通过 WithDiffReporterQueueCap(n) 调节每个 reporter 的队列深度(默认 64)。
Policy(策略引擎)
import "github.com/fastabc/fastconf/policy"

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

CUE / OPA 实现在独立 sub-module:policy/cuepolicy/opa

Severity Plan 行为 Reload 行为
SeverityWarning 记录警告,继续 记录警告,继续
SeverityError 降级为警告(dry-run 全量收集) 中止 reload,保留旧状态

多租户与 Preset

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

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

app, err := tm.Get("tenant-a") // *AppConfig, error(fastconf.ErrUnknownTenant)
_ = tm.Remove("tenant-a")      // 调用底层 Manager.Close()
tm.Close()

每个 tenant 完全隔离,AuditSink 自动注入 Cause.Tenant = id

Preset
// K8s ConfigMap 标准部署
mgr, _ := fastconf.New[AppConfig](ctx,
    fastconf.PresetK8s(fastconf.K8sOpts{
        Dir: "/etc/config", ProfileEnv: "APP_PROFILE", Default: "default", Watch: true,
    }),
    fastconf.WithStrict(false), // 覆盖 Preset 的 strict=true
)

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

// 测试(内存 layer)
fastconf.PresetTesting(fastconf.TestingOpts{
    Data: map[string]any{"server": map[string]any{"addr": ":9090"}},
})

// 多轴 overlay:region / zone / host
fastconf.PresetHierarchical(fastconf.HierarchicalOpts{ /* ... */ })

为什么没有 GetString("a.b.c")

这是 FastConf 的边界,不是遗漏。

  • 业务热路径应该走 mgr.Get().Server.Addr:强类型、零反射、零分配。
  • CLI / dump / diff 这类动态场景,走 state.Introspect().Keys()Settings()At(path)
  • 如果你的配置天生没有稳定 schema,也可以直接使用 fastconf.New[map[string]any](...),只是会主动放弃类型安全。

详见 docs/cookbook/introspect.md


性能与可靠性

最近一次基准重测环境:Apple M2 / darwin-arm64 / Go 1.26.2

Benchmark 中位数
BenchmarkGet 0.52 ns/op
BenchmarkReloadNoop 15.1 µs/op
BenchmarkReloadCommitSmall 16.5 µs/op
BenchmarkReloadManySubscribers/50 17.5 µs/op
BenchmarkIntrospectCold 1.67 µs/op
BenchmarkExplainDeep 219 ns/op

完整基线、命令和解释见 docs/design/perf.md。当前契约是: 热读极轻、reload 可失败但不污染 live state、订阅 fan-out 不阻塞读取


Sub-module 生态矩阵

主模块内置包(随根模块版本发布,import 路径不变)
路径 说明
contracts contracts Provider / Codec / Source / Event 接口定义
pkg/* pkg/{decoder,discovery,feature,flog,generator,mappath,merger,migration,profile,provider,transform,validate} 公开可复用实现原语
internal/* internal/{debounce,obs,typeinfo,watcher} 编译时 API boundary 私有 helper
http providers/http HTTP / SSE Provider(build tag no_provider_http
vault providers/vault HashiCorp Vault KV v2(build tag no_provider_vault
consul providers/consul Consul KV(build tag no_provider_consul
policy policy Policy 接口 + Func adapter
integrations/bus integrations/bus 配置变更事件总线
integrations/render integrations/render 模板渲染扩展
cmd/fastconfd cmd/fastconfd Sidecar HTTP + SSE 服务(与主模块同版)
独立 Sub-module(按需 go get
Sub-module 路径 Tag prefix 主要依赖
validate/playground validate/playground validate/playground/vX.Y.Z go-playground/validator
prometheus observability/metrics/prometheus observability/metrics/prometheus/vX.Y.Z prometheus/client_golang
otel observability/otel observability/otel/vX.Y.Z OpenTelemetry SDK
cue-policy policy/cue policy/cue/vX.Y.Z cuelang.org/go
opa-policy policy/opa policy/opa/vX.Y.Z open-policy-agent/opa
cue-validate validate/cue/cuelang validate/cue/cuelang/vX.Y.Z cuelang.org/go
log/phuslu integrations/log/phuslu integrations/log/phuslu/vX.Y.Z phuslu/log
log/zerolog integrations/log/zerolog integrations/log/zerolog/vX.Y.Z rs/zerolog
nats provider providers/nats providers/nats/vX.Y.Z 仅根 module(注入用户的 nats.Conn
redis-streams provider providers/redisstream providers/redisstream/vX.Y.Z 仅根 module(注入用户的 redis.Client
openfeature integrations/openfeature integrations/openfeature/vX.Y.Z OpenFeature SDK
cmd/fastconfctl cmd/fastconfctl cmd/fastconfctl/vX.Y.Z 仅根 module
cmd/fastconfgen cmd/fastconfgen cmd/fastconfgen/vX.Y.Z yaml.v3

统一打 tag(tools/tag-release.sh):

./tools/tag-release.sh vX.Y.Z          # 本地打全部 tag
./tools/tag-release.sh vX.Y.Z --push   # 同时推送(触发 release.yml)
./tools/tag-release.sh vX.Y.Z --force --push

扩展指南

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

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

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

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

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

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

fastconf.WithTransformers(PrefixTransformer{Prefix: "myorg"})
自定义 Codec
fastconf.RegisterCodec("toml", tomlCodec{})
fastconf.RegisterCodecExt("toml", "toml") // 让 .toml 扩展名走 "toml" codec
选择扩展点
需求 选择
新增数据源 实现 contracts.Provider
合并后改写树结构 实现 Transformer
decode 前解密 leaf 实现 SecretResolver
decode 前类型重写 leaf 实现 decoder.TypedHook
validate 后断言 WithValidator / WithPolicy
发布后动作 AuditSink / DiffReporter
新文件格式 实现 contracts.Codec + RegisterCodec

CLI 工具

fastconfd — Sidecar 服务
fastconfd --dir=/etc/config --profile=prod --addr=:8081
端点 方法 说明
/healthz GET {"status":"ok","generation":N}
/version GET 当前 State 版本(Hash + Generation)
/config GET 当前配置 JSON(secret 已脱敏)
/reload POST 触发手动 reload;接受 {"request_id":"…"}
/events GET SSE 流;每次成功 reload 推送 ReloadCause JSON
fastconfctl — 管理 CLI
fastconfctl snapshot --addr=:8081
fastconfctl reload   --addr=:8081 --request-id=deploy-123
fastconfctl plan     --addr=:8081
fastconfctl rollback --addr=:8081 --generation=42
fastconfctl sources  --addr=:8081
fastconfgen — 代码生成器
fastconfgen generate --input=conf.d/base/00-app.yaml --pkg=config --out=config/config_gen.go

本地开发

# 拉依赖
go mod tidy

# 构建 / 测试 / Lint
make build
make test         # 等价于 go test -race -count=1 ./...
make test-all     # 含 cmd/ 子模块
make lint         # 需要 golangci-lint

# Example 全跑
go test ./... -run '^Example' -v

# 性能基准
go test -bench=BenchmarkGet -benchmem ./...

# CI 防线
bash tools/check-layout.sh
bash tools/check-doc-symbols.sh
bash tools/check-deps.sh
bash tools/bench-guard.sh        # ns/op + allocs 阈值
bash tools/loc-budget.sh         # 主包 LOC 预算
bash tools/total-loc-budget.sh   # 全树 LOC 预算

# 代码评审依赖图
bash tools/code-review-graph.sh

文档地图

文档 用途
docs/cookbook/README.md 所有 recipe 的单一入口
docs/design/spec.md 运行模型、并发与模块边界
docs/design/perf.md 最新 benchmark baseline
CHANGELOG.md 变更记录
pkg.go.dev godoc 与 Example

最常用的 recipe:


License

See LICENSE.

Documentation

Overview

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

Start here

A typical application reads FastConf in this order:

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

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

Core ideas

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

Reading by need

  • Loading and overlays: New, Option, PresetK8s, PresetSidecar, WithProvider, WithProfile, WithMultiAxisOverlays.
  • Runtime reaction: Subscribe, Manager.Errors, Manager.Watcher, DiffReporter.
  • Inspection and recovery: Manager.Snapshot, State.Introspect, State.Explain, Manager.Plan, Manager.Replay.
  • Extension points: Transformer, WithTypedHook, WithSecretResolver, WithValidator, WithPolicy, AuditSink, MetricsSink, Tracer.

Module layout

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

cmd/fastconfctl, cmd/fastconfgen
integrations/log/phuslu, integrations/log/zerolog, integrations/openfeature
observability/metrics/prometheus, observability/otel
policy/cue, policy/opa
providers/nats, providers/redisstream
validate/cue/cuelang, validate/playground

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

Key files

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

Example_basic demonstrates loading a profile overlay from a config directory.

package main

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

	"github.com/fastabc/fastconf"
)

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

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

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

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

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

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

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

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

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

Example_externalProvider demonstrates plugging a third-party provider into fastconf.

package main

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

	"github.com/fastabc/fastconf"
	"github.com/fastabc/fastconf/contracts"

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

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() {
	demo := &staticExampleProvider{
		name:     "demo-static",
		priority: contracts.PriorityKV,
		data: map[string]any{
			"server":  map[string]any{"addr": ":9090"},
			"feature": map[string]any{"betaEnabled": true},
		},
	}

	// NewBytes default priority is 9000 (high); to let `demo` (PriorityKV=30)
	// supply the override, push the seed bytes below it.
	seed := provider.NewBytes("seed", "yaml", []byte("server:\n  addr: \":8080\"\nfeature:\n  betaEnabled: false\n")).
		WithPriority(contracts.PriorityStatic)

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

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

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

package main

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

	"github.com/fastabc/fastconf"
)

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

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

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

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

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

Index

Examples

Constants

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

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

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

View Source
const (
	BridgeJSON = bridgeJSON
	BridgeYAML = bridgeYAML
)

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

View Source
const DefaultDebounceInterval = 500 * time.Millisecond

DefaultDebounceInterval is the file-watcher debounce window. Events arriving within this window are coalesced into a single reload.

View Source
const DefaultSidecarHistoryCap = 16

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

Variables

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

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

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

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

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

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

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

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

View Source
var ErrPolicyDenied = errors.New("fastconf: policy denied")

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

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

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

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

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

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

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

Functions

func DefaultSecretRedactor

func DefaultSecretRedactor(_ string, _ any) any

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

func Eval

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

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

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

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

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

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

func LookupCodec

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

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

func RegisterCodec

func RegisterCodec(name string, c contracts.Codec)

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

func RegisterCodecExt

func RegisterCodecExt(ext, codec string)

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

func RegisterProviderFactory

func RegisterProviderFactory(name string, f ProviderFactory)

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

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

func RegisteredProviderNames

func RegisteredProviderNames() []string

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

func Sub

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

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

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

func Subscribe

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

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

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

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

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

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

Example

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

package main

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

	"github.com/fastabc/fastconf"
)

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

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

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

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

Types

type AuditSink

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

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

type AuditSinkFunc

type AuditSinkFunc func(context.Context, ReloadCause) error

AuditSinkFunc adapts a free function into an AuditSink.

func (AuditSinkFunc) Audit

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

Audit implements AuditSink.

type Defaulter

type Defaulter interface {
	Defaults()
}

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

Example:

type AppConfig struct { Port int; DataDir string }

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

type DiffEvent

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

DiffEvent is the payload handed to every DiffReporter.

type DiffReporter

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

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

type DiffReporterFunc

type DiffReporterFunc func(context.Context, DiffEvent) error

DiffReporterFunc adapts a function into a DiffReporter.

func (DiffReporterFunc) Report

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

Report implements DiffReporter.

type DiffReporterMetricsSink

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

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

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

type EvalContext

type EvalContext = feature.EvalContext

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

type FeatureRule

type FeatureRule = feature.Rule

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

type FieldSpec

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

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

type HierarchicalOpts

type HierarchicalOpts struct {
	Dir       string        // config root directory (default DefaultDir)
	RegionEnv string        // env var for region axis (default "REGION")
	ZoneEnv   string        // env var for zone axis (default "ZONE")
	HostEnv   string        // env var for host axis (default "HOST")
	Watch     bool          // enable fsnotify hot-reload
	Debounce  time.Duration // debounce interval (0 = DefaultDebounceInterval)
}

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

type LayerKind uint8

LayerKind identifies the merge semantics of a layer.

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

func (LayerKind) String

func (k LayerKind) String() string

String returns a human-readable name for the LayerKind.

type Manager

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

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

Typical usage:

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

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

func New

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

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

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

Example

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

package main

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

	"github.com/fastabc/fastconf"
)

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

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

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

func (*Manager[T]) Close

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

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

func (*Manager[T]) Errors

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

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

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

Example

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

package main

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

	"github.com/fastabc/fastconf"
)

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

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

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

func (*Manager[T]) Get

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

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

func (*Manager[T]) Plan

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

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

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

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

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

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

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

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

func (*Manager[T]) Reload

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

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

Options:

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

func (*Manager[T]) Replay

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

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

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

func (*Manager[T]) Snapshot

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

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

func (*Manager[T]) Watcher

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

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

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

type MetricsSink

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

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

type MigrationApplier

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

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

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

type MigrationFunc

type MigrationFunc func(map[string]any) error

MigrationFunc adapts a plain function to MigrationApplier.

func (MigrationFunc) Migrate

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

Migrate implements MigrationApplier.

type Option

type Option func(*options)

Option configures Manager behavior.

func PresetHierarchical

func PresetHierarchical(p HierarchicalOpts) Option

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

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

Example directory layout:

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

func PresetK8s

func PresetK8s(p K8sOpts) Option

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

func PresetSidecar

func PresetSidecar(p SidecarOpts) Option

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

func PresetTesting

func PresetTesting(p TestingOpts) Option

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

func WithAuditSink

func WithAuditSink(sink AuditSink) Option

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

func 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

func WithDebounceInterval(d time.Duration) Option

WithDebounceInterval sets the watch debounce window. Default: DefaultDebounceInterval.

func WithDefaultProfile

func WithDefaultProfile(p string) Option

WithDefaultProfile sets the fallback profile when no explicit profile exists.

func WithDefaulterFunc

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

WithDefaulterFunc installs a post-decode defaults function for cases where *T cannot implement the Defaulter interface (e.g., third-party types or when modifying the struct definition is not possible). It runs at the same point as the interface check: after struct-tag defaults and before validators.

func WithDiffReporter

func WithDiffReporter(r DiffReporter) Option

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

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

Tune the per-reporter queue depth with WithDiffReporterQueueCap.

func WithDiffReporterQueueCap

func WithDiffReporterQueueCap(n int) Option

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

func WithDir

func WithDir(dir string) Option

WithDir sets the configuration root directory. Default: DefaultDir.

func WithDotEnvAuto

func WithDotEnvAuto(prefix string) Option

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

Resolution is deferred to the end of option application so option order no longer matters — WithDotEnvAuto("APP_") placed before WithDir("conf.d") works correctly. The prefix is stashed and resolved once just before New() builds its Manager. This is the one Option whose mechanics cannot be replaced by a single WithProvider call (because it needs the final o.dir value); other env / .env / label / CLI / bytes sugars were removed in v0.14 — use WithProvider(provider.NewEnv(...)), WithProvider( provider.NewDotEnv(...)), WithProvider(provider.NewLabels(...)), WithProvider(provider.NewCLI(...)), WithProvider(provider.NewBytes(...)) directly.

func WithFS

func WithFS(f fs.FS) Option

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

func WithFeatureRules

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

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

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

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

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

func WithGenerator

func WithGenerator(g contracts.Generator) Option

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

func WithHistory

func WithHistory(n int) Option

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

func WithLogger

func WithLogger(l *slog.Logger) Option

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

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

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

fastconf.WithLogger(slog.New(myHandler))

func WithMergeKeys

func WithMergeKeys(keys map[string]string) Option

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

func WithMetrics

func WithMetrics(m MetricsSink) Option

WithMetrics injects the metrics sink used by the reload pipeline.

func WithMigrations

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

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

func WithMultiAxisOverlays

func WithMultiAxisOverlays(axes ...OverlayAxis) Option

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

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

Directories that do not exist are silently skipped.

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

Usage:

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

func WithPolicy

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

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

Use:

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

func WithProfile

func WithProfile(p string) Option

WithProfile sets the active overlay profile explicitly.

func WithProfileEnv

func WithProfileEnv(name string) Option

WithProfileEnv sets the environment variable used to resolve the profile.

func WithProfileExpr

func WithProfileExpr(expr string) Option

WithProfileExpr appends a global match expression to every overlay.

func WithProfiles

func WithProfiles(p ...string) Option

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

func WithProvenance

func WithProvenance(level ProvenanceLevel) Option

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

func WithProvider

func WithProvider(p contracts.Provider) Option

WithProvider registers an external provider merged after file layers.

func WithProviderByName

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

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

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

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

func WithProviderOrdered

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

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

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

func WithProviderRegistry

func WithProviderRegistry(r *ProviderRegistry) Option

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

Use cases:

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

func WithRawMapAccess

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

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

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

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

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

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

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

func WithSecretRedactor

func WithSecretRedactor(r SecretRedactor) Option

WithSecretRedactor installs the secret redactor used by dumps and snapshots.

func WithSecretResolver

func WithSecretResolver(r SecretResolver) Option

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

func WithStrict

func WithStrict(strict bool) Option

WithStrict enables strict file and merge validation.

func WithStructDefaults

func WithStructDefaults[T any]() Option

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

func WithTracer

func WithTracer(t Tracer) Option

WithTracer installs a tracer. The framework opens spans for the reload root plus seven stages: assemble, merge, migration, transform, decode, validate, commit. Pass nil to keep the default no-op tracer.

func WithTransformers

func WithTransformers(t ...Transformer) Option

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

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

func WithTypedHook

func WithTypedHook(h decoder.TypedHook) Option

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

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

func WithValidator

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

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

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

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

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

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

func WithWatch

func WithWatch(enabled bool) Option

WithWatch enables file-system driven reloads.

func WithWatchPaths

func WithWatchPaths(paths ...string) Option

WithWatchPaths appends additional paths to watch.

func WithoutDefaultTypedHooks

func WithoutDefaultTypedHooks() Option

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

type Origin

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

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

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

type OriginIndex

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

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

func (*OriginIndex) Explain

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

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

func (*OriginIndex) Format

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

Format renders an explain entry as one line per origin.

func (*OriginIndex) Paths

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

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

type OverlayAxis

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

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

Example:

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

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

Axis value resolution order:

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

type PlanBuilder

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

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

func (*PlanBuilder[T]) Run

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

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

func (*PlanBuilder[T]) WithHostname

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

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

type PlanResult

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

PlanResult describes the outcome of Manager.Plan.

type PolicyError

type PolicyError struct {
	Violations []policy.Violation
}

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

func (*PolicyError) Error

func (e *PolicyError) Error() string

func (*PolicyError) Is

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

type ProvenanceLevel

type ProvenanceLevel uint8

ProvenanceLevel controls how aggressively the merger records field origins.

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

type ProviderFactory

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

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

func LookupProviderFactory

func LookupProviderFactory(name string) (ProviderFactory, bool)

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

type ProviderMetricsSink

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

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

type ProviderRegistry

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

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

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

func NewProviderRegistry

func NewProviderRegistry() *ProviderRegistry

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

func (*ProviderRegistry) Lookup

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

Lookup returns a registered factory and whether it existed.

func (*ProviderRegistry) Names

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

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

func (*ProviderRegistry) Register

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

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

type ReloadCause

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

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

type ReloadError

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

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

type ReloadOption

type ReloadOption func(*reloadConfig)

ReloadOption tunes a single Reload invocation.

func WithReloadReason

func WithReloadReason(reason string) ReloadOption

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

func WithSourceOverride

func WithSourceOverride(override map[string]any) ReloadOption

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

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

type RenderMetricsSink

type RenderMetricsSink interface {
	RenderError(name string)
}

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

type Replay

type Replay[T any] Manager[T]

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

func (*Replay[T]) List

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

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

func (*Replay[T]) Rollback

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

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

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

Example

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

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

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

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

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

type SecretRedactor

type SecretRedactor func(path string, value any) any

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

type SecretRef

type SecretRef struct {
	Scheme string
	Body   string
}

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

type SecretResolver

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

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

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

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

type SecretResolverFunc

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

SecretResolverFunc adapts a pair of functions into a SecretResolver.

func (SecretResolverFunc) Recognize

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

Recognize implements SecretResolver.

func (SecretResolverFunc) Resolve

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

Resolve implements SecretResolver.

type SidecarOpts

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

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

type SourceRef

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

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

type Span

type Span = contracts.Span

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

type StageMetricsSink

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

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

type State

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

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

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

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

func (*State[T]) Diff

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

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

func (*State[T]) Explain

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

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

func (*State[T]) FeatureRules

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

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

func (*State[T]) Introspect

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

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

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

func (*State[T]) Lookup

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

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

func (*State[T]) LookupStrict

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

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

func (*State[T]) MarshalYAML

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

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

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

func (*State[T]) Origins

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

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

func (*State[T]) Redact

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

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

func (*State[T]) Redacted

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

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

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

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

type TenantManager

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

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

Design choices:

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

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

func NewTenantManager

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

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

func (*TenantManager[T]) Add

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

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

func (*TenantManager[T]) Close

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

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

func (*TenantManager[T]) Get

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

Get returns the manager for id; ErrUnknownTenant if absent.

func (*TenantManager[T]) Has

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

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

func (*TenantManager[T]) Remove

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

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

func (*TenantManager[T]) Tenants

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

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

type TestingOpts

type TestingOpts struct {
	FS      fs.FS
	Profile string
}

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

type Tracer

type Tracer interface {
	Start(ctx context.Context, name string) (context.Context, Span)
}

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

The framework guarantees:

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

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

type Transformer

type Transformer = transform.Transformer

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

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

type ValidatorReport

type ValidatorReport struct {
	Name string
	Err  error
}

ValidatorReport is one row in PlanResult.Validators.

type Watcher

type Watcher[T any] Manager[T]

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

func (*Watcher[T]) Pause

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

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

func (*Watcher[T]) Paused

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

Paused reports the current pause state.

func (*Watcher[T]) Resume

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

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

Directories

Path Synopsis
cmd
fastconfctl command
Command fastconfctl is a CLI companion to FastConf for CI / ops:
Command fastconfctl is a CLI companion to FastConf for CI / ops:
fastconfd command
fastconfd is the Phase 26 sidecar daemon.
fastconfd is the Phase 26 sidecar daemon.
fastconfgen command
fastconfgen reads a YAML or JSON configuration sample and emits an equivalent Go struct definition.
fastconfgen reads a YAML or JSON configuration sample and emits an equivalent Go struct definition.
internal/cli
Package cli centralises the FastConf command-line flag set so every cmd/* binary registers -dir / -profile / -strict / -watch with identical defaults and semantics, and constructs the Manager via a single canonical path.
Package cli centralises the FastConf command-line flag set so every cmd/* binary registers -dir / -profile / -strict / -watch with identical defaults and semantics, and constructs the Manager via a single canonical path.
Package contracts is the **public, stable** surface of FastConf interfaces.
Package contracts is the **public, stable** surface of FastConf interfaces.
fastconf
contracts module
integrations
bus
Package bus provides a small message-bus abstraction for FastConf.
Package bus provides a small message-bus abstraction for FastConf.
render
Package render plugs FastConf into the long tail of legacy daemons that only consume on-disk configuration files (nginx.conf, envoy.yaml, postgresql.conf, ...).
Package render plugs FastConf into the long tail of legacy daemons that only consume on-disk configuration files (nginx.conf, envoy.yaml, postgresql.conf, ...).
internal
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.
obs
testutil
Package testutil centralises test helpers shared across the fastconf module.
Package testutil centralises test helpers shared across the fastconf module.
typeinfo
Package typeinfo provides a single, cached reflect.Type walker for FastConf's per-T metadata extraction (secret paths, default tags, top-level field hashers).
Package typeinfo provides a single, cached reflect.Type walker for FastConf's per-T metadata extraction (secret paths, default tags, top-level field hashers).
watcher
Package watcher subscribes to filesystem changes and triggers reloads.
Package watcher subscribes to filesystem changes and triggers reloads.
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.
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.
validate
Package validate hosts reusable validation primitives for FastConf.
Package validate hosts reusable validation primitives for FastConf.
Package policy defines the Phase 23 policy interface.
Package policy defines the Phase 23 policy interface.
providers
consul
Package consul is a first-party Consul KV provider for FastConf.
Package consul is a first-party Consul KV provider for FastConf.
http
Package http is a first-party HTTP/HTTPS provider for FastConf.
Package http is a first-party HTTP/HTTPS provider for FastConf.
vault
Package vault is a first-party HashiCorp Vault KV v2 provider for FastConf.
Package vault is a first-party HashiCorp Vault KV v2 provider for FastConf.

Jump to

Keyboard shortcuts

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