Observability¶
Confii provides built-in observability through two systems: metrics collection for tracking access patterns and reload statistics, and event emission for reacting to configuration changes in real-time.
For trace and request correlation, OnChangeWithContext and
EventSubscriber.OnWithContext receive the originating operation context. See
Context, cancellation, and operation lifecycles.
Metrics Collection¶
Enabling Metrics¶
EnableObservability returns a confii.MetricsReader view. Once enabled,
Confii automatically records successful Get/GetWithContext accesses,
reloads, extensions, mutations, failures, and change events. Failed key lookups
are not counted as accesses. The view exposes detached statistics without
granting mutation access to Confii's collector.
Reading Statistics¶
stats := cfg.GetMetrics()
if stats == nil {
log.Println("observability not enabled")
return
}
fmt.Println(stats)
The returned map contains:
| Key | Type | Description |
|---|---|---|
total_keys |
int |
Total number of configuration keys |
accessed_keys |
int |
Number of distinct keys that have been accessed |
access_rate |
float64 |
Ratio of accessed keys to total keys (0.0 to 1.0) |
top_accessed_keys |
map[string]int |
Top 10 most accessed keys with their counts |
reload_count |
int |
Total number of reloads |
avg_reload_time |
string |
Average reload duration |
change_count |
int |
Total number of change events |
last_reload |
time.Time |
Timestamp of the last reload (if any) |
last_change |
time.Time |
Timestamp of the last change (if any) |
Example output:
map[string]any{
"total_keys": 15,
"accessed_keys": 8,
"access_rate": 0.533,
"top_accessed_keys": map[string]int{
"database.host": 42,
"database.port": 38,
"app.name": 25,
},
"reload_count": 3,
"avg_reload_time": "12.5ms",
"change_count": 2,
"last_reload": time.Time{...},
}
Metrics Control¶
metrics := cfg.EnableObservability() // starts or resumes collection
cfg.DisableObservability() // pause collection; retain existing data
cfg.ResetMetrics() // clear retained data
cfg.EnableObservability() // resume collection
Event Emission¶
Enabling Events¶
EnableEvents returns a confii.EventSubscriber. It can register and remove
listeners but cannot emit fabricated Config lifecycle events.
Event Types¶
| Event | When it fires | Arguments |
|---|---|---|
reload |
After a successful reload | config map[string]any, duration time.Duration |
extend |
After a source is successfully added | config map[string]any, duration time.Duration |
set |
After Set publishes |
key string, value any |
override |
After an override is applied | overrides map[string]any |
override_restored |
After an override frame is restored | config map[string]any |
secrets_refreshed |
After refreshed secrets are published | nil, duration time.Duration |
rollback |
After a version is restored | versionID string, config map[string]any |
change |
After any successful snapshot mutation | oldConfig map[string]any, newConfig map[string]any |
Operation-specific events are delivered before the generic change event.
The maps supplied to change are detached snapshots and cannot mutate the live
Config. Superseded optimistic transaction attempts emit nothing; one committed
operation produces one operation event and one change event.
On / Off Pattern¶
Register a listener:
emitter.On("reload", func(args ...any) {
config := args[0].(map[string]any)
duration := args[1].(time.Duration)
log.Printf("Config reloaded in %v with %d keys", duration, len(config))
})
emitter.On("change", func(args ...any) {
oldConfig := args[0].(map[string]any)
newConfig := args[1].(map[string]any)
log.Printf("Config changed: %d keys before, %d keys after",
len(oldConfig), len(newConfig))
})
Remove the last registered listener:
For independent application events, construct a separate
observe.NewEventEmitter; it is intentionally not the Config-owned lifecycle
emitter.
Chaining
On returns the emitter and supports chained registrations:
Panic safety
Listener panics are caught, logged, and do not propagate. A panic in one listener does not prevent other listeners from running.
Integration Patterns¶
Logging¶
emitter := cfg.EnableEvents()
emitter.On("reload", func(args ...any) {
duration := args[1].(time.Duration)
slog.Info("config reloaded", slog.Duration("duration", duration))
})
emitter.On("change", func(args ...any) {
slog.Info("config changed")
})
Prometheus Metrics¶
var (
configReloads = promauto.NewCounter(prometheus.CounterOpts{
Name: "config_reloads_total",
Help: "Total number of configuration reloads",
})
configReloadDuration = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "config_reload_duration_seconds",
Help: "Duration of configuration reloads",
Buckets: prometheus.DefBuckets,
})
configChanges = promauto.NewCounter(prometheus.CounterOpts{
Name: "config_changes_total",
Help: "Total number of configuration changes",
})
)
emitter := cfg.EnableEvents()
emitter.On("reload", func(args ...any) {
duration := args[1].(time.Duration)
configReloads.Inc()
configReloadDuration.Observe(duration.Seconds())
})
emitter.On("change", func(args ...any) {
configChanges.Inc()
})
Periodic Statistics Reporting¶
cfg.EnableObservability()
go func() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for range ticker.C {
stats := cfg.GetMetrics()
if stats != nil {
slog.Info("config stats",
slog.Int("accessed_keys", stats["accessed_keys"].(int)),
slog.Int("reload_count", stats["reload_count"].(int)),
slog.Float64("access_rate", stats["access_rate"].(float64)),
)
}
}
}()
Combined Observability Setup¶
func setupObservability(cfg *confii.Config[any]) {
// Enable metrics
cfg.EnableObservability()
// Enable events
emitter := cfg.EnableEvents()
// Log all events
emitter.On("reload", func(args ...any) {
duration := args[1].(time.Duration)
log.Printf("[config] reloaded in %v", duration)
})
emitter.On("change", func(args ...any) {
log.Println("[config] values changed")
})
// Register change callbacks for specific keys
cfg.OnChange(func(key string, oldVal, newVal any) {
log.Printf("[config] %s: %v -> %v", key, oldVal, newVal)
})
}
Full Example¶
package main
import (
"context"
"fmt"
"log"
"time"
confii "github.com/confiify/confii-go/v2"
"github.com/confiify/confii-go/v2/loader"
)
func main() {
ctx := context.Background()
cfg, err := confii.NewWithContext[any](ctx,
confii.WithLoaders(loader.NewYAML("config.yaml")),
confii.WithEnv("production"),
)
if err != nil {
log.Fatal(err)
}
// Enable metrics
metrics := cfg.EnableObservability()
// Enable events
emitter := cfg.EnableEvents()
emitter.On("reload", func(args ...any) {
duration := args[1].(time.Duration)
fmt.Printf("Reloaded in %v\n", duration)
})
emitter.On("change", func(args ...any) {
fmt.Println("Config values changed")
})
// Successful Config reads are tracked automatically
_, _ = cfg.Get("database.host")
_, _ = cfg.Get("database.host")
_, _ = cfg.Get("database.port")
// Trigger a reload
_ = cfg.ReloadWithContext(ctx)
// Print statistics
stats := cfg.GetMetrics()
fmt.Printf("\nStatistics:\n")
fmt.Printf(" Total keys: %v\n", stats["total_keys"])
fmt.Printf(" Accessed keys: %v\n", stats["accessed_keys"])
fmt.Printf(" Access rate: %.1f%%\n", stats["access_rate"].(float64)*100)
fmt.Printf(" Reload count: %v\n", stats["reload_count"])
fmt.Printf(" Change count: %v\n", stats["change_count"])
fmt.Printf(" Top accessed: %v\n", stats["top_accessed_keys"])
}