config.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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. // Routerd is routerd-specific config.
  127. type Routerd struct {
  128. Common
  129. // DedupeFlushMs is the M6.5 router-level dedupe collapse
  130. // window. A burst of identical alerts is held for up to
  131. // this many ms, then a single delivery is fanned out
  132. // with the final dedupe_count. A continuous stream
  133. // re-flushes every DedupeFlushMs. Default 2000ms.
  134. DedupeFlushMs int
  135. }
  136. // Archiverd is archiverd-specific config (M7).
  137. type Archiverd struct {
  138. Common
  139. // RunEverySeconds is the cadence between archiverd
  140. // passes. Default 3600 (1 hour).
  141. RunEverySeconds int
  142. // OlderThanHours is the retention threshold; rows
  143. // older than now() - OlderThanHours are eligible
  144. // for archive. Default 168 (7 days). The Timescale
  145. // retention policy does the same at 7 days; the
  146. // archiver just runs ahead so CH has the data
  147. // before TS drops it.
  148. OlderThanHours int
  149. // BatchSize is the cap per SELECT/INSERT. Default
  150. // 10000. Each pass drains until the SELECT returns
  151. // < BatchSize rows, with a 100-cycle safety cap.
  152. BatchSize int
  153. // ClickHouseURL is the HTTP base URL for the CH
  154. // server (no trailing slash). Default
  155. // http://clickhouse:8123.
  156. ClickHouseURL string
  157. }
  158. // LoadRouterd reads routerd-specific config.
  159. func LoadRouterd() (Routerd, error) {
  160. c, err := LoadCommon("routerd")
  161. if err != nil {
  162. return Routerd{}, err
  163. }
  164. return Routerd{
  165. Common: c,
  166. DedupeFlushMs: GetInt("BA_ROUTERD_DEDUPE_FLUSH_MS", 2000),
  167. }, nil
  168. }
  169. // LoadArchiverd reads archiverd-specific config.
  170. func LoadArchiverd() (Archiverd, error) {
  171. c, err := LoadCommon("archiverd")
  172. if err != nil {
  173. return Archiverd{}, err
  174. }
  175. return Archiverd{
  176. Common: c,
  177. RunEverySeconds: GetInt("BA_ARCHIVERD_RUN_EVERY_SECONDS", 3600),
  178. OlderThanHours: GetInt("BA_ARCHIVERD_OLDER_THAN_HOURS", 168),
  179. BatchSize: GetInt("BA_ARCHIVERD_BATCH_SIZE", 10000),
  180. ClickHouseURL: envOr("BA_ARCHIVERD_CLICKHOUSE_URL", "http://clickhouse:8123"),
  181. }, nil
  182. }
  183. // LoadIngestd reads ingestd-specific config.
  184. func LoadIngestd() (Ingestd, error) {
  185. c, err := LoadCommon("ingestd")
  186. if err != nil {
  187. return Ingestd{}, err
  188. }
  189. return Ingestd{
  190. Common: c,
  191. MaxPayloadBytes: GetInt("BA_INGESTD_MAX_PAYLOAD_BYTES", 256*1024),
  192. RateLimitPerSource: GetInt("BA_INGESTD_RATE_LIMIT_PER_SOURCE", 100),
  193. RateLimitPerCompany: GetInt("BA_INGESTD_RATE_LIMIT_PER_COMPANY", 10_000),
  194. MaxConcurrentPerIP: GetInt("BA_INGESTD_MAX_CONCURRENT_PER_IP", 64),
  195. DedupeTTLSeconds: GetInt("BA_INGESTD_DEDUPE_TTL_SECONDS", 300),
  196. QuarantineHitsThreshold: GetInt("BA_INGESTD_QUARANTINE_HITS_THRESHOLD", 100),
  197. QuarantineWindowSeconds: GetInt("BA_INGESTD_QUARANTINE_WINDOW_SECONDS", 60),
  198. QuarantineDurationSecond: GetInt("BA_INGESTD_QUARANTINE_DURATION_SECONDS", 300),
  199. }, nil
  200. }