config

package module
v0.4.3 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2026 License: MIT Imports: 16 Imported by: 0

README

Config

CI Go Reference Go Report Card Release OpenSSF Scorecard

A type-safe, namespace-aware configuration library for Go with support for multiple storage backends, built-in resilience, and OpenTelemetry instrumentation.

Features

  • Multiple Storage Backends: Memory, PostgreSQL, MongoDB
  • Multi-Store: Combine stores for caching, fallback, or replication patterns
  • Namespace Isolation: Organize configuration by environment, tenant, or service
  • Built-in Resilience: Internal cache ensures app works during backend outages
  • Type-safe Values: Strongly typed access with automatic conversion
  • Codecs: JSON, YAML, TOML encoding support
  • OpenTelemetry: Tracing and metrics instrumentation
  • Struct Binding: Bind configuration to Go structs with validation
  • Live Binding: Auto-reload structs on config changes via polling

Design Philosophy

This library is designed for use cases like feature flags, rate limits, and dynamic configuration where eventual consistency is acceptable. The key principle is: having some configuration (even slightly stale) is better than having no configuration at all.

The library maintains an internal in-memory cache that serves as a resilience layer:

  • If the backend store becomes temporarily unavailable, cached values continue to be served
  • Each application instance maintains its own local cache
  • Cache is automatically invalidated via the store's native change stream (MongoDB Change Streams, PostgreSQL LISTEN/NOTIFY)
  • No external dependencies like Redis required for caching

Installation

go get github.com/rbaliyan/config

Quick Start

package main

import (
    "context"
    "log"

    "github.com/rbaliyan/config"
    "github.com/rbaliyan/config/memory"
)

func main() {
    ctx := context.Background()

    // Create manager with memory store
    mgr := config.New(
        config.WithStore(memory.NewStore()),
    )

    // Connect to backend
    if err := mgr.Connect(ctx); err != nil {
        log.Fatal(err)
    }
    defer mgr.Close(ctx)

    // Get configuration for a namespace (use "" for default)
    cfg := mgr.Namespace("production")

    // Set a value
    if err := cfg.Set(ctx, "app/timeout", 30); err != nil {
        log.Fatal(err)
    }

    // Get a value
    val, err := cfg.Get(ctx, "app/timeout")
    if err != nil {
        log.Fatal(err)
    }

    // Unmarshal into a typed variable
    var timeout int
    if err := val.Unmarshal(&timeout); err != nil {
        log.Fatal(err)
    }

    log.Printf("Timeout: %d", timeout)
}

Storage Backends

Memory Store

In-memory storage for testing and single-instance deployments.

import "github.com/rbaliyan/config/memory"

store := memory.NewStore()
PostgreSQL Store

Persistent storage with LISTEN/NOTIFY for real-time updates.

import (
    "database/sql"
    "github.com/lib/pq"
    "github.com/rbaliyan/config/postgres"
)

db, _ := sql.Open("postgres", "postgres://localhost/mydb")
listener := pq.NewListener(dsn, 10*time.Second, time.Minute, nil)

store := postgres.NewStore(db, listener,
    postgres.WithTable("config_entries"),
    postgres.WithNotifyChannel("config_changes"),
)
MongoDB Store

Persistent storage with change streams for real-time updates.

import (
    "go.mongodb.org/mongo-driver/v2/mongo"
    "go.mongodb.org/mongo-driver/v2/mongo/options"
    "github.com/rbaliyan/config/mongodb"
)

client, _ := mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017"))

store := mongodb.NewStore(client,
    mongodb.WithDatabase("config"),
    mongodb.WithCollection("entries"),
)

Working with Values

Reading Values
val, err := cfg.Get(ctx, "database/port")
if err != nil {
    if config.IsNotFound(err) {
        // Key doesn't exist
    }
    return err
}

// Type-safe access via Value interface
port, err := val.Int64()
str, err := val.String()
flag, err := val.Bool()
num, err := val.Float64()

// Unmarshal into any type
var dbConfig DatabaseConfig
if err := val.Unmarshal(&dbConfig); err != nil {
    return err
}

// Access metadata
meta := val.Metadata()
version := meta.Version()
created := meta.CreatedAt()
updated := meta.UpdatedAt()
Writing Values
// Simple values
cfg.Set(ctx, "app/timeout", 30)
cfg.Set(ctx, "app/name", "myservice")
cfg.Set(ctx, "app/enabled", true)

// Complex values
cfg.Set(ctx, "app/servers", []string{"host1", "host2"})
cfg.Set(ctx, "app/limits", map[string]int{"max": 100, "min": 1})

// With options
cfg.Set(ctx, "app/config", value,
    config.WithType(config.TypeCustom),
    config.WithSetCodec(yamlCodec),
)
Conditional Writes

Control create/update behavior with conditional write options:

// Create only - fails if key already exists
err := cfg.Set(ctx, "feature/flag", true, config.WithIfNotExists())
if config.IsKeyExists(err) {
    // Key already existed, value not changed
}

// Update only - fails if key doesn't exist
err = cfg.Set(ctx, "feature/flag", false, config.WithIfExists())
if config.IsNotFound(err) {
    // Key didn't exist, nothing updated
}

// Default (upsert) - creates or updates
cfg.Set(ctx, "feature/flag", true) // Always succeeds

These options leverage atomic database operations:

  • PostgreSQL: Uses ON CONFLICT DO NOTHING / UPDATE with row count checks
  • MongoDB: Uses InsertOne / FindOneAndUpdate with upsert control
Listing Values
// List all keys with prefix using the Filter builder
limit := 100
page, err := cfg.Find(ctx, config.NewFilter().
    WithPrefix("app/database").
    WithLimit(limit).
    Build())

for key, val := range page.Results() {
    str, _ := val.String()
    fmt.Printf("%s = %s\n", key, str)
}

