metrics.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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. }
  49. // NewIngestdMetrics registers and returns the ingestd metrics.
  50. func NewIngestdMetrics(reg prometheus.Registerer, serviceName string) *IngestdMetrics {
  51. m := &IngestdMetrics{
  52. AlertsReceived: prometheus.NewCounterVec(prometheus.CounterOpts{
  53. Namespace: "ba",
  54. Subsystem: "ingestd",
  55. Name: "alerts_received_total",
  56. Help: "Number of inbound alerts by result.",
  57. ConstLabels: prometheus.Labels{"service": serviceName},
  58. }, []string{"result"}),
  59. PayloadBytes: prometheus.NewHistogram(prometheus.HistogramOpts{
  60. Namespace: "ba",
  61. Subsystem: "ingestd",
  62. Name: "payload_bytes",
  63. Help: "Accepted alert payload size in bytes.",
  64. Buckets: prometheus.ExponentialBuckets(64, 4, 8), // 64..1MB
  65. ConstLabels: prometheus.Labels{"service": serviceName},
  66. }),
  67. RateLimitHits: prometheus.NewCounterVec(prometheus.CounterOpts{
  68. Namespace: "ba",
  69. Subsystem: "ingestd",
  70. Name: "rate_limited_total",
  71. Help: "Rate-limit rejections by scope.",
  72. ConstLabels: prometheus.Labels{"service": serviceName},
  73. }, []string{"scope"}),
  74. Quarantines: prometheus.NewCounterVec(prometheus.CounterOpts{
  75. Namespace: "ba",
  76. Subsystem: "ingestd",
  77. Name: "source_quarantined_total",
  78. Help: "Source quarantines triggered.",
  79. ConstLabels: prometheus.Labels{"service": serviceName},
  80. }, []string{"source_id", "company_id"}),
  81. CBState: prometheus.NewGaugeVec(prometheus.GaugeOpts{
  82. Namespace: "ba",
  83. Subsystem: "ingestd",
  84. Name: "circuit_breaker_state",
  85. Help: "0=closed, 1=half_open, 2=open.",
  86. ConstLabels: prometheus.Labels{"service": serviceName},
  87. }, []string{"component"}),
  88. PublishLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
  89. Namespace: "ba",
  90. Subsystem: "ingestd",
  91. Name: "publish_latency_seconds",
  92. Help: "Time to publish an accepted alert to NATS.",
  93. Buckets: prometheus.DefBuckets,
  94. ConstLabels: prometheus.Labels{"service": serviceName},
  95. }),
  96. MQTTMessages: prometheus.NewCounterVec(prometheus.CounterOpts{
  97. Namespace: "ba",
  98. Subsystem: "ingestd",
  99. Name: "mqtt_messages_total",
  100. Help: "Inbound MQTT messages by result (M4).",
  101. ConstLabels: prometheus.Labels{"service": serviceName},
  102. }, []string{"result"}),
  103. WSMessages: prometheus.NewCounterVec(prometheus.CounterOpts{
  104. Namespace: "ba",
  105. Subsystem: "ingestd",
  106. Name: "ws_messages_total",
  107. Help: "Inbound WebSocket messages by result (M5).",
  108. ConstLabels: prometheus.Labels{"service": serviceName},
  109. }, []string{"result"}),
  110. WSConnections: prometheus.NewCounterVec(prometheus.CounterOpts{
  111. Namespace: "ba",
  112. Subsystem: "ingestd",
  113. Name: "ws_connections_total",
  114. Help: "WebSocket connection lifecycle events (M5).",
  115. ConstLabels: prometheus.Labels{"service": serviceName},
  116. }, []string{"state"}),
  117. ConnectionRejected: prometheus.NewCounterVec(prometheus.CounterOpts{
  118. Namespace: "ba",
  119. Subsystem: "ingestd",
  120. Name: "connection_rejected_total",
  121. Help: "Per-IP concurrency cap rejections (M5, SPEC §22 layer 2).",
  122. ConstLabels: prometheus.Labels{"service": serviceName},
  123. }, []string{"transport"}),
  124. TailSubscribers: prometheus.NewGauge(prometheus.GaugeOpts{
  125. Namespace: "ba",
  126. Subsystem: "ingestd",
  127. Name: "tail_subscribers",
  128. Help: "Current number of /v1/tail/ws clients.",
  129. ConstLabels: prometheus.Labels{"service": serviceName},
  130. }),
  131. TailDropped: prometheus.NewCounterVec(prometheus.CounterOpts{
  132. Namespace: "ba",
  133. Subsystem: "ingestd",
  134. Name: "tail_dropped_total",
  135. Help: "Tail events dropped because a subscriber was too slow.",
  136. ConstLabels: prometheus.Labels{"service": serviceName},
  137. }, []string{"reason"}),
  138. }
  139. reg.MustRegister(
  140. m.AlertsReceived,
  141. m.PayloadBytes,
  142. m.RateLimitHits,
  143. m.Quarantines,
  144. m.CBState,
  145. m.PublishLatency,
  146. m.MQTTMessages,
  147. m.WSMessages,
  148. m.WSConnections,
  149. m.ConnectionRejected,
  150. m.TailSubscribers,
  151. m.TailDropped,
  152. )
  153. m.AlertsReceived.WithLabelValues("accepted")
  154. return m
  155. }