config

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package config provides a Config interface with YAML + environment variable loading. Environment variables take precedence over YAML values. Nested keys use dot notation (e.g., "server.port").

ref: go-micro/go-micro config/config.go — Config interface with Get/Scan pattern Adopted: Get/Scan/Keys interface shape. Deviated: simpler flat-map model with dot-separated keys instead of go-micro's source/reader/value abstraction layers.

Package config provides YAML + environment variable configuration loading for GoCell applications. Environment variables take precedence over YAML values. Nested keys use dot notation (e.g., "server.port").

Example:

cfg, err := config.Load("config.yaml", "APP")
if err != nil { ... }
port := cfg.Get("server.http.port")

For testing, use NewFromMap to create a Config from an existing map:

cfg := config.NewFromMap(map[string]any{"server.port": 8080})

Package config — watcher.go

ref: spf13/viper viper.go — WatchConfig watches the parent directory for atomic saves and renames; fsnotify docs recommend directory-level watch. Adopted: watch filepath.Dir(path), filter by filepath.Base(path). Deviated from go-micro (file-level watch with Rename re-add): directory-level handles atomic replace (rename+create, remove+recreate).

ref: thanos-io/thanos pkg/reloader/reloader.go — debounce with configurable delay interval and timer reset on each event. Adopted: WithDebounce option.

ref: spf13/viper viper.go — filepath.EvalSymlinks for Kubernetes ConfigMap ..data symlink pivot detection. Adopted: checkSymlinkPivot on each event.

ref: kubernetes/kubernetes — generation/observedGeneration pattern for tracking desired vs applied config state. Adopted: ObservedGenerationer on config.

Index

Constants

View Source
const (
	EventTypeWrite        = "write"
	EventTypeCreate       = "create"
	EventTypeSymlinkPivot = "symlink_pivot"
)

Event type constants for WatcherCollector.RecordEvent.

Variables

This section is empty.

Functions

func DeepCloneValue

func DeepCloneValue(v any) any

DeepCloneValue recursively deep-copies a config value. It handles the types produced by YAML unmarshalling: map[string]any, []any, and primitives (string, int, float64, bool) which are immutable and returned as-is.

func Diff

func Diff(oldData, newData map[string]any) (added, updated, removed []string)

Diff computes the difference between two flat config maps. It returns keys that were added, updated (value changed), or removed. Returned slices are sorted for deterministic output.

ref: micro/go-micro config/watcher.go — checksum-based change dedup Adopted: explicit key-set diff for deterministic change detection.

func HasDrift

func HasDrift(cfg Config) bool

HasDrift reports whether the config's desired generation differs from the observed (applied) generation. Useful for health endpoints to detect stale config state. Returns false if cfg does not implement both interfaces.

Types

type Config

type Config interface {
	// Get returns the value for the given dot-separated key, or nil if absent.
	Get(key string) any
	// Scan unmarshals the entire configuration into dest.
	Scan(dest any) error
	// Keys returns all available configuration keys sorted alphabetically.
	Keys() []string
}

Config provides read access to configuration values.

func Load

func Load(yamlPath, envPrefix string) (Config, error)

Load reads a YAML file and overlays environment variable overrides. Environment variables override YAML values. The env prefix maps to nested keys by replacing underscores with dots and lowering the case. For example, APP_SERVER_PORT overrides the key "app.server.port".

func NewFromMap

func NewFromMap(data map[string]any) Config

NewFromMap creates a Config from an existing map (useful for testing).

type Generationer

type Generationer interface {
	Generation() int64
}

Generationer is an optional interface for configs that track a monotonically increasing reload generation. Generation starts at 0 (initial load) and increments by 1 on each successful Reload. Failed reloads do not increment.

type KeyFilter

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

KeyFilter checks whether any of a set of changed keys matches registered prefixes. This is a utility type for bootstrap-level filtering — the watcher itself does not apply key filtering (it watches files, not config keys).

func NewKeyFilter

func NewKeyFilter(prefixes ...string) *KeyFilter

NewKeyFilter creates a KeyFilter for the given key prefixes.

func (*KeyFilter) Matches

func (f *KeyFilter) Matches(keys []string) bool

Matches returns true if any key in keys has any of the registered prefixes. An empty filter (no prefixes) matches everything. An empty keys list matches nothing (unless the filter is also empty).

type NoopWatcherCollector

type NoopWatcherCollector struct{}

NoopWatcherCollector is a no-op implementation used when metrics are disabled. All methods are safe for concurrent use.

func (NoopWatcherCollector) RecordDebounceCoalesced

func (NoopWatcherCollector) RecordDebounceCoalesced()

func (NoopWatcherCollector) RecordEvent

func (NoopWatcherCollector) RecordEvent(string)

func (NoopWatcherCollector) RecordLastEventTimestamp

func (NoopWatcherCollector) RecordLastEventTimestamp(time.Time)

type ObservedGenerationer

type ObservedGenerationer interface {
	ObservedGeneration() int64
	SetObservedGeneration(gen int64)
}

ObservedGenerationer is an optional interface for configs that track the last generation successfully applied by all consumers. The gap between Generation() and ObservedGeneration() indicates configuration drift (desired vs applied).

ref: kubernetes/kubernetes — generation/observedGeneration pattern: metadata.Generation = desired state version (bumped on spec change), status.ObservedGeneration = last version the controller reconciled.

type Reloader

type Reloader interface {
	Reload(yamlPath, envPrefix string) error
}

Reloader is an optional interface for configs that support hot-reloading. The concrete *config returned by Load implements this; NewFromMap does not.

type Snapshotter

type Snapshotter interface {
	Snapshot() map[string]any
}