// Pagination: check if len(results) < limit to determine if more pages exist
if len(page.Results()) == page.Limit() {
    nextPage, _ := cfg.Find(ctx, config.NewFilter().
        WithPrefix("app/database").
        WithLimit(limit).
        WithCursor(page.NextCursor()).
        Build())
    // Process nextPage...
}

Namespaces

Namespaces provide isolation between different environments or tenants.

// Get configs for different namespaces
prodCfg := mgr.Namespace("production")
devCfg := mgr.Namespace("development")

// Same key, different values per namespace
prodCfg.Set(ctx, "timeout", 60)
devCfg.Set(ctx, "timeout", 5)

// Use "" for the default namespace
defaultCfg := mgr.Namespace("")

Context Helpers

Access configuration from context without explicit dependency injection.

// Add manager to context
ctx = config.ContextWithManager(ctx, mgr)
ctx = config.ContextWithNamespace(ctx, "production")

// Use anywhere in your application
val, err := config.Get(ctx, "app/setting")
err = config.Set(ctx, "app/setting", "value")

Multi-Store (Fallback Pattern)

Combine multiple stores for caching, fallback, or replication:

import "github.com/rbaliyan/config/multi"

// Cache + Backend pattern
cacheStore := memory.NewStore()
backendStore := postgres.NewStore(db, listener)
store := multi.NewStoreWithOptions(
    []config.Store{cacheStore, backendStore},
    []multi.Option{multi.WithStrategy(multi.StrategyReadThrough)},
)

// Primary + Backup pattern
primaryStore := postgres.NewStore(primaryDB, primaryListener)
backupStore := postgres.NewStore(backupDB, backupListener)
store := multi.NewStoreWithOptions(
    []config.Store{primaryStore, backupStore},
    []multi.Option{multi.WithStrategy(multi.StrategyFallback)},
)

mgr := config.New(config.WithStore(store))

Strategies:

  • StrategyFallback: Read from first available store, write to all stores
  • StrategyReadThrough: Read through stores (cache miss populates earlier stores), write to all stores
  • StrategyWriteThrough: Write to all stores, read from first available store

All strategies write to all stores to maintain consistency. The difference is in read behavior:

  • Fallback/WriteThrough: Return first successful read
  • ReadThrough: Try each store in order, populate earlier stores on cache miss
Multi-Store Diagnostics

Multi-Store supports health checks and statistics when the underlying stores implement the optional interfaces:

// Health check (if underlying stores implement HealthChecker)
if err := store.Health(ctx); err != nil {
    log.Printf("Store unhealthy: %v", err)
}

// Statistics (if underlying stores implement StatsProvider)
stats, err := store.Stats(ctx)
if err == nil {
    log.Printf("Total entries: %d", stats.TotalEntries)
}

OpenTelemetry Instrumentation

Wrap stores with tracing and metrics. Both are disabled by default and must be explicitly enabled.

import "github.com/rbaliyan/config/otel"

// Enable tracing and metrics explicitly
instrumentedStore, _ := otel.WrapStore(store,
    otel.WithServiceName("my-service"),
    otel.WithBackendName("postgres"),
    otel.WithTracesEnabled(true),   // Opt-in, disabled by default
    otel.WithMetricsEnabled(true),  // Opt-in, disabled by default
)

mgr := config.New(config.WithStore(instrumentedStore))

Metrics exported:

  • config.operations.total - Counter of all operations
  • config.errors.total - Counter of errors by type
  • config.operation.duration - Histogram of operation latency

Struct Binding

Bind configuration to Go structs with validation. Struct fields are automatically mapped to hierarchical keys using the configured struct tag (default: json).

import "github.com/rbaliyan/config/bind"

type DatabaseConfig struct {
    Host string `json:"host" validate:"required"`
    Port int    `json:"port" validate:"required,min=1,max=65535"`
}

binder := bind.New(cfg, bind.WithTagValidation())
bound := binder.Bind()

// Store a struct - creates keys: database/host, database/port
err := bound.SetStruct(ctx, "database", DatabaseConfig{Host: "localhost", Port: 5432})

// Retrieve a struct - reads keys: database/host, database/port
var dbConfig DatabaseConfig
err = bound.GetStruct(ctx, "database", &dbConfig)

Nested structs are also supported:

type AppConfig struct {
    Name  string      `json:"name"`
    Cache CacheConfig `json:"cache"`
}

type CacheConfig struct {
    TTL     int  `json:"ttl"`
    Enabled bool `json:"enabled"`
}

// SetStruct creates: app/name, app/cache/ttl, app/cache/enabled
err := bound.SetStruct(ctx, "app", AppConfig{
    Name: "myapp",
    Cache: CacheConfig{TTL: 300, Enabled: true},
})

Use nonrecursive to store a nested struct as a single JSON value instead of flattening:

type Credentials struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type AppConfig struct {
    Name  string      `json:"name"`
    Creds Credentials `json:"creds,nonrecursive"` // Store as single JSON value
}

// SetStruct creates: app/name, app/creds (not app/creds/username, app/creds/password)
// Useful when fields are tightly coupled and should be updated atomically

Live Config (Auto-Reload)

Keep a typed struct automatically synchronized with configuration using polling and atomic swap:

import "github.com/rbaliyan/config/live"

type DatabaseConfig struct {
    Host string `json:"host"`
    Port int    `json:"port"`
}

ref, err := live.New[DatabaseConfig](ctx, cfg, "database",
    live.PollInterval(10*time.Second),
    live.OnChange(func(old, new DatabaseConfig) {
        log.Printf("config changed: %s -> %s", old.Host, new.Host)
    }),
    live.OnError(func(err error) {
        log.Printf("reload error: %v", err)
    }),
)
if err != nil {
    return err
}
defer ref.Close()

