|
@@ -0,0 +1,152 @@
|
|
|
|
|
+// Package config is the env-driven configuration loader used by every
|
|
|
|
|
+// service. Twelve-factor: no flag parsing, no config files, just env
|
|
|
|
|
+// vars. Anything not in here has a sane default.
|
|
|
|
|
+//
|
|
|
|
|
+// Naming convention: BA_<SERVICE>_<KEY>, e.g. BA_INGESTD_HTTP_ADDR.
|
|
|
|
|
+package config
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "strconv"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "time"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// Common is shared by every service.
|
|
|
|
|
+type Common struct {
|
|
|
|
|
+ Env string // dev | staging | prod
|
|
|
|
|
+ ServiceName string
|
|
|
|
|
+ LogLevel string // debug | info | warn | error
|
|
|
|
|
+ HTTPAddr string // /health + /metrics + (later) /v1/*
|
|
|
|
|
+
|
|
|
|
|
+ // NATS
|
|
|
|
|
+ NATSURL string // nats://nats:4222
|
|
|
|
|
+
|
|
|
|
|
+ // Postgres
|
|
|
|
|
+ PostgresDSN string
|
|
|
|
|
+
|
|
|
|
|
+ // Redis
|
|
|
|
|
+ RedisURL string // redis://redis:6379/0
|
|
|
|
|
+
|
|
|
|
|
+ // Shutdown
|
|
|
|
|
+ ShutdownGrace time.Duration
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Default values applied if env unset.
|
|
|
|
|
+func defaultCommon() Common {
|
|
|
|
|
+ return Common{
|
|
|
|
|
+ Env: "dev",
|
|
|
|
|
+ ServiceName: "broad-announce",
|
|
|
|
|
+ LogLevel: "info",
|
|
|
|
|
+ HTTPAddr: ":8080",
|
|
|
|
|
+ NATSURL: envOr("BA_NATS_URL", "nats://localhost:4222"),
|
|
|
|
|
+ PostgresDSN: envOr("BA_POSTGRES_DSN", "postgres://ba:ba@localhost:5432/ba?sslmode=disable"),
|
|
|
|
|
+ RedisURL: envOr("BA_REDIS_URL", "redis://localhost:6379/0"),
|
|
|
|
|
+ ShutdownGrace: 15 * time.Second,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// LoadCommon reads env, applies defaults, and returns a validated Common.
|
|
|
|
|
+func LoadCommon(serviceName string) (Common, error) {
|
|
|
|
|
+ c := defaultCommon()
|
|
|
|
|
+ c.ServiceName = serviceName
|
|
|
|
|
+
|
|
|
|
|
+ if v := os.Getenv("BA_ENV"); v != "" {
|
|
|
|
|
+ c.Env = v
|
|
|
|
|
+ }
|
|
|
|
|
+ if v := os.Getenv("BA_LOG_LEVEL"); v != "" {
|
|
|
|
|
+ c.LogLevel = v
|
|
|
|
|
+ }
|
|
|
|
|
+ if v := os.Getenv("BA_HTTP_ADDR"); v != "" {
|
|
|
|
|
+ c.HTTPAddr = v
|
|
|
|
|
+ }
|
|
|
|
|
+ if v := os.Getenv("BA_NATS_URL"); v != "" {
|
|
|
|
|
+ c.NATSURL = v
|
|
|
|
|
+ }
|
|
|
|
|
+ if v := os.Getenv("BA_POSTGRES_DSN"); v != "" {
|
|
|
|
|
+ c.PostgresDSN = v
|
|
|
|
|
+ }
|
|
|
|
|
+ if v := os.Getenv("BA_REDIS_URL"); v != "" {
|
|
|
|
|
+ c.RedisURL = v
|
|
|
|
|
+ }
|
|
|
|
|
+ if v := os.Getenv("BA_SHUTDOWN_GRACE_SEC"); v != "" {
|
|
|
|
|
+ n, err := strconv.Atoi(v)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return c, fmt.Errorf("BA_SHUTDOWN_GRACE_SEC: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ c.ShutdownGrace = time.Duration(n) * time.Second
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if c.Env != "dev" && c.Env != "staging" && c.Env != "prod" {
|
|
|
|
|
+ return c, fmt.Errorf("BA_ENV must be dev|staging|prod, got %q", c.Env)
|
|
|
|
|
+ }
|
|
|
|
|
+ return c, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func envOr(k, def string) string {
|
|
|
|
|
+ if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+ return def
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetInt parses an env var as int, returns def if unset.
|
|
|
|
|
+func GetInt(k string, def int) int {
|
|
|
|
|
+ v := os.Getenv(k)
|
|
|
|
|
+ if v == "" {
|
|
|
|
|
+ return def
|
|
|
|
|
+ }
|
|
|
|
|
+ n, err := strconv.Atoi(v)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return def
|
|
|
|
|
+ }
|
|
|
|
|
+ return n
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetDuration parses an env var as Go duration, returns def if unset.
|
|
|
|
|
+func GetDuration(k string, def time.Duration) time.Duration {
|
|
|
|
|
+ v := os.Getenv(k)
|
|
|
|
|
+ if v == "" {
|
|
|
|
|
+ return def
|
|
|
|
|
+ }
|
|
|
|
|
+ d, err := time.ParseDuration(v)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return def
|
|
|
|
|
+ }
|
|
|
|
|
+ return d
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Ingestd is ingestd-specific config. Kept here so the service
|
|
|
|
|
+// binary has one import.
|
|
|
|
|
+type Ingestd struct {
|
|
|
|
|
+ Common
|
|
|
|
|
+
|
|
|
|
|
+ // Source protection defaults (SPEC §22). Per-source overrides
|
|
|
|
|
+ // come from the DB and override these.
|
|
|
|
|
+ MaxPayloadBytes int
|
|
|
|
|
+ RateLimitPerSource int
|
|
|
|
|
+ RateLimitPerCompany int
|
|
|
|
|
+ MaxConcurrentPerIP int
|
|
|
|
|
+ QuarantineHitsThreshold int
|
|
|
|
|
+ QuarantineWindowSeconds int
|
|
|
|
|
+ QuarantineDurationSecond int
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// LoadIngestd reads ingestd-specific config.
|
|
|
|
|
+func LoadIngestd() (Ingestd, error) {
|
|
|
|
|
+ c, err := LoadCommon("ingestd")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return Ingestd{}, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return Ingestd{
|
|
|
|
|
+ Common: c,
|
|
|
|
|
+ MaxPayloadBytes: GetInt("BA_INGESTD_MAX_PAYLOAD_BYTES", 256*1024),
|
|
|
|
|
+ RateLimitPerSource: GetInt("BA_INGESTD_RATE_LIMIT_PER_SOURCE", 100),
|
|
|
|
|
+ RateLimitPerCompany: GetInt("BA_INGESTD_RATE_LIMIT_PER_COMPANY", 10_000),
|
|
|
|
|
+ MaxConcurrentPerIP: GetInt("BA_INGESTD_MAX_CONCURRENT_PER_IP", 64),
|
|
|
|
|
+ QuarantineHitsThreshold: GetInt("BA_INGESTD_QUARANTINE_HITS_THRESHOLD", 100),
|
|
|
|
|
+ QuarantineWindowSeconds: GetInt("BA_INGESTD_QUARANTINE_WINDOW_SECONDS", 60),
|
|
|
|
|
+ QuarantineDurationSecond: GetInt("BA_INGESTD_QUARANTINE_DURATION_SECONDS", 300),
|
|
|
|
|
+ }, nil
|
|
|
|
|
+}
|