// Package config loads and validates the backend's runtime configuration from // the process environment. package config import ( "fmt" "os" "strconv" "time" "scrabble/backend/internal/postgres" "scrabble/backend/internal/telemetry" ) // Config holds the backend's runtime configuration. type Config struct { // HTTPAddr is the listen address of the HTTP listener (host:port). HTTPAddr string // LogLevel is the zap log level: "debug", "info", "warn" or "error". LogLevel string // Postgres configures the primary database pool. Postgres postgres.Config // Telemetry configures the OpenTelemetry providers. Telemetry telemetry.Config } // Defaults applied when the corresponding environment variable is unset. const ( defaultHTTPAddr = ":8080" defaultLogLevel = "info" ) // Load reads the configuration from the environment, applies defaults for unset // variables, and validates the result. func Load() (Config, error) { pg := postgres.DefaultConfig() pg.DSN = os.Getenv("BACKEND_POSTGRES_DSN") var err error if pg.MaxOpenConns, err = envInt("BACKEND_POSTGRES_MAX_OPEN_CONNS", pg.MaxOpenConns); err != nil { return Config{}, err } if pg.MaxIdleConns, err = envInt("BACKEND_POSTGRES_MAX_IDLE_CONNS", pg.MaxIdleConns); err != nil { return Config{}, err } if pg.ConnMaxLifetime, err = envDuration("BACKEND_POSTGRES_CONN_MAX_LIFETIME", pg.ConnMaxLifetime); err != nil { return Config{}, err } if pg.OperationTimeout, err = envDuration("BACKEND_POSTGRES_OPERATION_TIMEOUT", pg.OperationTimeout); err != nil { return Config{}, err } tel := telemetry.DefaultConfig() tel.ServiceName = envOr("BACKEND_SERVICE_NAME", tel.ServiceName) tel.TracesExporter = envOr("BACKEND_OTEL_TRACES_EXPORTER", tel.TracesExporter) tel.MetricsExporter = envOr("BACKEND_OTEL_METRICS_EXPORTER", tel.MetricsExporter) c := Config{ HTTPAddr: envOr("BACKEND_HTTP_ADDR", defaultHTTPAddr), LogLevel: envOr("BACKEND_LOG_LEVEL", defaultLogLevel), Postgres: pg, Telemetry: tel, } if err := c.validate(); err != nil { return Config{}, err } return c, nil } // validate reports whether the configuration values are acceptable. func (c Config) validate() error { switch c.LogLevel { case "debug", "info", "warn", "error": default: return fmt.Errorf("config: invalid BACKEND_LOG_LEVEL %q", c.LogLevel) } if c.HTTPAddr == "" { return fmt.Errorf("config: BACKEND_HTTP_ADDR must not be empty") } if err := c.Postgres.Validate(); err != nil { return fmt.Errorf("config: %w (set BACKEND_POSTGRES_DSN)", err) } if err := c.Telemetry.Validate(); err != nil { return fmt.Errorf("config: %w", err) } return nil } // envOr returns the value of the environment variable named key, or fallback // when the variable is unset or empty. func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // envInt parses the environment variable named key as an int, returning // fallback when it is unset and an error when it is set but malformed. func envInt(key string, fallback int) (int, error) { v := os.Getenv(key) if v == "" { return fallback, nil } n, err := strconv.Atoi(v) if err != nil { return 0, fmt.Errorf("config: %s: %w", key, err) } return n, nil } // envDuration parses the environment variable named key as a Go duration, // returning fallback when it is unset and an error when it is set but malformed. func envDuration(key string, fallback time.Duration) (time.Duration, error) { v := os.Getenv(key) if v == "" { return fallback, nil } d, err := time.ParseDuration(v) if err != nil { return 0, fmt.Errorf("config: %s: %w", key, err) } return d, nil }