// Hot path: single atomic load, zero contention
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    snap := ref.Load()
    fmt.Fprintf(w, "Host: %s, Port: %d", snap.Host, snap.Port)
})

Options:

  • PollInterval(d) - Set polling interval (default: 30s)
  • OnChange(fn) - Callback with old and new values on change
  • OnError(fn) - Callback on reload error

Methods:

  • Load() - Get current snapshot (atomic, zero-cost)
  • Close() - Stop background polling
  • ReloadNow(ctx) - Force immediate reload
  • LastReload() - Get last reload timestamp
  • LastError() - Get last error (nil if successful)
  • ReloadCount() - Get total successful reload count

Codecs

Multiple encoding formats are supported.

import "github.com/rbaliyan/config/codec"

// Available codecs
jsonCodec := codec.Get("json")
yamlCodec := codec.Get("yaml")
tomlCodec := codec.Get("toml")

// Use with manager
mgr := config.New(
    config.WithStore(store),
    config.WithCodec(yamlCodec),
)

Configuration Types

The library tracks value types for better type safety.

const (
    TypeInt             // int, int64
    TypeFloat           // float64
    TypeString          // string
    TypeBool            // bool
    TypeMapStringInt    // map[string]int
    TypeMapStringFloat  // map[string]float64
    TypeMapStringString // map[string]string
    TypeListInt         // []int
    TypeListFloat       // []float64
    TypeListString      // []string
    TypeCustom          // any other type
)

Error Handling

val, err := cfg.Get(ctx, "key")
if err != nil {
    switch {
    case config.IsNotFound(err):
        // Key doesn't exist
    case config.IsTypeMismatch(err):
        // Type conversion failed
    case config.IsKeyExists(err):
        // Key already exists (from WithIfNotExists)
    default:
        // Other error
    }
}

Manager Options

mgr := config.New(
    config.WithStore(store),           // Required: storage backend
    config.WithCodec(yamlCodec),       // Optional: default codec (default: JSON)
    config.WithLogger(slogLogger),     // Optional: custom logger
)

License

MIT License

Documentation

Index

Examples

Constants

View Source
const DefaultNamespace = ""

DefaultNamespace is the default namespace (empty string). Use this when you don't need namespace separation.

Variables

View Source
var (
	// ErrNotFound is returned when a config key does not exist.
	ErrNotFound = errors.New("config: key not found")

	// ErrTypeMismatch is returned when attempting to convert a value to an incompatible type.
	ErrTypeMismatch = errors.New("config: type mismatch")

	// ErrInvalidKey is returned when a config key is empty or malformed.
	ErrInvalidKey = errors.New("config: invalid key")

	// ErrInvalidNamespace is returned when a namespace is malformed.
	ErrInvalidNamespace = errors.New("config: invalid namespace")

	// ErrInvalidValue is returned when a value cannot be stored.
	ErrInvalidValue = errors.New("config: invalid value")

	// ErrStoreNotConnected is returned when operating on a disconnected store.
	ErrStoreNotConnected = errors.New("config: store not connected")

	// ErrStoreClosed is returned when operating on a closed store.
	ErrStoreClosed = errors.New("config: store closed")

	// ErrWatchNotSupported is returned when the store does not support watching.
	ErrWatchNotSupported = errors.New("config: watch not supported")

	// ErrManagerClosed is returned when operating on a closed manager.
	ErrManagerClosed = errors.New("config: manager closed")

	// ErrCodecNotFound is returned when a codec is not registered.
	ErrCodecNotFound = errors.New("config: codec not found")

	// ErrReadOnly is returned when attempting to write to a read-only store.
	ErrReadOnly = errors.New("config: store is read-only")

	// ErrKeyExists is returned when attempting to create a key that already exists.
	ErrKeyExists = errors.New("config: key already exists")

	// ErrUnsupportedCodec is returned when a store does not support the requested codec.
	ErrUnsupportedCodec = errors.New("config: unsupported codec")
)

Sentinel errors for config operations. Use errors.Is() to check for these errors as they may be wrapped.

View Source
var ErrBulkWritePartial = errors.New("config: bulk write partially failed")

ErrBulkWritePartial is returned when a bulk write operation partially fails.

View Source
var ErrWatchUnhealthy = errors.New("config: watch unhealthy")

ErrWatchUnhealthy is returned by Health() when watch has consecutive failures.

Functions

func ContextNamespace

func ContextNamespace(ctx context.Context) string

ContextNamespace retrieves the namespace from context. Returns empty string if no namespace is set.

func ContextWithManager

func ContextWithManager(ctx context.Context, mgr Manager) context.Context

ContextWithManager adds a Manager to the context. This allows handlers to access configuration without explicit dependency injection.

Example
package main

import (
	"context"
	"fmt"

	"github.com/rbaliyan/config"
	"github.com/rbaliyan/config/memory"
)

func main() {
	ctx := context.Background()

	mgr, _ := config.New(config.WithStore(memory.NewStore()))
	_ = mgr.Connect(ctx)
	defer mgr.Close(ctx)

	// Add manager and namespace to context
	ctx = config.ContextWithManager(ctx, mgr)
	ctx = config.ContextWithNamespace(ctx, "myapp")

	// Set and get via context convenience functions
	_ = config.Set(ctx, "greeting", "hello")

	val, _ := config.Get(ctx, "greeting")
	s, _ := val.String()
	fmt.Println(s)
}
Output:

hello

func ContextWithNamespace

func ContextWithNamespace(ctx context.Context, namespace string) context.Context

ContextWithNamespace adds a namespace to the context. Operations will use this namespace.

func IsBulkWritePartial

func IsBulkWritePartial(err error) bool

IsBulkWritePartial checks if an error indicates a partial bulk write failure. Use errors.As() to get the BulkWriteError for details about which keys failed.

