config.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. // M8: retry knobs shared by every deliverd-* binary.
  29. // Ingestd, routerd, archiverd, and admind ignore them
  30. // (they don't call retry.Run). Defaults match
  31. // internal/retry.Default() so the .env.example can
  32. // stay sparse.
  33. DeliverdMaxAttempts int
  34. DeliverdRetryBaseMs int
  35. DeliverdRetryMaxMs int
  36. DeliverdRetryBudgetMs int
  37. }
  38. // Default values applied if env unset.
  39. func defaultCommon() Common {
  40. return Common{
  41. Env: "dev",
  42. ServiceName: "broad-announce",
  43. LogLevel: "info",
  44. HTTPAddr: ":8800",
  45. NATSURL: envOr("BA_NATS_URL", "nats://localhost:4222"),
  46. PostgresDSN: envOr("BA_POSTGRES_DSN", "postgres://ba:ba@localhost:5432/ba?sslmode=disable"),
  47. RedisURL: envOr("BA_REDIS_URL", "redis://localhost:6379/0"),
  48. ShutdownGrace: 15 * time.Second,
  49. }
  50. }
  51. // LoadCommon reads env, applies defaults, and returns a validated Common.
  52. func LoadCommon(serviceName string) (Common, error) {
  53. c := defaultCommon()
  54. c.ServiceName = serviceName
  55. if v := os.Getenv("BA_ENV"); v != "" {
  56. c.Env = v
  57. }
  58. if v := os.Getenv("BA_LOG_LEVEL"); v != "" {
  59. c.LogLevel = v
  60. }
  61. if v := os.Getenv("BA_HTTP_ADDR"); v != "" {
  62. c.HTTPAddr = v
  63. }
  64. if v := os.Getenv("BA_NATS_URL"); v != "" {
  65. c.NATSURL = v
  66. }
  67. if v := os.Getenv("BA_POSTGRES_DSN"); v != "" {
  68. c.PostgresDSN = v
  69. }
  70. if v := os.Getenv("BA_REDIS_URL"); v != "" {
  71. c.RedisURL = v
  72. }
  73. if v := os.Getenv("BA_SHUTDOWN_GRACE_SEC"); v != "" {
  74. n, err := strconv.Atoi(v)
  75. if err != nil {
  76. return c, fmt.Errorf("BA_SHUTDOWN_GRACE_SEC: %w", err)
  77. }
  78. c.ShutdownGrace = time.Duration(n) * time.Second
  79. }
  80. // M8 retry knobs. Read on every LoadCommon so the
  81. // deliverd-* binaries don't need a separate config
  82. // struct just for these four values. Non-deliverd
  83. // services simply never call retry.Run.
  84. c.DeliverdMaxAttempts = GetInt("BA_DELIVERD_MAX_ATTEMPTS", 10)
  85. c.DeliverdRetryBaseMs = GetInt("BA_DELIVERD_RETRY_BASE_MS", 100)
  86. c.DeliverdRetryMaxMs = GetInt("BA_DELIVERD_RETRY_MAX_MS", 2000)
  87. c.DeliverdRetryBudgetMs = GetInt("BA_DELIVERD_RETRY_BUDGET_MS", 30000)
  88. if c.Env != "dev" && c.Env != "staging" && c.Env != "prod" {
  89. return c, fmt.Errorf("BA_ENV must be dev|staging|prod, got %q", c.Env)
  90. }
  91. return c, nil
  92. }
  93. func envOr(k, def string) string {
  94. if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
  95. return v
  96. }
  97. return def
  98. }
  99. // GetInt parses an env var as int, returns def if unset.
  100. func GetInt(k string, def int) int {
  101. v := os.Getenv(k)
  102. if v == "" {
  103. return def
  104. }
  105. n, err := strconv.Atoi(v)
  106. if err != nil {
  107. return def
  108. }
  109. return n
  110. }
  111. // GetDuration parses an env var as Go duration, returns def if unset.
  112. func GetDuration(k string, def time.Duration) time.Duration {
  113. v := os.Getenv(k)
  114. if v == "" {
  115. return def
  116. }
  117. d, err := time.ParseDuration(v)
  118. if err != nil {
  119. return def
  120. }
  121. return d
  122. }
  123. // Ingestd is ingestd-specific config. Kept here so the service
  124. // binary has one import.
  125. type Ingestd struct {
  126. Common
  127. // Source protection defaults (SPEC §22). Per-source overrides
  128. // come from the DB and override these.
  129. MaxPayloadBytes int
  130. RateLimitPerSource int
  131. RateLimitPerCompany int
  132. MaxConcurrentPerIP int
  133. QuarantineHitsThreshold int
  134. QuarantineWindowSeconds int
  135. QuarantineDurationSecond int
  136. // Circuit breaker (SPEC §22 layer 6). Trips when
  137. // CircuitFailureThreshold failures accumulate in
  138. // CircuitFailureWindowSeconds. Stays open for
  139. // CircuitOpenDurationSeconds, then admits up to
  140. // CircuitMaxHalfOpen test calls.
  141. CircuitFailureThreshold int
  142. CircuitFailureWindowSecs int
  143. CircuitOpenDurationSecs int
  144. CircuitMaxHalfOpen int
  145. // DedupeTTLSeconds is the M6 sliding-window TTL for a
  146. // dedupe entry. Refreshed on every duplicate observation,
  147. // so a steady stream of duplicates keeps the window alive.
  148. // Default 300s (5 min) — up from 60s in M0–M5 to give
  149. // operators a longer window to see `×N` rollups.
  150. DedupeTTLSeconds int
  151. }
  152. // Routerd is routerd-specific config.
  153. type Routerd struct {
  154. Common
  155. // DedupeFlushMs is the M6.5 router-level dedupe collapse
  156. // window. A burst of identical alerts is held for up to
  157. // this many ms, then a single delivery is fanned out
  158. // with the final dedupe_count. A continuous stream
  159. // re-flushes every DedupeFlushMs. Default 2000ms.
  160. DedupeFlushMs int
  161. }
  162. // Archiverd is archiverd-specific config (M7).
  163. type Archiverd struct {
  164. Common
  165. // RunEverySeconds is the cadence between archiverd
  166. // passes. Default 3600 (1 hour).
  167. RunEverySeconds int
  168. // OlderThanHours is the retention threshold; rows
  169. // older than now() - OlderThanHours are eligible
  170. // for archive. Default 168 (7 days). The Timescale
  171. // retention policy does the same at 7 days; the
  172. // archiver just runs ahead so CH has the data
  173. // before TS drops it.
  174. OlderThanHours int
  175. // BatchSize is the cap per SELECT/INSERT. Default
  176. // 10000. Each pass drains until the SELECT returns
  177. // < BatchSize rows, with a 100-cycle safety cap.
  178. BatchSize int
  179. // ClickHouseURL is the HTTP base URL for the CH
  180. // server (no trailing slash). Default
  181. // http://clickhouse:8123.
  182. ClickHouseURL string
  183. }
  184. // LoadRouterd reads routerd-specific config.
  185. func LoadRouterd() (Routerd, error) {
  186. c, err := LoadCommon("routerd")
  187. if err != nil {
  188. return Routerd{}, err
  189. }
  190. return Routerd{
  191. Common: c,
  192. DedupeFlushMs: GetInt("BA_ROUTERD_DEDUPE_FLUSH_MS", 2000),
  193. }, nil
  194. }
  195. // LoadArchiverd reads archiverd-specific config.
  196. func LoadArchiverd() (Archiverd, error) {
  197. c, err := LoadCommon("archiverd")
  198. if err != nil {
  199. return Archiverd{}, err
  200. }
  201. return Archiverd{
  202. Common: c,
  203. RunEverySeconds: GetInt("BA_ARCHIVERD_RUN_EVERY_SECONDS", 3600),
  204. OlderThanHours: GetInt("BA_ARCHIVERD_OLDER_THAN_HOURS", 168),
  205. BatchSize: GetInt("BA_ARCHIVERD_BATCH_SIZE", 10000),
  206. ClickHouseURL: envOr("BA_ARCHIVERD_CLICKHOUSE_URL", "http://clickhouse:8123"),
  207. }, nil
  208. }
  209. // LoadIngestd reads ingestd-specific config.
  210. func LoadIngestd() (Ingestd, error) {
  211. c, err := LoadCommon("ingestd")
  212. if err != nil {
  213. return Ingestd{}, err
  214. }
  215. return Ingestd{
  216. Common: c,
  217. MaxPayloadBytes: GetInt("BA_INGESTD_MAX_PAYLOAD_BYTES", 256*1024),
  218. RateLimitPerSource: GetInt("BA_INGESTD_RATE_LIMIT_PER_SOURCE", 100),
  219. RateLimitPerCompany: GetInt("BA_INGESTD_RATE_LIMIT_PER_COMPANY", 10_000),
  220. MaxConcurrentPerIP: GetInt("BA_INGESTD_MAX_CONCURRENT_PER_IP", 64),
  221. DedupeTTLSeconds: GetInt("BA_INGESTD_DEDUPE_TTL_SECONDS", 300),
  222. QuarantineHitsThreshold: GetInt("BA_INGESTD_QUARANTINE_HITS_THRESHOLD", 100),
  223. QuarantineWindowSeconds: GetInt("BA_INGESTD_QUARANTINE_WINDOW_SECONDS", 60),
  224. QuarantineDurationSecond: GetInt("BA_INGESTD_QUARANTINE_DURATION_SECONDS", 300),
  225. CircuitFailureThreshold: GetInt("BA_INGESTD_CB_FAILURE_THRESHOLD", 5),
  226. CircuitFailureWindowSecs: GetInt("BA_INGESTD_CB_FAILURE_WINDOW_SECS", 10),
  227. CircuitOpenDurationSecs: GetInt("BA_INGESTD_CB_OPEN_DURATION_SECS", 30),
  228. CircuitMaxHalfOpen: GetInt("BA_INGESTD_CB_MAX_HALF_OPEN", 1),
  229. }, nil
  230. }