metrics.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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.Histogram
  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. }
  61. // NewIngestdMetrics registers and returns the ingestd metrics.
  62. func NewIngestdMetrics(reg prometheus.Registerer, serviceName string) *IngestdMetrics {
  63. m := &IngestdMetrics{
  64. AlertsReceived: prometheus.NewCounterVec(prometheus.CounterOpts{
  65. Namespace: "ba",
  66. Subsystem: "ingestd",
  67. Name: "alerts_received_total",
  68. Help: "Number of inbound alerts by result.",
  69. ConstLabels: prometheus.Labels{"service": serviceName},
  70. }, []string{"result"}),
  71. PayloadBytes: prometheus.NewHistogram(prometheus.HistogramOpts{
  72. Namespace: "ba",
  73. Subsystem: "ingestd",
  74. Name: "payload_bytes",
  75. Help: "Accepted alert payload size in bytes.",
  76. Buckets: prometheus.ExponentialBuckets(64, 4, 8), // 64..1MB
  77. ConstLabels: prometheus.Labels{"service": serviceName},
  78. }),
  79. RateLimitHits: prometheus.NewCounterVec(prometheus.CounterOpts{
  80. Namespace: "ba",
  81. Subsystem: "ingestd",
  82. Name: "rate_limited_total",
  83. Help: "Rate-limit rejections by scope.",
  84. ConstLabels: prometheus.Labels{"service": serviceName},
  85. }, []string{"scope"}),
  86. Quarantines: prometheus.NewCounterVec(prometheus.CounterOpts{
  87. Namespace: "ba",
  88. Subsystem: "ingestd",
  89. Name: "source_quarantined_total",
  90. Help: "Source quarantines triggered.",
  91. ConstLabels: prometheus.Labels{"service": serviceName},
  92. }, []string{"source_id", "company_id"}),
  93. CBState: prometheus.NewGaugeVec(prometheus.GaugeOpts{
  94. Namespace: "ba",
  95. Subsystem: "ingestd",
  96. Name: "circuit_breaker_state",
  97. Help: "0=closed, 1=half_open, 2=open.",
  98. ConstLabels: prometheus.Labels{"service": serviceName},
  99. }, []string{"component"}),
  100. PublishLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
  101. Namespace: "ba",
  102. Subsystem: "ingestd",
  103. Name: "publish_latency_seconds",
  104. Help: "Time to publish an accepted alert to NATS.",
  105. Buckets: prometheus.DefBuckets,
  106. ConstLabels: prometheus.Labels{"service": serviceName},
  107. }),
  108. MQTTMessages: prometheus.NewCounterVec(prometheus.CounterOpts{
  109. Namespace: "ba",
  110. Subsystem: "ingestd",
  111. Name: "mqtt_messages_total",
  112. Help: "Inbound MQTT messages by result (M4).",
  113. ConstLabels: prometheus.Labels{"service": serviceName},
  114. }, []string{"result"}),
  115. WSMessages: prometheus.NewCounterVec(prometheus.CounterOpts{
  116. Namespace: "ba",
  117. Subsystem: "ingestd",
  118. Name: "ws_messages_total",
  119. Help: "Inbound WebSocket messages by result (M5).",
  120. ConstLabels: prometheus.Labels{"service": serviceName},
  121. }, []string{"result"}),
  122. WSConnections: prometheus.NewCounterVec(prometheus.CounterOpts{
  123. Namespace: "ba",
  124. Subsystem: "ingestd",
  125. Name: "ws_connections_total",
  126. Help: "WebSocket connection lifecycle events (M5).",
  127. ConstLabels: prometheus.Labels{"service": serviceName},
  128. }, []string{"state"}),
  129. ConnectionRejected: prometheus.NewCounterVec(prometheus.CounterOpts{
  130. Namespace: "ba",
  131. Subsystem: "ingestd",
  132. Name: "connection_rejected_total",
  133. Help: "Per-IP concurrency cap rejections (M5, SPEC §22 layer 2).",
  134. ConstLabels: prometheus.Labels{"service": serviceName},
  135. }, []string{"transport"}),
  136. TailSubscribers: prometheus.NewGauge(prometheus.GaugeOpts{
  137. Namespace: "ba",
  138. Subsystem: "ingestd",
  139. Name: "tail_subscribers",
  140. Help: "Current number of /v1/tail/ws clients.",
  141. ConstLabels: prometheus.Labels{"service": serviceName},
  142. }),
  143. TailDropped: prometheus.NewCounterVec(prometheus.CounterOpts{
  144. Namespace: "ba",
  145. Subsystem: "ingestd",
  146. Name: "tail_dropped_total",
  147. Help: "Tail events dropped because a subscriber was too slow.",
  148. ConstLabels: prometheus.Labels{"service": serviceName},
  149. }, []string{"reason"}),
  150. DedupeCollapsed: prometheus.NewCounterVec(prometheus.CounterOpts{
  151. Namespace: "ba",
  152. Subsystem: "ingestd",
  153. Name: "dedupe_collapsed_total",
  154. Help: "M6: alert messages that hit an existing dedupe key (isNew=false).",
  155. ConstLabels: prometheus.Labels{"service": serviceName},
  156. }, []string{"source"}),
  157. DedupeCountMax: prometheus.NewGaugeVec(prometheus.GaugeOpts{
  158. Namespace: "ba",
  159. Subsystem: "ingestd",
  160. Name: "dedupe_count_max_observed",
  161. Help: "M6: highest dedupe_count ever observed since process start, per source.",
  162. ConstLabels: prometheus.Labels{"service": serviceName},
  163. }, []string{"source"}),
  164. }
  165. reg.MustRegister(
  166. m.AlertsReceived,
  167. m.PayloadBytes,
  168. m.RateLimitHits,
  169. m.Quarantines,
  170. m.CBState,
  171. m.PublishLatency,
  172. m.MQTTMessages,
  173. m.WSMessages,
  174. m.WSConnections,
  175. m.ConnectionRejected,
  176. m.TailSubscribers,
  177. m.TailDropped,
  178. m.DedupeCollapsed,
  179. m.DedupeCountMax,
  180. )
  181. m.AlertsReceived.WithLabelValues("accepted")
  182. return m
  183. }
  184. // DeliverdMetrics groups the Prometheus counters/histograms for
  185. // the deliverd tier (SPEC §22 L3: delivery attempts + DLQ).
  186. // Both deliverd-fcm and deliverd-telegram share this type.
  187. type DeliverdMetrics struct {
  188. // DeliveryAttempts records each per-attempt delivery row.
  189. // channel=fcm|telegram, status=sent|failed.
  190. DeliveryAttempts *prometheus.CounterVec
  191. // DLQTotal records each time an alert is parked in the DLQ.
  192. // channel=fcm|telegram.
  193. DLQTotal *prometheus.CounterVec
  194. // DLQLatency records how long the retry budget lasted before
  195. // the alert hit the DLQ (wall-clock time from first attempt
  196. // to DLQ insert).
  197. DLQLatency prometheus.Histogram
  198. // RetryAttempts is the total number of retry loop iterations
  199. // across all alerts (sum of the attempts column on deliveries
  200. // rows that ended in DLQ).
  201. RetryAttempts *prometheus.CounterVec
  202. }
  203. // NewDeliverdMetrics registers and returns deliverd metrics.
  204. func NewDeliverdMetrics(reg prometheus.Registerer, serviceName string) *DeliverdMetrics {
  205. m := &DeliverdMetrics{
  206. DeliveryAttempts: prometheus.NewCounterVec(prometheus.CounterOpts{
  207. Namespace: "ba",
  208. Subsystem: "deliverd",
  209. Name: "delivery_attempts_total",
  210. Help: "Per-channel delivery attempt rows (one row per attempt).",
  211. ConstLabels: prometheus.Labels{"service": serviceName},
  212. }, []string{"channel", "status"}),
  213. DLQTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
  214. Namespace: "ba",
  215. Subsystem: "deliverd",
  216. Name: "dlq_total",
  217. Help: "Alerts parked in the DLQ (one per alert that exhausted retries).",
  218. ConstLabels: prometheus.Labels{"service": serviceName},
  219. }, []string{"channel"}),
  220. DLQLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
  221. Namespace: "ba",
  222. Subsystem: "deliverd",
  223. Name: "dlq_latency_seconds",
  224. Help: "Wall-clock time from first delivery attempt to DLQ insert.",
  225. ConstLabels: prometheus.Labels{"service": serviceName},
  226. Buckets: prometheus.ExponentialBuckets(0.1, 2, 10), // 100ms → ~100s
  227. }),
  228. RetryAttempts: prometheus.NewCounterVec(prometheus.CounterOpts{
  229. Namespace: "ba",
  230. Subsystem: "deliverd",
  231. Name: "retry_attempts_total",
  232. Help: "Total retry loop iterations across all DLQ'd alerts.",
  233. ConstLabels: prometheus.Labels{"service": serviceName},
  234. }, []string{"channel"}),
  235. }
  236. reg.MustRegister(m.DeliveryAttempts, m.DLQTotal, m.DLQLatency, m.RetryAttempts)
  237. return m
  238. }
  239. // RouterdMetrics groups the Prometheus histogram for the routerd tier
  240. // (SPEC §22 L1: recipient resolution latency).
  241. type RouterdMetrics struct {
  242. // RecipientExpansionLatency is the wall-clock time for
  243. // routing.Resolver.ResolveTargets to complete (DB call).
  244. RecipientExpansionLatency prometheus.Histogram
  245. }
  246. // NewRouterdMetrics registers and returns routerd metrics.
  247. func NewRouterdMetrics(reg prometheus.Registerer, serviceName string) *RouterdMetrics {
  248. m := &RouterdMetrics{
  249. RecipientExpansionLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
  250. Namespace: "ba",
  251. Subsystem: "routerd",
  252. Name: "recipient_expansion_seconds",
  253. Help: "Time to resolve recipients for an alert (DB call).",
  254. ConstLabels: prometheus.Labels{"service": serviceName},
  255. Buckets: prometheus.DefBuckets,
  256. }),
  257. }
  258. reg.MustRegister(m.RecipientExpansionLatency)
  259. return m
  260. }