func IsInvalidKey

func IsInvalidKey(err error) bool

IsInvalidKey checks if an error indicates an invalid key.

func IsKeyExists

func IsKeyExists(err error) bool

IsKeyExists checks if an error indicates a key already exists.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound checks if an error indicates a missing key.

func IsTypeMismatch

func IsTypeMismatch(err error) bool

IsTypeMismatch checks if an error indicates a type mismatch.

func IsUnsupportedCodec added in v0.4.2

func IsUnsupportedCodec(err error) bool

IsUnsupportedCodec checks if an error indicates an unsupported codec.

func IsWatchUnhealthy

func IsWatchUnhealthy(err error) bool

IsWatchUnhealthy checks if an error indicates watch connection issues.

func Set

func Set(ctx context.Context, key string, value any, opts ...SetOption) error

Set is a convenience function that sets a value using the Manager from context. Returns ErrManagerClosed if no manager is in context. Uses the namespace from context, or "" (default) if not set.

func ValidateKey

func ValidateKey(key string) error

ValidateKey validates a configuration key. Keys must:

  • Not be empty
  • Contain only alphanumeric characters, underscores, dashes, dots, and slashes
  • Not contain path traversal sequences (..)
  • Not start or end with a slash

Returns an InvalidKeyError if the key is invalid.

func ValidateNamespace

func ValidateNamespace(namespace string) error

ValidateNamespace validates a namespace name. Empty namespaces are allowed (represents the default namespace). Returns ErrInvalidNamespace if the namespace contains invalid characters.

func WrapStoreError

func WrapStoreError(op, backend, key string, err error) error

WrapStoreError creates a StoreError from a backend error.

Types

type BulkStore

type BulkStore interface {
	// GetMany retrieves multiple values in a single operation.
	// Returns a map of key -> Value. Missing keys are not included in the result.
	GetMany(ctx context.Context, namespace string, keys []string) (map[string]Value, error)

	// SetMany creates or updates multiple values in a single operation.
	SetMany(ctx context.Context, namespace string, values map[string]Value) error

	// DeleteMany removes multiple values in a single operation.
	// Returns the number of entries actually deleted.
	DeleteMany(ctx context.Context, namespace string, keys []string) (int64, error)
}

BulkStore is an optional interface for stores that support batch operations. Implementing this interface allows efficient bulk reads and writes.

type BulkWriteError

type BulkWriteError struct {
	// Errors maps keys to their specific errors. Only failed keys are included.
	Errors map[string]error
	// Succeeded contains keys that were written successfully.
	Succeeded []string
}

BulkWriteError provides details about partial failures in bulk operations. Use KeyErrors() to see which keys failed and which succeeded.

func (*BulkWriteError) Error

func (e *BulkWriteError) Error() string

func (*BulkWriteError) FailedKeys

func (e *BulkWriteError) FailedKeys() []string

FailedKeys returns the list of keys that failed.

func (*BulkWriteError) KeyErrors

func (e *BulkWriteError) KeyErrors() map[string]error

KeyErrors returns the map of key -> error for failed keys.

func (*BulkWriteError) Unwrap

func (e *BulkWriteError) Unwrap() error

type CacheStats

type CacheStats struct {
	// Hits is the number of successful cache lookups.
	Hits int64 `json:"hits"`

	// Misses is the number of cache lookups that found no entry.
	Misses int64 `json:"misses"`

	// Size is the current number of entries in the cache.
	Size int64 `json:"size"`

	// Capacity is the maximum number of entries (0 = unbounded).
	Capacity int64 `json:"capacity"`

	// Evictions is the number of entries evicted due to capacity limits.
	Evictions int64 `json:"evictions"`
}

CacheStats contains cache statistics.

func (*CacheStats) HitRate

func (s *CacheStats) HitRate() float64

HitRate returns the cache hit rate as a percentage (0.0 to 1.0). Returns 0 if there have been no lookups.

type ChangeEvent

type ChangeEvent struct {
	// Type indicates the kind of change.
	Type ChangeType

	// Namespace is the namespace of the changed key.
	Namespace string

	// Key is the key that changed.
	Key string

	// Value is the new value (nil for Delete events).
	Value Value

	// Timestamp is when the change occurred.
	Timestamp time.Time
}

ChangeEvent represents a configuration change notification.

type ChangeType

type ChangeType int

ChangeType represents the type of configuration change.

const (
	// ChangeTypeSet indicates a create or update operation.
	ChangeTypeSet ChangeType = iota

	// ChangeTypeDelete indicates a delete operation.
	ChangeTypeDelete
)

func (ChangeType) String

func (c ChangeType) String() string

String returns the string representation of the change type.

type CodecValidator added in v0.4.2

type CodecValidator interface {
	SupportsCodec(codecName string) bool
}

CodecValidator is an optional interface for stores that restrict supported codecs. Stores that do not implement this accept all codecs (backward compatible).

type Config

type Config interface {
	Reader
	Writer

	// Namespace returns the namespace name.
	Namespace() string
}

Config combines Reader and Writer for a specific namespace.

type Filter

type Filter interface {
	// Keys returns specific keys to retrieve (mutually exclusive with Prefix).
	Keys() []string

	// Prefix returns the prefix to match (mutually exclusive with Keys).
	Prefix() string

	// Limit returns the maximum number of results (0 = no limit).
	Limit() int

	// Cursor returns the pagination cursor (entry ID) for continuing from a previous result.
	Cursor() string
}

Filter defines criteria for listing configuration entries. Use NewFilter() to create a FilterBuilder and construct filters.

Filters support two mutually exclusive modes:

  • Keys mode: retrieve specific keys by exact match
  • Prefix mode: retrieve all keys matching a prefix

Example:

// Get specific keys
filter := config.NewFilter().WithKeys("db/host", "db/port").Build()