Snapshotter is an optional interface for configs that support atomic point-in-time snapshots. The concrete *config returned by Load implements this. Snapshot holds the read lock for the entire copy, ensuring the returned map is a consistent view — unlike iterating Keys()+Get() which acquires/releases the lock per call.

type WatchEvent

type WatchEvent struct {
	Path         string // Path of the changed file (original, not resolved).
	SymlinkPivot bool   // True if triggered by a symlink target change.
}

WatchEvent carries information about a file change detected by the Watcher.

type Watcher

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

Watcher monitors a file for changes by watching its parent directory. This correctly handles atomic replace (rename+create) and remove+recreate, where file-level inotify/kqueue watches would silently break due to inode rebinding.

The watcher detects Kubernetes ConfigMap updates via ..data symlink pivot (see WithDebounce for coalescing rapid events). Note: symlink pivot is a detection mechanism, not a security boundary. The watcher does not validate that the resolved symlink target stays within a trusted directory — any process with write access to the watched directory can redirect config content. Deploy-time filesystem permissions are the trust boundary.

func NewWatcher

func NewWatcher(path string, clk clock.Clock, opts ...WatcherOption) (*Watcher, error)

NewWatcher creates a Watcher for the given file path. The watcher monitors the parent directory and filters events for the target filename. The watcher does not start until Start is called.

func (*Watcher) Close

func (w *Watcher) Close(ctx context.Context) error

Close stops the watcher and releases resources, bounded by ctx.

Phase 1: signals the internal done channel (stops the fsnotify loop and debounce goroutines), then waits for in-flight callbacks bounded by ctx. If ctx expires before all callbacks finish, Close returns ctx.Err() but still closes the underlying fsnotify.Watcher (OS file descriptor leak prevention takes priority over the caller's budget).

Phase 2: closes the underlying fsnotify.Watcher unconditionally.

Close is idempotent: concurrent and repeated calls are safe.

ref: uber-go/fx app.go StopTimeout — ctx as shared shutdown budget. ref: uber-go/fx lifecycle OnStop(ctx) — ContextCloser pattern.

func (*Watcher) Health

func (w *Watcher) Health() error

Health reports watcher readiness for bootstrap /readyz integration. A watcher is healthy only after its event loop has started and before it has been closed.

func (*Watcher) KeyFilters

func (w *Watcher) KeyFilters() []string

KeyFilters returns the key prefixes registered via WithKeyFilter (sorted).

func (*Watcher) OnChange

func (w *Watcher) OnChange(fn func(WatchEvent))

OnChange registers a callback that fires when the watched file changes.

func (*Watcher) Ready

func (w *Watcher) Ready() <-chan struct{}

Ready returns a channel that is closed when the event loop has started and is ready to process file-system events. Useful in tests to avoid time.Sleep.

func (*Watcher) Start

func (w *Watcher) Start()

Start begins watching for file changes in a goroutine. It blocks until Close is called or an unrecoverable error occurs.

func (*Watcher) StartWithContext

func (w *Watcher) StartWithContext(ctx context.Context)

StartWithContext begins watching using the provided context. When ctx is canceled, the watcher is closed automatically. This allows the watcher to be tied to a parent shutdown context.

type WatcherCollector

type WatcherCollector interface {
	// RecordEvent records a watcher event by type ("write", "create", "symlink_pivot").
	RecordEvent(eventType string)

	// RecordLastEventTimestamp records the time of the most recent event.
	RecordLastEventTimestamp(t time.Time)

	// RecordDebounceCoalesced increments the count of events absorbed by debounce.
	RecordDebounceCoalesced()
}

WatcherCollector records watcher operational metrics. Implementations must be safe for concurrent use.

ref: prometheus/prometheus cmd/prometheus/main.go — 4-metric baseline: reload total, success gauge, last timestamp, event count.

type WatcherOption

type WatcherOption func(*watcherConfig)

WatcherOption configures a Watcher. Pass to NewWatcher.

func WithDebounce

func WithDebounce(d time.Duration) WatcherOption

WithDebounce sets the debounce interval for coalescing rapid file events. Events are held for this duration; if another event arrives, the timer resets. Set to 0 to fire callbacks immediately (no debounce).

func WithDrainTimeout

func WithDrainTimeout(d time.Duration) WatcherOption

WithDrainTimeout sets how long Close waits for in-flight callbacks to finish. After this timeout, Close proceeds even if callbacks are still running.

func WithKeyFilter

func WithKeyFilter(prefixes ...string) WatcherOption

WithKeyFilter stores key prefixes on the watcher. Bootstrap can read these via KeyFilters() to decide which cells to notify after a config reload. The watcher itself does not apply key-level filtering (it watches files, not config keys). Calling this option multiple times replaces previous prefixes (last-writer-wins).

Note: bootstrap does not yet consume key filters in its reload fanout. This is a building block for selective cell notification (planned: #11 OPS-5).

func WithMaxDebounce

func WithMaxDebounce(d time.Duration) WatcherOption

WithMaxDebounce sets the maximum time events can be deferred. Even if events keep arriving, callbacks fire after this ceiling. Prevents infinite deferral. Only meaningful when debounce > 0.

func WithMetrics

func WithMetrics(m WatcherCollector) WatcherOption

WithMetrics injects a WatcherCollector for recording operational metrics. Bootstrap does not yet pass a collector; inject via a custom configWatcherFactory or wait for bootstrap option wiring (#11 OPS-5).

func WithSymlinkPollInterval

func WithSymlinkPollInterval(d time.Duration) WatcherOption

WithSymlinkPollInterval sets the polling interval used to detect symlink pivot changes. kqueue on macOS (fsnotify ≥ v1.9.0) does not reliably deliver Create/Remove events for symlinks inside a watched directory, so a periodic EvalSymlinks poll is used as a fallback. Set to 0 to disable polling.

Jump to

Keyboard shortcuts

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