Watching

Monitor configuration sources for changes with hot reload, debouncing, and change callbacks.

Confy supports watching configuration sources for changes and automatically reloading values. File sources use fsnotify for filesystem events, while other sources poll at configurable intervals.

Quick Start

// Enable watching on the file source
source, _ := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
    WatchEnabled: true,
})
cfg.LoadFrom(source)

// Register a change callback
cfg.WatchChanges(func(change confy.ConfigChange) {
    log.Printf("config changed: %s = %v (was %v)",
        change.Key, change.NewValue, change.OldValue)
})

// Start watching (blocks until context is cancelled)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cfg.Watch(ctx)

Change Callbacks

Watch All Changes

WatchChanges receives every configuration change from any source:

cfg.WatchChanges(func(change confy.ConfigChange) {
    switch change.Type {
    case confy.ChangeTypeSet:
        log.Printf("new key: %s = %v", change.Key, change.NewValue)
    case confy.ChangeTypeUpdate:
        log.Printf("updated: %s = %v%v", change.Key, change.OldValue, change.NewValue)
    case confy.ChangeTypeDelete:
        log.Printf("deleted: %s (was %v)", change.Key, change.OldValue)
    case confy.ChangeTypeReload:
        log.Printf("full reload triggered")
    }
})

Watch Specific Keys

WatchWithCallback monitors a specific key:

cfg.WatchWithCallback("log.level", func(key string, value any) {
    newLevel := value.(string)
    logger.SetLevel(newLevel)
    log.Printf("Log level changed to: %s", newLevel)
})

cfg.WatchWithCallback("database.host", func(key string, value any) {
    newHost := value.(string)
    reconnectDatabase(newHost)
})

Change Types

TypeConstantDescription
SetChangeTypeSetNew key added
UpdateChangeTypeUpdateExisting key changed
DeleteChangeTypeDeleteKey removed
ReloadChangeTypeReloadFull configuration reloaded

ConfigChange Structure

type ConfigChange struct {
    Key      string     // dot-separated key path
    OldValue any        // previous value (nil for new keys)
    NewValue any        // new value (nil for deleted keys)
    Type     ChangeType // set, update, delete, or reload
}

Watcher Configuration

For advanced control, create a standalone watcher:

watcher := confy.NewWatcher(confy.WatcherConfig{
    Interval:       5 * time.Second,   // polling interval
    BufferSize:     100,               // event buffer size
    MaxRetries:     3,                 // retry attempts on error
    RetryInterval:  5 * time.Second,   // delay between retries
    EnableDebounce: true,              // debounce rapid changes
    DebounceTime:   500 * time.Millisecond,
})

Watcher Configuration Options

OptionTypeDefaultDescription
Intervaltime.Duration5sPolling interval
BufferSizeint100Event buffer size
MaxRetriesint3Retry limit for errors
RetryIntervaltime.Duration5sDelay between retries
EnableDebounceboolfalseDebounce rapid changes
DebounceTimetime.Duration500msDebounce window

Watch Events

The standalone watcher emits WatchEvent structs with metadata:

type WatchEvent struct {
    SourceName string
    EventType  WatchEventType
    Data       map[string]any
    Timestamp  time.Time
    Checksum   string
    Error      error
}

Event Types

TypeDescription
WatchEventTypeChangeSource data changed
WatchEventTypeCreateSource created
WatchEventTypeDeleteSource deleted
WatchEventTypeErrorError occurred
WatchEventTypeReloadFull reload

Standalone Watcher

Use the standalone watcher for direct source management:

watcher := confy.NewWatcher(confy.WatcherConfig{
    Interval: 10 * time.Second,
})

// Watch a specific source
err := watcher.WatchSource(ctx, fileSource, func(name string, data map[string]any) {
    log.Printf("Source %s changed", name)
    // Handle new data
})

// Get watched source names
sources := watcher.GetWatchedSources()

// Get statistics
stats := watcher.GetAllStats()
for name, stat := range stats {
    log.Printf("Source %s: %d changes, last at %v", name, stat.ChangeCount, stat.LastChange)
}

// Stop watching a specific source
watcher.StopWatching("config.yaml")

// Stop all watches
watcher.StopAll()

Automatic Reload

Enable automatic reload when any source changes:

cfg := confy.New(
    confy.WithReloadOnChange(true),
    confy.WithWatchInterval(10 * time.Second),
)

With ReloadOnChange enabled, the configuration is automatically merged when any source emits a change event. You can still register WatchChanges callbacks to react to changes.

Dynamic Reconfiguration Pattern

A common pattern for hot-reloading production applications:

func main() {
    cfg := confy.New(
        confy.WithReloadOnChange(true),
    )

    source, _ := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
        WatchEnabled: true,
    })
    cfg.LoadFrom(source)

    // React to specific changes
    cfg.WatchWithCallback("log.level", func(key string, value any) {
        setLogLevel(value.(string))
    })

    cfg.WatchWithCallback("rate_limit.rps", func(key string, value any) {
        updateRateLimiter(cfg.GetInt("rate_limit.rps", 100))
    })

    // Start watching in background
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go cfg.Watch(ctx)

    // Application runs with hot-reloadable config
    startServer(cfg)
}

When using ReloadOnChange, ensure your application code is safe for concurrent config reads. All confy getter methods are thread-safe.

How is this guide?

On this page