// Get all keys with prefix
filter := config.NewFilter().WithPrefix("db/").WithLimit(100).Build()

// Paginate with cursor
page, _ := cfg.Find(ctx, config.NewFilter().WithPrefix("").WithLimit(50).Build())
nextPage, _ := cfg.Find(ctx, config.NewFilter().WithPrefix("").WithLimit(50).WithCursor(page.NextCursor()).Build())

type FilterBuilder

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

FilterBuilder builds Filter instances using a fluent API.

func NewFilter

func NewFilter() *FilterBuilder

NewFilter creates a new FilterBuilder.

func (*FilterBuilder) Build

func (b *FilterBuilder) Build() Filter

Build creates the Filter.

func (*FilterBuilder) WithCursor

func (b *FilterBuilder) WithCursor(cursor string) *FilterBuilder

WithCursor sets the pagination cursor for continuing from a previous result.

func (*FilterBuilder) WithKeys

func (b *FilterBuilder) WithKeys(keys ...string) *FilterBuilder

WithKeys sets specific keys to retrieve. Cannot be used with WithPrefix - calling this clears any prefix.

func (*FilterBuilder) WithLimit

func (b *FilterBuilder) WithLimit(limit int) *FilterBuilder

WithLimit sets the maximum number of results.

func (*FilterBuilder) WithPrefix

func (b *FilterBuilder) WithPrefix(prefix string) *FilterBuilder

WithPrefix sets a prefix to match keys. Cannot be used with WithKeys - calling this clears any keys.

type HealthChecker

type HealthChecker interface {
	// Health performs a health check on the store.
	// Returns nil if healthy, or an error describing the issue.
	Health(ctx context.Context) error
}

HealthChecker is an optional interface for stores that support health checks.

type InvalidKeyError

type InvalidKeyError struct {
	Key    string
	Reason string
}

InvalidKeyError provides details about an invalid key.

func (*InvalidKeyError) Error

func (e *InvalidKeyError) Error() string

func (*InvalidKeyError) Unwrap

func (e *InvalidKeyError) Unwrap() error

type KeyExistsError

type KeyExistsError struct {
	Key       string
	Namespace string
}

KeyExistsError provides details about a key that already exists.

func (*KeyExistsError) Error

func (e *KeyExistsError) Error() string

func (*KeyExistsError) Unwrap

func (e *KeyExistsError) Unwrap() error

type KeyNotFoundError

type KeyNotFoundError struct {
	Key       string
	Namespace string
}

KeyNotFoundError provides details about a missing key.

func (*KeyNotFoundError) Error

func (e *KeyNotFoundError) Error() string

func (*KeyNotFoundError) Unwrap

func (e *KeyNotFoundError) Unwrap() error

type Manager

type Manager interface {
	// Connect establishes connection to the backend and starts watching.
	// Must be called before any other operations.
	Connect(ctx context.Context) error

	// Close stops watching and releases resources.
	Close(ctx context.Context) error

	// Namespace returns a Config for the specified namespace.
	// Use "" for the default namespace.
	Namespace(name string) Config

	// Refresh forces a cache refresh for a specific key.
	Refresh(ctx context.Context, namespace, key string) error

	// Health performs a health check on the manager and underlying store.
	// Returns nil if healthy, or an error describing the issue.
	// Includes watch status - returns an error if watch has consecutive failures.
	Health(ctx context.Context) error
}

Manager manages configuration across namespaces.

The Manager wraps a Store with caching and automatic cache invalidation. It provides access to namespaced configuration via the Namespace method.

Example:

// Create manager
mgr := config.New(
    config.WithStore(memory.NewStore()),
)

// Connect to backend
if err := mgr.Connect(ctx); err != nil {
    return err
}
defer mgr.Close(ctx)

// Get configuration for a namespace (use "" for default)
prodConfig := mgr.Namespace("production")

// Use Reader interface in application code
val, err := prodConfig.Get(ctx, "app/database/timeout")
if err != nil {
    return err
}
var timeout int
if err := val.Unmarshal(&timeout); err != nil {
    return err
}

// Use Writer interface for management
if err := prodConfig.Set(ctx, "app/database/timeout", 30); err != nil {
    return err
}

func ContextManager

func ContextManager(ctx context.Context) Manager

ContextManager retrieves the Manager from context. Returns nil if no manager is set.

func New

func New(opts ...Option) (Manager, error)

New creates a new configuration Manager.

The manager is created but not connected. Call Connect() before use. This follows the New/Connect split pattern for better error handling.

The manager always maintains an internal cache for resilience. If the backend store becomes temporarily unavailable, cached values will continue to be served. This ensures your application keeps working during database outages.

Returns an error if cache initialization fails.

Example
package main

import (
	"context"
	"fmt"

	"github.com/rbaliyan/config"
	"github.com/rbaliyan/config/memory"
)

func main() {
	ctx := context.Background()

	// Create a manager with an in-memory store
	mgr, err := config.New(config.WithStore(memory.NewStore()))
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Connect to the backend
	if err := mgr.Connect(ctx); err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer mgr.Close(ctx)

	// Get a namespaced config and set a value
	cfg := mgr.Namespace("production")
	if err := cfg.Set(ctx, "app/timeout", 30); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Read the value back
	val, err := cfg.Get(ctx, "app/timeout")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	i, _ := val.Int64()
	fmt.Println("timeout:", i)
}
Output:

timeout: 30

type ManagerObserver

type ManagerObserver interface {
	// CacheStats returns statistics about the internal cache.
	CacheStats() CacheStats

	// WatchStatus returns the current status of the watch connection.
	WatchStatus() WatchStatus
}

ManagerObserver provides observability into the Manager's internal state. Use a type assertion to access these methods:

