metrics.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. package observability
  2. import (
  3. "github.com/prometheus/client_golang/prometheus"
  4. )
  5. // NewRegistry returns a fresh Prometheus registry. Each service gets
  6. // its own so the metric labels are scoped correctly.
  7. func NewRegistry(serviceName string) (*prometheus.Registry, *IngestdMetrics) {
  8. reg := prometheus.NewRegistry()
  9. reg.MustRegister(
  10. prometheus.NewGoCollector(),
  11. prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}),
  12. )
  13. return reg, NewIngestdMetrics(reg, serviceName)
  14. }
  15. // IngestdMetrics groups the counters/histograms declared in SPEC §22
  16. // for the ingest tier. Other tiers get their own metric groups.
  17. type IngestdMetrics struct {
  18. AlertsReceived *prometheus.CounterVec // result=accepted|invalid|rate_limited|payload_too_large|quarantined|circuit_open
  19. PayloadBytes prometheus.Histogram
  20. RateLimitHits *prometheus.CounterVec // scope=source|company
  21. Quarantines *prometheus.CounterVec
  22. CBState *prometheus.GaugeVec
  23. PublishLatency *prometheus.HistogramVec // labels: source_id
  24. // MQTTMessages is the M4 per-message counter; labels mirror
  25. // the same result taxonomy as AlertsReceived (accepted,
  26. // deduped, bad_topic, bad_signature, unknown_source,
  27. // rate_limited_source, rate_limited_company, invalid,
  28. // invalid_json, broker_unavailable) plus a "received" label
  29. // for every message that survived parseIncomingTopic.
  30. MQTTMessages *prometheus.CounterVec
  31. // WSMessages is the M5 per-message counter; same result
  32. // taxonomy as MQTTMessages plus a "received" label for every
  33. // message that survived the post-auth frame read.
  34. WSMessages *prometheus.CounterVec
  35. // WSConnections tracks WS endpoint lifecycle. state ∈
  36. // {open, closed_clean, closed_protocol_error, closed_unauth,
  37. // closed_rate_limited, closed_per_ip_cap}.
  38. WSConnections *prometheus.CounterVec
  39. // ConnectionRejected tracks per-IP concurrency cap rejections.
  40. // transport ∈ {http, ws}.
  41. ConnectionRejected *prometheus.CounterVec
  42. // TailSubscribers is the current number of /v1/tail/ws
  43. // clients (gauge, not counter).
  44. TailSubscribers prometheus.Gauge
  45. // TailDropped tracks tail events dropped because a
  46. // subscriber's channel was full. The hub increments this.
  47. TailDropped *prometheus.CounterVec
  48. // DedupeCollapsed is the M6 per-message counter for
  49. // duplicate observations (isNew=false). Incremented on
  50. // every dedupe hit that finds an existing key. Lets
  51. // operators answer "how loud is the dupe noise?" without
  52. // parsing logs.
  53. DedupeCollapsed *prometheus.CounterVec
  54. // DedupeCountMax is the M6 per-source gauge of the
  55. // highest dedupe_count ever observed since process start.
  56. // source label is the source_id. Helps dashboards alert
  57. // on multi-hundred duplicates (e.g. a misconfigured
  58. // Prometheus rule that loops every 100ms).
  59. DedupeCountMax *prometheus.GaugeVec
  60. // --- gRPC transport (M11) ---
  61. StreamsActive prometheus.Gauge // ba_ingestd_grpc_streams_active
  62. GRPCInflight *prometheus.HistogramVec // ba_ingestd_grpc_inflight_per_stream{source_id}
  63. GRPCRateLimited *prometheus.CounterVec // ba_ingestd_grpc_rate_limited_total{source_id}
  64. GRPCAckLatency *prometheus.HistogramVec // ba_ingestd_grpc_ack_latency_seconds{source_id}
  65. }
  66. // NewIngestdMetrics registers and returns the ingestd metrics.
  67. func NewIngestdMetrics(reg prometheus.Registerer, serviceName string) *IngestdMetrics {
  68. m := &IngestdMetrics{
  69. AlertsReceived: prometheus.NewCounterVec(prometheus.CounterOpts{
  70. Namespace: "ba",
  71. Subsystem: "ingestd",
  72. Name: "alerts_received_total",
  73. Help: "Number of inbound alerts by result and transport.",
  74. ConstLabels: prometheus.Labels{"service": serviceName},
  75. }, []string{"transport", "result"}),
  76. PayloadBytes: prometheus.NewHistogram(prometheus.HistogramOpts{
  77. Namespace: "ba",
  78. Subsystem: "ingestd",
  79. Name: "payload_bytes",
  80. Help: "Accepted alert payload size in bytes.",
  81. Buckets: prometheus.ExponentialBuckets(64, 4, 8), // 64..1MB
  82. ConstLabels: prometheus.Labels{"service": serviceName},
  83. }),
  84. RateLimitHits: prometheus.NewCounterVec(prometheus.CounterOpts{
  85. Namespace: "ba",
  86. Subsystem: "ingestd",
  87. Name: "rate_limited_total",
  88. Help: "Rate-limit rejections by scope.",
  89. ConstLabels: prometheus.Labels{"service": serviceName},
  90. }, []string{"scope"}),
  91. Quarantines: prometheus.NewCounterVec(prometheus.CounterOpts{
  92. Namespace: "ba",
  93. Subsystem: "ingestd",
  94. Name: "source_quarantined_total",
  95. Help: "Source quarantines triggered.",
  96. ConstLabels: prometheus.Labels{"service": serviceName},
  97. }, []string{"source_id", "company_id"}),
  98. CBState: prometheus.NewGaugeVec(prometheus.GaugeOpts{
  99. Namespace: "ba",
  100. Subsystem: "ingestd",
  101. Name: "circuit_breaker_state",
  102. Help: "0=closed, 1=half_open, 2=open.",
  103. ConstLabels: prometheus.Labels{"service": serviceName},
  104. }, []string{"component"}),
  105. PublishLatency: prometheus.NewHistogramVec(prometheus.HistogramOpts{
  106. Namespace: "ba",
  107. Subsystem: "ingestd",
  108. Name: "publish_latency_seconds",
  109. Help: "Time to publish an accepted alert to NATS.",
  110. Buckets: prometheus.DefBuckets,
  111. ConstLabels: prometheus.Labels{"service": serviceName},
  112. }, []string{"source_id"}),
  113. MQTTMessages: prometheus.NewCounterVec(prometheus.CounterOpts{
  114. Namespace: "ba",
  115. Subsystem: "ingestd",
  116. Name: "mqtt_messages_total",
  117. Help: "Inbound MQTT messages by result (M4).",
  118. ConstLabels: prometheus.Labels{"service": serviceName},
  119. }, []string{"result"}),
  120. WSMessages: prometheus.NewCounterVec(prometheus.CounterOpts{
  121. Namespace: "ba",
  122. Subsystem: "ingestd",
  123. Name: "ws_messages_total",
  124. Help: "Inbound WebSocket messages by result (M5).",
  125. ConstLabels: prometheus.Labels{"service": serviceName},
  126. }, []string{"result"}),
  127. WSConnections: prometheus.NewCounterVec(prometheus.CounterOpts{
  128. Namespace: "ba",
  129. Subsystem: "ingestd",
  130. Name: "ws_connections_total",
  131. Help: "WebSocket connection lifecycle events (M5).",
  132. ConstLabels: prometheus.Labels{"service": serviceName},
  133. }, []string{"state"}),
  134. ConnectionRejected: prometheus.NewCounterVec(prometheus.CounterOpts{
  135. Namespace: "ba",
  136. Subsystem: "ingestd",
  137. Name: "connection_rejected_total",
  138. Help: "Per-IP concurrency cap rejections (M5, SPEC §22 layer 2).",
  139. ConstLabels: prometheus.Labels{"service": serviceName},
  140. }, []string{"transport"}),
  141. TailSubscribers: prometheus.NewGauge(prometheus.GaugeOpts{
  142. Namespace: "ba",
  143. Subsystem: "ingestd",
  144. Name: "tail_subscribers",
  145. Help: "Current number of /v1/tail/ws clients.",
  146. ConstLabels: prometheus.Labels{"service": serviceName},
  147. }),
  148. TailDropped: prometheus.NewCounterVec(prometheus.CounterOpts{
  149. Namespace: "ba",
  150. Subsystem: "ingestd",
  151. Name: "tail_dropped_total",
  152. Help: "Tail events dropped because a subscriber was too slow.",
  153. ConstLabels: prometheus.Labels{"service": serviceName},
  154. }, []string{"reason"}),
  155. DedupeCollapsed: prometheus.NewCounterVec(prometheus.CounterOpts{
  156. Namespace: "ba",
  157. Subsystem: "ingestd",
  158. Name: "dedupe_collapsed_total",
  159. Help: "M6: alert messages that hit an existing dedupe key (isNew=false).",
  160. ConstLabels: prometheus.Labels{"service": serviceName},
  161. }, []string{"source"}),
  162. DedupeCountMax: prometheus.NewGaugeVec(prometheus.GaugeOpts{
  163. Namespace: "ba",
  164. Subsystem: "ingestd",
  165. Name: "dedupe_count_max_observed",
  166. Help: "M6: highest dedupe_count ever observed since process start, per source.",
  167. ConstLabels: prometheus.Labels{"service": serviceName},
  168. }, []string{"source"}),
  169. // gRPC transport (M11)
  170. StreamsActive: prometheus.NewGauge(prometheus.GaugeOpts{
  171. Namespace: "ba",
  172. Subsystem: "ingestd",
  173. Name: "grpc_streams_active",
  174. Help: "Number of currently open gRPC StreamAlerts streams.",
  175. ConstLabels: prometheus.Labels{"service": serviceName},
  176. }),
  177. GRPCInflight: prometheus.NewHistogramVec(prometheus.HistogramOpts{
  178. Namespace: "ba",
  179. Subsystem: "ingestd",
  180. Name: "grpc_inflight_per_stream",
  181. Help: "Messages currently being processed per gRPC stream.",
  182. Buckets: []float64{1, 8, 16, 32, 64, 128, 256, 512},
  183. ConstLabels: prometheus.Labels{"service": serviceName},
  184. }, []string{"source_id"}),
  185. GRPCRateLimited: prometheus.NewCounterVec(prometheus.CounterOpts{
  186. Namespace: "ba",
  187. Subsystem: "ingestd",
  188. Name: "grpc_rate_limited_total",
  189. Help: "RATE_LIMITED Acks sent to gRPC streams.",
  190. ConstLabels: prometheus.Labels{"service": serviceName},
  191. }, []string{"source_id"}),
  192. GRPCAckLatency: prometheus.NewHistogramVec(prometheus.HistogramOpts{
  193. Namespace: "ba",
  194. Subsystem: "ingestd",
  195. Name: "grpc_ack_latency_seconds",
  196. Help: "Server-side Ack latency for gRPC StreamAlerts (seconds).",
  197. Buckets: []float64{0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500, 1.0},
  198. ConstLabels: prometheus.Labels{"service": serviceName},
  199. }, []string{"source_id"}),
  200. }
  201. reg.MustRegister(
  202. m.AlertsReceived,
  203. m.PayloadBytes,
  204. m.RateLimitHits,
  205. m.Quarantines,
  206. m.CBState,
  207. m.PublishLatency,
  208. m.MQTTMessages,
  209. m.WSMessages,
  210. m.WSConnections,
  211. m.ConnectionRejected,
  212. m.TailSubscribers,
  213. m.TailDropped,
  214. m.DedupeCollapsed,
  215. m.DedupeCountMax,
  216. // gRPC transport (M11)
  217. m.StreamsActive,
  218. m.GRPCInflight,
  219. m.GRPCRateLimited,
  220. m.GRPCAckLatency,
  221. )
  222. m.AlertsReceived.WithLabelValues("internal", "accepted")
  223. return m
  224. }
  225. // DeliverdMetrics groups the Prometheus counters/histograms for
  226. // the deliverd tier (SPEC §22 L3: delivery attempts + DLQ).
  227. // Both deliverd-fcm and deliverd-telegram share this type.
  228. type DeliverdMetrics struct {
  229. // DeliveryAttempts records each per-attempt delivery row.
  230. // channel=fcm|telegram, status=sent|failed.
  231. DeliveryAttempts *prometheus.CounterVec
  232. // DLQTotal records each time an alert is parked in the DLQ.
  233. // channel=fcm|telegram.
  234. DLQTotal *prometheus.CounterVec
  235. // DLQLatency records how long the retry budget lasted before
  236. // the alert hit the DLQ (wall-clock time from first attempt
  237. // to DLQ insert).
  238. DLQLatency prometheus.Histogram
  239. // RetryAttempts is the total number of retry loop iterations
  240. // across all alerts (sum of the attempts column on deliveries
  241. // rows that ended in DLQ).
  242. RetryAttempts *prometheus.CounterVec
  243. }
  244. // NewDeliverdMetrics registers and returns deliverd metrics.
  245. func NewDeliverdMetrics(reg prometheus.Registerer, serviceName string) *DeliverdMetrics {
  246. m := &DeliverdMetrics{
  247. DeliveryAttempts: prometheus.NewCounterVec(prometheus.CounterOpts{
  248. Namespace: "ba",
  249. Subsystem: "deliverd",
  250. Name: "delivery_attempts_total",
  251. Help: "Per-channel delivery attempt rows (one row per attempt).",
  252. ConstLabels: prometheus.Labels{"service": serviceName},
  253. }, []string{"channel", "status"}),
  254. DLQTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
  255. Namespace: "ba",
  256. Subsystem: "deliverd",
  257. Name: "dlq_total",
  258. Help: "Alerts parked in the DLQ (one per alert that exhausted retries).",
  259. ConstLabels: prometheus.Labels{"service": serviceName},
  260. }, []string{"channel"}),
  261. DLQLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
  262. Namespace: "ba",
  263. Subsystem: "deliverd",
  264. Name: "dlq_latency_seconds",
  265. Help: "Wall-clock time from first delivery attempt to DLQ insert.",
  266. ConstLabels: prometheus.Labels{"service": serviceName},
  267. Buckets: prometheus.ExponentialBuckets(0.1, 2, 10), // 100ms → ~100s
  268. }),
  269. RetryAttempts: prometheus.NewCounterVec(prometheus.CounterOpts{
  270. Namespace: "ba",
  271. Subsystem: "deliverd",
  272. Name: "retry_attempts_total",
  273. Help: "Total retry loop iterations across all DLQ'd alerts.",
  274. ConstLabels: prometheus.Labels{"service": serviceName},
  275. }, []string{"channel"}),
  276. }
  277. reg.MustRegister(m.DeliveryAttempts, m.DLQTotal, m.DLQLatency, m.RetryAttempts)
  278. return m
  279. }
  280. // RouterdMetrics groups the Prometheus histogram for the routerd tier
  281. // (SPEC §22 L1: recipient resolution latency).
  282. type RouterdMetrics struct {
  283. // RecipientExpansionLatency is the wall-clock time for
  284. // routing.Resolver.ResolveTargets to complete (DB call).
  285. RecipientExpansionLatency prometheus.Histogram
  286. }
  287. // NewRouterdMetrics registers and returns routerd metrics.
  288. func NewRouterdMetrics(reg prometheus.Registerer, serviceName string) *RouterdMetrics {
  289. m := &RouterdMetrics{
  290. RecipientExpansionLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
  291. Namespace: "ba",
  292. Subsystem: "routerd",
  293. Name: "recipient_expansion_seconds",
  294. Help: "Time to resolve recipients for an alert (DB call).",
  295. ConstLabels: prometheus.Labels{"service": serviceName},
  296. Buckets: prometheus.DefBuckets,
  297. }),
  298. }
  299. reg.MustRegister(m.RecipientExpansionLatency)
  300. return m
  301. }