config.go 8.0 KB

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