if obs, ok := mgr.(config.ManagerObserver); ok {
    stats := obs.CacheStats()
    status := obs.WatchStatus()
}

type Metadata

type Metadata interface {
	// Version returns the version number of the value.
	Version() int64

	// CreatedAt returns when the value was first created.
	CreatedAt() time.Time

	// UpdatedAt returns when the value was last modified.
	UpdatedAt() time.Time

	// IsStale returns true if this value was served from cache due to a store error.
	// When true, the value may be outdated. Applications can use this to:
	// - Log warnings about stale data
	// - Show degraded UI indicators
	// - Trigger background refresh
	IsStale() bool
}

Metadata provides version and timestamp information for stored values.

type Option

type Option func(*managerOptions)

Option configures the Manager.

func WithCodec

func WithCodec(c codec.Codec) Option

WithCodec sets the default codec for encoding/decoding values. Default is JSON if not specified.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets a custom logger.

func WithStore

func WithStore(store Store) Option

WithStore sets the configuration store backend. This is required - the manager will fail to connect without a store.

func WithWatchBackoffFactor added in v0.1.12

func WithWatchBackoffFactor(f float64) Option

WithWatchBackoffFactor sets the multiplier applied to backoff after each watch failure. Default: 2.0.

func WithWatchInitialBackoff added in v0.1.12

func WithWatchInitialBackoff(d time.Duration) Option

WithWatchInitialBackoff sets the initial wait time between watch reconnection attempts. Default: 100ms.

func WithWatchMaxBackoff added in v0.1.12

func WithWatchMaxBackoff(d time.Duration) Option

WithWatchMaxBackoff sets the maximum wait time between watch reconnection attempts. Default: 30s.

type Page

type Page interface {
	// Results returns the values in this page as a map of key -> Value.
	Results() map[string]Value

	// NextCursor returns the cursor for fetching the next page.
	// This is typically the last key in the results.
	NextCursor() string

	// Limit returns the actual limit used by the server.
	// The server may adjust the requested limit. Clients should check
	// len(Results()) < Limit() to determine if there are more results.
	Limit() int
}

Page represents a page of results from a Find operation. It provides access to the results and pagination information.

Example usage for pagination:

limit := 100
filter := config.NewFilter().WithPrefix("app/").WithLimit(limit).Build()
for {
    page, err := cfg.Find(ctx, filter)
    if err != nil {
        return err
    }
    for key, val := range page.Results() {
        // Process each entry
    }
    // No more results if returned count < limit
    if len(page.Results()) < page.Limit() {
        break
    }
    filter = config.NewFilter().WithPrefix("app/").WithLimit(limit).WithCursor(page.NextCursor()).Build()
}

func NewPage

func NewPage(results map[string]Value, nextCursor string, limit int) Page

NewPage creates a new Page with the given results and pagination info. This is used by Store implementations to create Page results.

type Reader

type Reader interface {
	// Get retrieves a configuration value by key.
	//
	// Get uses an internal cache for resilience. If the store is unavailable,
	// it will return a cached value if one exists. This is intentional - for
	// use cases like feature flags and rate limits, having slightly stale
	// configuration is better than failing completely.
	Get(ctx context.Context, key string) (Value, error)

	// Find returns a page of keys and values matching the filter.
	// Use Page.NextCursor() to paginate through results.
	Find(ctx context.Context, filter Filter) (Page, error)
}

Reader provides read-only access to configuration. Use this interface in application code to read config values.

The Reader automatically uses an internal cache for resilience. If the backend store is temporarily unavailable, cached values will be returned. This ensures your application continues working even during database outages.

type SetOption

type SetOption func(*setOptions)

SetOption configures Set operations.

func WithIfExists

func WithIfExists() SetOption

WithIfExists configures Set to only update the key if it already exists. Returns ErrNotFound if the key doesn't exist.

This is useful for implementing "update-only" semantics where you want to ensure the key was previously created.

Example:

err := cfg.Set(ctx, "app/timeout", 60, config.WithIfExists())
if config.IsNotFound(err) {
    // Key doesn't exist, need to create it first
}

func WithIfNotExists

func WithIfNotExists() SetOption

WithIfNotExists configures Set to only create the key if it doesn't exist. Returns ErrKeyExists if the key already exists.

This is useful for implementing "create-only" semantics where you want to ensure you don't accidentally overwrite an existing value.

Example:

err := cfg.Set(ctx, "lock/owner", "instance-1", config.WithIfNotExists())
if config.IsKeyExists(err) {
    // Key was already taken by another instance
}

func WithSetCodec

func WithSetCodec(c codec.Codec) SetOption

WithSetCodec sets the codec for encoding the value.

func WithType

func WithType(t Type) SetOption

WithType explicitly sets the value type.

type StatsProvider

type StatsProvider interface {
	// Stats returns store statistics.
	Stats(ctx context.Context) (*StoreStats, error)
}

StatsProvider is an optional interface for stores that provide statistics.

type Store

type Store interface {
	// Connect establishes connection to the storage backend.
	// Must be called before any other operations.
	Connect(ctx context.Context) error

	// Close releases resources and closes the connection.
	Close(ctx context.Context) error

	// Get retrieves a configuration value by namespace and key.
	// Returns ErrNotFound if the entry does not exist.
	Get(ctx context.Context, namespace, key string) (Value, error)

	// Set creates or updates a configuration value.
	// The version is auto-incremented on each update.
	// Returns the stored Value with updated metadata (version, timestamps).
	Set(ctx context.Context, namespace, key string, value Value) (Value, error)

	// Delete removes a configuration value by namespace and key.
	// Returns ErrNotFound if the entry does not exist.
	Delete(ctx context.Context, namespace, key string) error

	// Find returns a page of keys and values matching the filter within a namespace.
	// Use Page.NextCursor() to paginate through results.
	Find(ctx context.Context, namespace string, filter Filter) (Page, error)

	// Watch returns a channel that receives change events for cache invalidation.
	// The channel is closed when the context is cancelled.
	// This is used by the Manager for automatic cache synchronization.
	// For stores that don't support real-time watching (e.g., file-based),
	// return ErrWatchNotSupported.
	Watch(ctx context.Context, filter WatchFilter) (<-chan ChangeEvent, error)
}

