Files
pgsql-broker/pkg/broker/config/config.go
T
SG Command f69cbea49f
Integration Tests / integration-test (pull_request) Successful in 2m33s
feat: add prometheus metrics and dashboard
2026-09-19 05:12:04 +02:00

217 lines
6.9 KiB
Go

package config
import (
"fmt"
"strings"
"time"
"git.warky.dev/wdevs/pgsql-broker/pkg/broker/adapter"
"github.com/spf13/viper"
)
// Config holds all broker configuration
type Config struct {
Databases []DatabaseConfig `mapstructure:"databases"`
Broker BrokerConfig `mapstructure:"broker"`
Logging LoggingConfig `mapstructure:"logging"`
}
// DatabaseConfig holds database connection settings
type DatabaseConfig struct {
Name string `mapstructure:"name"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Database string `mapstructure:"database"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
SSLMode string `mapstructure:"sslmode"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
QueueCount int `mapstructure:"queue_count"`
// TenantID is the RLS tenant this instance's own workers operate as
// (via broker_set_tenant) when claiming/running jobs. Defaults to
// "default" so single-tenant deployments are unaffected.
TenantID string `mapstructure:"tenant_id"`
// AutoMigrate, when true, applies any pending embedded migrations on
// connect during normal `start`. When false (default), startup fails
// fast if the schema is behind, naming the missing migrations and
// pointing at `pgsql-broker install`.
AutoMigrate bool `mapstructure:"auto_migrate"`
}
// BrokerConfig holds broker-specific settings
type BrokerConfig struct {
Name string `mapstructure:"name"`
FetchQueryQueSize int `mapstructure:"fetch_query_que_size"`
QueueTimerSec int `mapstructure:"queue_timer_sec"`
QueueBufferSize int `mapstructure:"queue_buffer_size"`
WorkerIdleTimeoutSec int `mapstructure:"worker_idle_timeout_sec"`
NotifyRetryInterval time.Duration `mapstructure:"notify_retry_seconds"`
EnableDebug bool `mapstructure:"enable_debug"`
// LeaseSeconds is how long a claimed job's lease is valid for before
// broker_recover_stale_jobs considers it abandoned.
LeaseSeconds int `mapstructure:"lease_seconds"`
// StaleJobRecoverySec is the interval between broker_recover_stale_jobs sweeps.
StaleJobRecoverySec int `mapstructure:"stale_job_recovery_sec"`
// MetricsEnabled controls whether the embedded Prometheus metrics HTTP
// server (exposition endpoint + HTML dashboard) is started.
MetricsEnabled bool `mapstructure:"metrics_enabled"`
// MetricsHost is the bind address for the metrics server.
MetricsHost string `mapstructure:"metrics_host"`
// MetricsPort is the bind port for the metrics server.
MetricsPort int `mapstructure:"metrics_port"`
// QueueDepthPollSec is the interval between polls of pending job counts
// used to populate the broker_jobs_queued gauge.
QueueDepthPollSec int `mapstructure:"queue_depth_poll_sec"`
}
// LoggingConfig holds logging settings
type LoggingConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"` // json or text
}
// LoadConfig loads configuration from file and environment variables
func LoadConfig(configPath string) (*Config, error) {
v := viper.New()
// Set defaults
setDefaults(v)
// Config file settings
if configPath != "" {
v.SetConfigFile(configPath)
} else {
v.SetConfigName("broker")
v.SetConfigType("yaml")
v.AddConfigPath(".")
v.AddConfigPath("/etc/pgsql-broker/")
v.AddConfigPath("$HOME/.pgsql-broker/")
}
// Read from environment variables
v.SetEnvPrefix("BROKER")
v.AutomaticEnv()
// Read config file
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
}
var config Config
if err := v.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Validate configuration
if err := validateConfig(&config); err != nil {
return nil, err
}
// Apply defaults to databases
applyDatabaseDefaults(&config)
return &config, nil
}
// setDefaults sets default configuration values
func setDefaults(v *viper.Viper) {
// Broker defaults
v.SetDefault("broker.name", "pgsql-broker")
v.SetDefault("broker.fetch_query_que_size", 100)
v.SetDefault("broker.queue_timer_sec", 10)
v.SetDefault("broker.queue_buffer_size", 50)
v.SetDefault("broker.worker_idle_timeout_sec", 10)
v.SetDefault("broker.notify_retry_seconds", 30*time.Second)
v.SetDefault("broker.enable_debug", false)
v.SetDefault("broker.lease_seconds", 60)
v.SetDefault("broker.stale_job_recovery_sec", 30)
v.SetDefault("broker.metrics_enabled", true)
v.SetDefault("broker.metrics_host", "0.0.0.0")
v.SetDefault("broker.metrics_port", 9469)
v.SetDefault("broker.queue_depth_poll_sec", 15)
// Logging defaults
v.SetDefault("logging.level", "info")
v.SetDefault("logging.format", "json")
}
// validateConfig validates the configuration
func validateConfig(config *Config) error {
if len(config.Databases) == 0 {
return fmt.Errorf("at least one database must be configured")
}
// Validate each database configuration
for i := range config.Databases {
db := &config.Databases[i]
if db.Name == "" {
return fmt.Errorf("database[%d]: name is required", i)
}
if db.Host == "" {
return fmt.Errorf("database[%d] (%s): host is required", i, db.Name)
}
if db.Database == "" {
return fmt.Errorf("database[%d] (%s): database name is required", i, db.Name)
}
if db.User == "" {
return fmt.Errorf("database[%d] (%s): user is required", i, db.Name)
}
}
return nil
}
// applyDatabaseDefaults applies default values to database configurations
func applyDatabaseDefaults(config *Config) {
for i := range config.Databases {
db := &config.Databases[i]
if db.Port == 0 {
db.Port = 5432
}
if db.SSLMode == "" {
db.SSLMode = "disable"
}
if db.MaxOpenConns == 0 {
db.MaxOpenConns = 25
}
if db.MaxIdleConns == 0 {
db.MaxIdleConns = 5
}
if db.ConnMaxLifetime == 0 {
db.ConnMaxLifetime = 5 * time.Minute
}
if db.ConnMaxIdleTime == 0 {
db.ConnMaxIdleTime = 10 * time.Minute
}
if db.QueueCount == 0 {
db.QueueCount = 4
}
if db.TenantID == "" {
db.TenantID = "default"
}
}
}
// ToPostgresConfig converts DatabaseConfig to adapter.PostgresConfig
func (d *DatabaseConfig) ToPostgresConfig() adapter.PostgresConfig {
return adapter.PostgresConfig{
Host: d.Host,
Port: d.Port,
Database: d.Database,
User: d.User,
Password: d.Password,
SSLMode: d.SSLMode,
MaxOpenConns: d.MaxOpenConns,
MaxIdleConns: d.MaxIdleConns,
ConnMaxLifetime: d.ConnMaxLifetime,
ConnMaxIdleTime: d.ConnMaxIdleTime,
ApplicationName: fmt.Sprintf("PGSQL_BROKER_%s", strings.ToUpper(d.Name)),
}
}