config.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. // Package config is the env-driven configuration loader used by every
  2. // service. Twelve-factor: no flag parsing, no config files, just env
  3. // vars. Anything not in here has a sane default.
  4. //
  5. // Naming convention: BA_<SERVICE>_<KEY>, e.g. BA_INGESTD_HTTP_ADDR.
  6. package config
  7. import (
  8. "fmt"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "time"
  13. )
  14. // Common is shared by every service.
  15. type Common struct {
  16. Env string // dev | staging | prod
  17. ServiceName string
  18. LogLevel string // debug | info | warn | error
  19. HTTPAddr string // /health + /metrics + (later) /v1/*
  20. // NATS
  21. NATSURL string // nats://nats:4222
  22. // Postgres
  23. PostgresDSN string
  24. // Redis
  25. RedisURL string // redis://redis:6379/0
  26. // Shutdown
  27. ShutdownGrace time.Duration
  28. }
  29. // Default values applied if env unset.
  30. func defaultCommon() Common {
  31. return Common{
  32. Env: "dev",
  33. ServiceName: "broad-announce",
  34. LogLevel: "info",
  35. HTTPAddr: ":8800",
  36. NATSURL: envOr("BA_NATS_URL", "nats://localhost:4222"),
  37. PostgresDSN: envOr("BA_POSTGRES_DSN", "postgres://ba:ba@localhost:5432/ba?sslmode=disable"),
  38. RedisURL: envOr("BA_REDIS_URL", "redis://localhost:6379/0"),
  39. ShutdownGrace: 15 * time.Second,
  40. }
  41. }
  42. // LoadCommon reads env, applies defaults, and returns a validated Common.
  43. func LoadCommon(serviceName string) (Common, error) {
  44. c := defaultCommon()
  45. c.ServiceName = serviceName
  46. if v := os.Getenv("BA_ENV"); v != "" {
  47. c.Env = v
  48. }
  49. if v := os.Getenv("BA_LOG_LEVEL"); v != "" {
  50. c.LogLevel = v
  51. }
  52. if v := os.Getenv("BA_HTTP_ADDR"); v != "" {
  53. c.HTTPAddr = v
  54. }
  55. if v := os.Getenv("BA_NATS_URL"); v != "" {
  56. c.NATSURL = v
  57. }
  58. if v := os.Getenv("BA_POSTGRES_DSN"); v != "" {
  59. c.PostgresDSN = v
  60. }
  61. if v := os.Getenv("BA_REDIS_URL"); v != "" {
  62. c.RedisURL = v
  63. }
  64. if v := os.Getenv("BA_SHUTDOWN_GRACE_SEC"); v != "" {
  65. n, err := strconv.Atoi(v)
  66. if err != nil {
  67. return c, fmt.Errorf("BA_SHUTDOWN_GRACE_SEC: %w", err)
  68. }
  69. c.ShutdownGrace = time.Duration(n) * time.Second
  70. }
  71. if c.Env != "dev" && c.Env != "staging" && c.Env != "prod" {
  72. return c, fmt.Errorf("BA_ENV must be dev|staging|prod, got %q", c.Env)
  73. }
  74. return c, nil
  75. }
  76. func envOr(k, def string) string {
  77. if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
  78. return v
  79. }
  80. return def
  81. }
  82. // GetInt parses an env var as int, returns def if unset.
  83. func GetInt(k string, def int) int {
  84. v := os.Getenv(k)
  85. if v == "" {
  86. return def
  87. }
  88. n, err := strconv.Atoi(v)
  89. if err != nil {
  90. return def
  91. }
  92. return n
  93. }
  94. // GetDuration parses an env var as Go duration, returns def if unset.
  95. func GetDuration(k string, def time.Duration) time.Duration {
  96. v := os.Getenv(k)
  97. if v == "" {
  98. return def
  99. }
  100. d, err := time.ParseDuration(v)
  101. if err != nil {
  102. return def
  103. }
  104. return d
  105. }
  106. // Ingestd is ingestd-specific config. Kept here so the service
  107. // binary has one import.
  108. type Ingestd struct {
  109. Common
  110. // Source protection defaults (SPEC §22). Per-source overrides
  111. // come from the DB and override these.
  112. MaxPayloadBytes int
  113. RateLimitPerSource int
  114. RateLimitPerCompany int
  115. MaxConcurrentPerIP int
  116. QuarantineHitsThreshold int
  117. QuarantineWindowSeconds int
  118. QuarantineDurationSecond int
  119. // DedupeTTLSeconds is the M6 sliding-window TTL for a
  120. // dedupe entry. Refreshed on every duplicate observation,
  121. // so a steady stream of duplicates keeps the window alive.
  122. // Default 300s (5 min) — up from 60s in M0–M5 to give
  123. // operators a longer window to see `×N` rollups.
  124. DedupeTTLSeconds int
  125. }
  126. // LoadIngestd reads ingestd-specific config.
  127. func LoadIngestd() (Ingestd, error) {
  128. c, err := LoadCommon("ingestd")
  129. if err != nil {
  130. return Ingestd{}, err
  131. }
  132. return Ingestd{
  133. Common: c,
  134. MaxPayloadBytes: GetInt("BA_INGESTD_MAX_PAYLOAD_BYTES", 256*1024),
  135. RateLimitPerSource: GetInt("BA_INGESTD_RATE_LIMIT_PER_SOURCE", 100),
  136. RateLimitPerCompany: GetInt("BA_INGESTD_RATE_LIMIT_PER_COMPANY", 10_000),
  137. MaxConcurrentPerIP: GetInt("BA_INGESTD_MAX_CONCURRENT_PER_IP", 64),
  138. DedupeTTLSeconds: GetInt("BA_INGESTD_DEDUPE_TTL_SECONDS", 300),
  139. QuarantineHitsThreshold: GetInt("BA_INGESTD_QUARANTINE_HITS_THRESHOLD", 100),
  140. QuarantineWindowSeconds: GetInt("BA_INGESTD_QUARANTINE_WINDOW_SECONDS", 60),
  141. QuarantineDurationSecond: GetInt("BA_INGESTD_QUARANTINE_DURATION_SECONDS", 300),
  142. }, nil
  143. }