Store defines the interface for configuration storage backends.

Implementations must be safe for concurrent use by multiple goroutines. The store is responsible for persistence and versioning.

Design Philosophy

This library is designed for use cases like feature flags, rate limits, and dynamic configuration where eventual consistency is acceptable. The key principle is: having some configuration (even slightly stale) is better than having no configuration at all.

The library maintains an internal in-memory cache that serves as a resilience layer. If the backend store becomes temporarily unavailable, the application can continue operating with cached values. This cache is NOT meant for sharing state across multiple application instances - each instance maintains its own cache that is kept in sync with the backend via the store's Watch mechanism.

For multi-instance deployments, each instance independently watches the backend (e.g., MongoDB Change Streams, PostgreSQL LISTEN/NOTIFY) to invalidate its local cache. This provides eventual consistency without requiring external dependencies like Redis.

Implementations:

  • memory.Store: For testing and single-instance deployments
  • mongodb.Store: For MongoDB databases (uses Change Streams internally)
  • postgres.Store: For PostgreSQL databases (uses LISTEN/NOTIFY internally)

type StoreError

type StoreError struct {
	Op      string // Operation that failed
	Key     string // Key involved (if applicable)
	Backend string // Backend name (memory, mongodb, postgres)
	Err     error  // Underlying error
}

StoreError wraps backend-specific errors with domain context.

func (*StoreError) Error

func (e *StoreError) Error() string

func (*StoreError) Unwrap

func (e *StoreError) Unwrap() error

type StoreStats

type StoreStats struct {
	TotalEntries       int64            `json:"total_entries"`
	EntriesByType      map[Type]int64   `json:"entries_by_type"`
	EntriesByNamespace map[string]int64 `json:"entries_by_namespace"`
}

StoreStats contains storage statistics.

type Type

type Type int

Type represents the type of a configuration value.

const (
	// TypeUnknown indicates an unknown or unsupported type.
	TypeUnknown Type = iota

	// TypeInt represents an integer value.
	TypeInt

	// TypeFloat represents a floating-point value.
	TypeFloat

	// TypeString represents a string value.
	TypeString

	// TypeBool represents a boolean value.
	TypeBool

	// TypeMapStringInt represents a map[string]int value.
	TypeMapStringInt

	// TypeMapStringFloat represents a map[string]float64 value.
	TypeMapStringFloat

	// TypeMapStringString represents a map[string]string value.
	TypeMapStringString

	// TypeListInt represents a []int value.
	TypeListInt

	// TypeListFloat represents a []float64 value.
	TypeListFloat

	// TypeListString represents a []string value.
	TypeListString

	// TypeCustom represents a custom type that requires Unmarshal.
	TypeCustom
)

func ParseType

func ParseType(s string) Type

ParseType parses a string into a Type. Returns TypeUnknown for unrecognized strings.

func (Type) IsList

func (t Type) IsList() bool

IsList returns true if the type is a list type.

func (Type) IsMap

func (t Type) IsMap() bool

IsMap returns true if the type is a map type.

func (Type) IsPrimitive

func (t Type) IsPrimitive() bool

IsPrimitive returns true if the type is a primitive (int, float, string, bool).

func (Type) String

func (t Type) String() string

String returns the string representation of the type.

type TypeMismatchError

type TypeMismatchError struct {
	Key      string
	Expected Type
	Actual   Type
}

TypeMismatchError provides details about a type conversion failure.

func (*TypeMismatchError) Error

func (e *TypeMismatchError) Error() string

func (*TypeMismatchError) Unwrap

func (e *TypeMismatchError) Unwrap() error

type UnsupportedCodecError added in v0.4.2

type UnsupportedCodecError struct {
	Codec   string
	Backend string
}

UnsupportedCodecError provides details about a codec that is not supported by the store.

func (*UnsupportedCodecError) Error added in v0.4.2

func (e *UnsupportedCodecError) Error() string

func (*UnsupportedCodecError) Unwrap added in v0.4.2

func (e *UnsupportedCodecError) Unwrap() error

type Value

type Value interface {
	// Marshal serializes the value to bytes using the configured codec.
	Marshal() ([]byte, error)

	// Unmarshal deserializes the value into the target.
	Unmarshal(v any) error

	// Type returns the detected type of the value.
	Type() Type

	// Codec returns the codec name used for this value.
	Codec() string

	// Int64 returns the value as int64.
	Int64() (int64, error)

	// Float64 returns the value as float64.
	Float64() (float64, error)

	// String returns the value as string.
	String() (string, error)

	// Bool returns the value as bool.
	Bool() (bool, error)

	// Metadata returns associated metadata, if any.
	Metadata() Metadata
}

Value provides type-safe access to configuration values. This interface is focused on read operations. Write-related concerns like WriteMode are accessed via the WriteModer interface.

func Get

func Get(ctx context.Context, key string) (Value, error)

Get is a convenience function that retrieves a value using the Manager from context. Returns ErrManagerClosed if no manager is in context. Uses the namespace from context, or "" (default) if not set.

func MarkStale

func MarkStale(v Value) Value

MarkStale returns a copy of the value with the stale flag set. This is used when serving cached values due to store errors. The returned value's Metadata().IsStale() will return true.

func NewValue

func NewValue(data any, opts ...ValueOption) Value

NewValue creates a Value from any data with optional configuration.

Example
package main

import (
	"fmt"

	"github.com/rbaliyan/config"
)

func main() {
	// Create a value and inspect its type
	val := config.NewValue(42)
	fmt.Println("type:", val.Type())

	i, err := val.Int64()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("value:", i)
}
Output:

type: int
value: 42

func NewValueFromBytes

func NewValueFromBytes(data []byte, codecName string, opts ...ValueOption) (Value, error)

NewValueFromBytes creates a Value from encoded bytes.

type ValueOption

type ValueOption func(*val)

ValueOption configures a value during construction.

func WithValueCodec

func WithValueCodec(c codec.Codec) ValueOption

WithCodec sets the codec for the value.

func WithValueEntryID

func WithValueEntryID(id string) ValueOption

WithValueEntryID sets the internal entry ID for the value. This is used by store implementations to track the database-level ID. Not intended for end-user code.

func WithValueMetadata

func WithValueMetadata(version int64, createdAt, updatedAt time.Time) ValueOption

WithValueMetadata sets metadata for the value.

func WithValueStale

func WithValueStale(stale bool) ValueOption

WithValueStale marks the value as stale (served from cache due to store error). This is used internally by the Manager when falling back to cached values.

func WithValueType

func WithValueType(t Type) ValueOption

WithValueType sets the type for the value.

func WithValueWriteMode

func WithValueWriteMode(mode WriteMode) ValueOption

WithValueWriteMode sets the write mode for the value.

type WatchFilter

type WatchFilter struct {
	// Namespaces to watch (empty = all namespaces).
	Namespaces []string

	// Prefixes to watch within namespaces (empty = all keys).
	Prefixes []string
}

WatchFilter specifies criteria for watching changes.

type WatchHealthError

type WatchHealthError struct {
	ConsecutiveFailures int32
	LastError           string
}

WatchHealthError provides details about watch connection failures. Returned by Manager.Health() when watch has multiple consecutive failures.

func (*WatchHealthError) Error

func (e *WatchHealthError) Error() string

func (*WatchHealthError) Unwrap

func (e *WatchHealthError) Unwrap() error

type WatchStatus

type WatchStatus struct {
	// Connected indicates if the manager is connected to the store.
	Connected bool `json:"connected"`

	// ConsecutiveFailures is the number of consecutive watch failures.
	// Resets to 0 when watch successfully connects.
	ConsecutiveFailures int32 `json:"consecutive_failures"`

	// LastError is the most recent watch error message (empty if no error).
	LastError string `json:"last_error,omitempty"`

	// LastAttempt is when the last watch connection was attempted.
	LastAttempt time.Time `json:"last_attempt,omitempty"`

	// Cache contains cache statistics for correlation with watch health.
	Cache CacheStats `json:"cache"`
}

WatchStatus provides observability into the watch connection state. Applications can use this to monitor watch health and implement their own alerting or circuit breaking logic if needed.

type WriteMode

type WriteMode int

WriteMode specifies the conditional behavior for Set operations.

const (
	// WriteModeUpsert creates or updates the key (default behavior).
	WriteModeUpsert WriteMode = iota

	// WriteModeCreate only creates the key if it doesn't exist.
	// Returns ErrKeyExists if the key already exists.
	WriteModeCreate

	// WriteModeUpdate only updates the key if it already exists.
	// Returns ErrNotFound if the key doesn't exist.
	WriteModeUpdate
)

func GetWriteMode

func GetWriteMode(v Value) WriteMode

GetWriteMode extracts the write mode from a Value. Returns WriteModeUpsert (default) if the value does not implement WriteModer. Store implementations should use this instead of calling WriteMode() directly.

func (WriteMode) String

func (m WriteMode) String() string

String returns the string representation of the write mode.

type WriteModer

type WriteModer interface {
	WriteMode() WriteMode
}

WriteModer is an optional interface for values that carry write mode hints. Store implementations check for this interface in Set operations to determine conditional write behavior (create-only, update-only, or upsert).

type Writer

type Writer interface {
	// Set creates or updates a configuration value.
	Set(ctx context.Context, key string, value any, opts ...SetOption) error

	// Delete removes a configuration value by key.
	Delete(ctx context.Context, key string) error
}

Writer provides write access to configuration. Use this interface for gRPC/HTTP servers to manage config.

Directories

Path Synopsis
Package bind provides struct binding and validation for the config library.
Package bind provides struct binding and validation for the config library.
json
Package json provides a JSON codec for the config library.
Package json provides a JSON codec for the config library.
toml
Package toml provides a TOML codec for the config library.
Package toml provides a TOML codec for the config library.
yaml
Package yaml provides a YAML codec for the config library.
Package yaml provides a YAML codec for the config library.
Package file provides file-based configuration loading and a read-only config.Store backed by YAML, TOML, or JSON files.
Package file provides file-based configuration loading and a read-only config.Store backed by YAML, TOML, or JSON files.
Package memory provides an in-memory configuration store for testing and single-instance deployments.
Package memory provides an in-memory configuration store for testing and single-instance deployments.
Package mongodb provides a MongoDB-backed configuration store with change stream notifications.
Package mongodb provides a MongoDB-backed configuration store with change stream notifications.
Package multi provides a multi-store wrapper that supports fallback patterns.
Package multi provides a multi-store wrapper that supports fallback patterns.
Package otel provides OpenTelemetry instrumentation for the config library.
Package otel provides OpenTelemetry instrumentation for the config library.
Package postgres provides a PostgreSQL-backed configuration store with LISTEN/NOTIFY change notifications.
Package postgres provides a PostgreSQL-backed configuration store with LISTEN/NOTIFY change notifications.
Package sqlite provides a SQLite-backed configuration store with application-level change notifications.
Package sqlite provides a SQLite-backed configuration store with application-level change notifications.

Jump to

Keyboard shortcuts

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