-
-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathmetrics.go
More file actions
1702 lines (1517 loc) · 61.3 KB
/
Copy pathmetrics.go
File metadata and controls
1702 lines (1517 loc) · 61.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package centrifuge
import (
"errors"
"strconv"
"strings"
"sync"
"time"
"github.com/centrifugal/protocol"
"github.com/maypok86/otter/v2"
"github.com/prometheus/client_golang/prometheus"
)
// default namespace for prometheus metrics. Can be changed over Config.
var defaultMetricsNamespace = "centrifuge"
var registryMu sync.RWMutex
// clientMetricDef defines a Prometheus metric with its subsystem and name.
type clientMetricDef struct {
Subsystem string
Name string
}
// Client metric definitions.
var (
metricClientConnectionsAccepted = clientMetricDef{
Subsystem: "client",
Name: "connections_accepted",
}
metricClientConnectionsInflight = clientMetricDef{
Subsystem: "client",
Name: "connections_inflight",
}
metricClientSubscriptionsAccepted = clientMetricDef{
Subsystem: "client",
Name: "subscriptions_accepted",
}
metricClientSubscriptionsInflight = clientMetricDef{
Subsystem: "client",
Name: "subscriptions_inflight",
}
metricClientCommandDuration = clientMetricDef{
Subsystem: "client",
Name: "command_duration_seconds",
}
metricClientNumReplyErrors = clientMetricDef{
Subsystem: "client",
Name: "num_reply_errors",
}
metricClientNumServerUnsubscribes = clientMetricDef{
Subsystem: "client",
Name: "num_server_unsubscribes",
}
metricClientNumServerDisconnects = clientMetricDef{
Subsystem: "client",
Name: "num_server_disconnects",
}
metricTransportMessagesSent = clientMetricDef{
Subsystem: "transport",
Name: "messages_sent",
}
metricTransportMessagesSentSize = clientMetricDef{
Subsystem: "transport",
Name: "messages_sent_size",
}
metricTransportMessagesReceived = clientMetricDef{
Subsystem: "transport",
Name: "messages_received",
}
metricTransportMessagesReceivedSize = clientMetricDef{
Subsystem: "transport",
Name: "messages_received_size",
}
metricTransportOutgoingClose = clientMetricDef{
Subsystem: "transport",
Name: "outgoing_close_count",
}
)
type metrics struct {
messagesSentCount *prometheus.CounterVec
messagesReceivedCount *prometheus.CounterVec
actionCount *prometheus.CounterVec
buildInfoGauge *prometheus.GaugeVec
numClientsGauge prometheus.Gauge
numUsersGauge prometheus.Gauge
numSubsGauge prometheus.Gauge
numChannelsGauge prometheus.Gauge
numNodesGauge prometheus.Gauge
replyErrorCount *prometheus.CounterVec
connectionsAccepted *prometheus.CounterVec
connectionsInflight *prometheus.GaugeVec
subscriptionsAccepted *prometheus.CounterVec
subscriptionsInflight *prometheus.GaugeVec
serverUnsubscribeCount *prometheus.CounterVec
serverDisconnectCount *prometheus.CounterVec
transportOutgoingCloseCount *prometheus.CounterVec
// commandDurationSummary holds the legacy Summary by default; when
// EnableNativeHistograms is true it is a no-op (the Summary is not
// exposed). The companion commandDurationHistogram below always carries
// the real observations, with native schema when the flag is on.
commandDurationSummary prometheus.ObserverVec
commandDurationHistogram *prometheus.HistogramVec
surveyDurationSummary prometheus.ObserverVec
surveyDurationHistogram *prometheus.HistogramVec
recoverCount *prometheus.CounterVec
recoveredPublications *prometheus.HistogramVec
transportMessagesSent *prometheus.CounterVec
transportMessagesSentSize *prometheus.CounterVec
transportMessagesReceived *prometheus.CounterVec
transportMessagesReceivedSize *prometheus.CounterVec
tagsFilterDroppedCount *prometheus.CounterVec
messagesReceivedCountPublication prometheus.Counter
messagesReceivedCountJoin prometheus.Counter
messagesReceivedCountLeave prometheus.Counter
messagesReceivedCountControl prometheus.Counter
messagesSentCountPublication prometheus.Counter
messagesSentCountJoin prometheus.Counter
messagesSentCountLeave prometheus.Counter
messagesSentCountControl prometheus.Counter
commandDurationConnect prometheus.Observer
commandDurationSubscribe prometheus.Observer
commandDurationUnsubscribe prometheus.Observer
commandDurationPublish prometheus.Observer
commandDurationPresence prometheus.Observer
commandDurationPresenceStats prometheus.Observer
commandDurationHistory prometheus.Observer
commandDurationSend prometheus.Observer
commandDurationRPC prometheus.Observer
commandDurationRefresh prometheus.Observer
commandDurationSubRefresh prometheus.Observer
commandDurationUnknown prometheus.Observer
broadcastDurationHistogram *prometheus.HistogramVec
pubSubLagHistogram *prometheus.HistogramVec
pingPongDurationHistogram *prometheus.HistogramVec
brokerPublishSuppressedCount *prometheus.CounterVec
mapBrokerPublishSuppressedCount *prometheus.CounterVec
mapBrokerRemoveSuppressedCount *prometheus.CounterVec
mapBrokerCleanupLag *prometheus.GaugeVec
mapBrokerCleanupRemoved *prometheus.CounterVec
mapBrokerCleanupErrors *prometheus.CounterVec
redisBrokerPubSubErrors *prometheus.CounterVec
redisBrokerPubSubDroppedMessages *prometheus.CounterVec
redisBrokerPubSubBufferedMessages *prometheus.GaugeVec
// brokerPubSub and mapBrokerPubSub bundle the Redis PUB/SUB metric vectors per
// broker kind so the shared pub/sub loop reports under each broker's own
// subsystem: broker_* for RedisBroker, map_broker_* for RedisMapBroker.
brokerPubSub redisPubSubMetrics
mapBrokerPubSub redisPubSubMetrics
// Shared poll metrics.
sharedPollCycleDurationHistogram *prometheus.HistogramVec
sharedPollCycleWorkDurationHistogram *prometheus.HistogramVec
sharedPollHandlerDurationHistogram *prometheus.HistogramVec
sharedPollSemWaitDurationHistogram *prometheus.HistogramVec
sharedPollHandlerErrorCount *prometheus.CounterVec
sharedPollItemsCount *prometheus.CounterVec
sharedPollNotifyCount *prometheus.CounterVec
sharedPollDroppedNotifyCount *prometheus.CounterVec
sharedPollPublishCount *prometheus.CounterVec
sharedPollNumChannelsGauge prometheus.Gauge
sharedPollNumKeysGauge prometheus.Gauge
config MetricsConfig
transportMessagesSentCache sync.Map
transportMessagesReceivedCache sync.Map
commandDurationCache sync.Map
replyErrorCache sync.Map
actionCache sync.Map
recoverCache sync.Map
unsubscribeCache sync.Map
disconnectCache sync.Map
messagesSentCache sync.Map
messagesReceivedCache sync.Map
tagsFilterDroppedCache sync.Map
brokerPublishSuppressedCache sync.Map
mapBrokerPublishSuppressedCache sync.Map
mapBrokerRemoveSuppressedCache sync.Map
pubSubLagCache sync.Map
broadcastDurationCache sync.Map
sharedPollHandlerCache sync.Map
sharedPollResultCache sync.Map
sharedPollChannelCache sync.Map
sharedPollPublishCache sync.Map
nsCache *otter.Cache[string, string]
codeStrings map[uint32]string
// Cache for client label combinations: maps cache key -> {labelValues, cacheKey}
// This allows sharing pre-computed label data across all clients with the same label values
clientLabelCombinationsCache sync.Map // map[string]*clientLabelCombination
}
// clientLabelCombination holds pre-computed label values and cache key for a unique combination
type clientLabelCombination struct {
labelValues []string
cacheKey string
}
func getMetricsNamespace(config MetricsConfig) string {
if config.MetricsNamespace == "" {
return defaultMetricsNamespace
}
return config.MetricsNamespace
}
// clientLabelPrefix is prepended to every client-label name exported as a
// Prometheus dimension. It guarantees that user-chosen names in
// MetricsConfig.ClientLabels can never collide with built-in metric labels
// like "transport", "code", "method", etc.
const clientLabelPrefix = "app_"
// buildMetricLabels creates a label slice, optionally appending client labels if enabled.
// Exported client-label names are prefixed with clientLabelPrefix.
func (m *metrics) buildMetricLabels(baseLabels []string) []string {
if len(m.config.ClientLabels) == 0 {
return baseLabels
}
labels := make([]string, 0, len(baseLabels)+len(m.config.ClientLabels))
labels = append(labels, baseLabels...)
for _, name := range m.config.ClientLabels {
labels = append(labels, clientLabelPrefix+name)
}
return labels
}
// appendClientLabels appends client label values to base labels if client labels are enabled.
// Returns a new slice with client labels appended, or the original base labels if disabled.
func (m *metrics) appendClientLabels(baseLabels []string, c *Client) []string {
if len(m.config.ClientLabels) == 0 {
return baseLabels
}
clientLabelValues := m.extractClientLabelValues(c)
if clientLabelValues != nil {
result := make([]string, len(baseLabels)+len(clientLabelValues))
copy(result, baseLabels)
copy(result[len(baseLabels):], clientLabelValues)
return result
}
// Append empty strings for missing client labels to match metric definition
result := make([]string, len(baseLabels)+len(m.config.ClientLabels))
copy(result, baseLabels)
return result
}
// getOrCreateClientLabelCombinationFromLabels returns a cached combination for the given labels map.
// This is used during client connect to precompute and cache the combination.
func (m *metrics) getOrCreateClientLabelCombinationFromLabels(labels map[string]string) *clientLabelCombination {
if len(m.config.ClientLabels) == 0 {
return nil
}
// Build cache key directly from the map to check if it's already cached
cacheKey := buildClientLabelsCacheKeyFromMap(m.config.ClientLabels, labels)
// Try to load existing combination from global cache
if combo, ok := m.clientLabelCombinationsCache.Load(cacheKey); ok {
return combo.(*clientLabelCombination)
}
// Not cached - now build the values slice (only done once per unique combination)
labelValues := make([]string, len(m.config.ClientLabels))
if labels != nil {
for i, label := range m.config.ClientLabels {
labelValues[i] = labels[label]
}
}
// Create new combination
combo := &clientLabelCombination{
labelValues: labelValues,
cacheKey: cacheKey,
}
// Store in global cache (even if another goroutine stored it first, we'll use theirs)
actual, _ := m.clientLabelCombinationsCache.LoadOrStore(cacheKey, combo)
return actual.(*clientLabelCombination)
}
// getCachedClientLabelCombination returns the cached combination for the given client.
// The combination is pre-cached during client connect. This is used only in non-hot paths
// and for tests. Hot paths should call c.labelCombinationCached.Load() directly.
func (m *metrics) getCachedClientLabelCombination(c *Client) *clientLabelCombination {
if len(m.config.ClientLabels) == 0 || c == nil {
return nil
}
// Load pre-cached combination from client (set during connect)
if cached := c.labelCombinationCached.Load(); cached != nil {
return cached
}
// Fallback: shouldn't happen in normal flow, but handle gracefully
// This can happen if metrics are recorded before client is fully connected or in tests
return nil
}
// extractClientLabelValues extracts client label values from a client, returning empty strings for missing labels.
// This is a helper for non-hot-path uses. For hot paths, call c.labelCombinationCached.Load() directly.
func (m *metrics) extractClientLabelValues(c *Client) []string {
if len(m.config.ClientLabels) == 0 || c == nil {
return nil
}
// Try to get the cached combination first
combo := m.getCachedClientLabelCombination(c)
if combo != nil {
return combo.labelValues
}
// Fallback: client doesn't have combination cached (e.g., in tests)
// Build label values directly from client.labels map
// Note: c.labels is set once during connect and never modified, so safe to read without lock
values := make([]string, len(m.config.ClientLabels))
if c.labels != nil {
for i, label := range m.config.ClientLabels {
values[i] = c.labels[label]
}
}
return values
}
// buildClientLabelsCacheKey builds a cache key from client label values without allocations.
// Uses strings.Builder with pre-sized buffer to minimize allocations.
func buildClientLabelsCacheKey(values []string) string {
if len(values) == 0 {
return ""
}
// Pre-calculate size to avoid Builder growth allocations
size := 0
for _, v := range values {
size += len(v) + 1 // +1 for separator
}
var b strings.Builder
b.Grow(size)
for i, val := range values {
if i > 0 {
b.WriteByte(0) // Use null byte as separator
}
b.WriteString(val)
}
return b.String()
}
// buildClientLabelsCacheKeyFromMap builds a cache key directly from a labels map.
// This avoids allocating the intermediate values slice.
func buildClientLabelsCacheKeyFromMap(labelNames []string, labelsMap map[string]string) string {
if len(labelNames) == 0 {
return ""
}
// Pre-calculate size to avoid Builder growth allocations
size := len(labelNames) - 1 // separators
if labelsMap != nil {
for _, name := range labelNames {
size += len(labelsMap[name])
}
}
var b strings.Builder
b.Grow(size)
for i, name := range labelNames {
if i > 0 {
b.WriteByte(0) // Use null byte as separator
}
if labelsMap != nil {
b.WriteString(labelsMap[name])
}
}
return b.String()
}
// dualObserver fans observations out to both a Summary and a Histogram
// observer. Used when a metric is exposed as both instrument types — the
// Summary preserves existing {quantile="..."} dashboards while the Histogram
// supplies the histogram_quantile()- and OpenTelemetry-friendly form. When
// EnableNativeHistograms is true the Summary side is a no-op, so only the
// Histogram records data.
type dualObserver struct {
summary, histogram prometheus.Observer
}
func (d dualObserver) Observe(v float64) {
d.summary.Observe(v)
d.histogram.Observe(v)
}
// noopObserverVec implements prometheus.ObserverVec with all no-op methods.
// Assigned to a Summary accessor when EnableNativeHistograms is true so the
// Summary side of a dual-instrument metric is not exposed and contributes no
// observation cost. Callers that cache observers via WithLabelValues do not
// need nil-checks — they get a noopObserver that silently drops Observe()
// calls.
type noopObserverVec struct{}
func (noopObserverVec) Describe(chan<- *prometheus.Desc) {}
func (noopObserverVec) Collect(chan<- prometheus.Metric) {}
func (noopObserverVec) WithLabelValues(...string) prometheus.Observer { return noopObserver{} }
func (noopObserverVec) With(prometheus.Labels) prometheus.Observer { return noopObserver{} }
func (noopObserverVec) GetMetricWith(prometheus.Labels) (prometheus.Observer, error) {
return noopObserver{}, nil
}
func (noopObserverVec) GetMetricWithLabelValues(...string) (prometheus.Observer, error) {
return noopObserver{}, nil
}
func (noopObserverVec) CurryWith(prometheus.Labels) (prometheus.ObserverVec, error) {
return noopObserverVec{}, nil
}
func (noopObserverVec) MustCurryWith(prometheus.Labels) prometheus.ObserverVec {
return noopObserverVec{}
}
type noopObserver struct{}
func (noopObserver) Observe(float64) {}
// nativeHistogramOpts returns opts unchanged when native is false. When true,
// it enables Prometheus native histogram schema with no explicit buckets —
// the metric exposes only _count, _sum, and the native histogram chunk.
func nativeHistogramOpts(opts prometheus.HistogramOpts, native bool) prometheus.HistogramOpts {
if !native {
return opts
}
opts.Buckets = nil
opts.NativeHistogramBucketFactor = 1.1
opts.NativeHistogramMaxBucketNumber = 200
opts.NativeHistogramMinResetDuration = time.Hour
return opts
}
func newMetricsRegistry(config MetricsConfig) (*metrics, error) {
registryMu.Lock()
defer registryMu.Unlock()
metricsNamespace := getMetricsNamespace(config)
var registerer prometheus.Registerer
if config.RegistererGatherer != nil {
registerer = config.RegistererGatherer
} else {
registerer = prometheus.DefaultRegisterer
}
var nsCache *otter.Cache[string, string]
if config.GetChannelNamespaceLabel != nil {
cacheSize := config.ChannelNamespaceCacheSize
if cacheSize == 0 {
cacheSize = 4096
}
cacheTTL := config.ChannelNamespaceCacheTTL
if cacheTTL == 0 {
cacheTTL = 15 * time.Second
}
if cacheTTL < 0 {
return nil, errors.New("channel namespace cache TTL must be positive")
}
if cacheSize != -1 {
nsCache = otter.Must(&otter.Options[string, string]{
MaximumSize: cacheSize,
ExpiryCalculator: otter.ExpiryWriting[string, string](cacheTTL),
})
}
}
codeStrings := make(map[uint32]string)
for i := uint32(0); i <= 5000; i++ {
codeStrings[i] = strconv.FormatUint(uint64(i), 10)
}
m := &metrics{
config: config,
nsCache: nsCache,
codeStrings: codeStrings,
}
m.actionCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "action_count",
Help: "Number of various actions called.",
}, []string{"action", "channel_namespace"})
m.numClientsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "num_clients",
Help: "Number of clients connected.",
})
m.numUsersGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "num_users",
Help: "Number of unique users connected.",
})
m.numSubsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "num_subscriptions",
Help: "Number of subscriptions.",
})
m.numNodesGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "num_nodes",
Help: "Number of nodes in the cluster.",
})
m.buildInfoGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "build",
Help: "Node build info.",
}, []string{"version"})
m.numChannelsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "num_channels",
Help: "Number of channels with one or more subscribers.",
})
if config.EnableNativeHistograms {
m.surveyDurationSummary = noopObserverVec{}
} else {
m.surveyDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "survey_duration_seconds",
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
Help: "DEPRECATED — use survey_duration_seconds_histogram. Will be removed in future releases. Survey duration summary.",
}, []string{"op"})
}
m.surveyDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "survey_duration_seconds_histogram",
Help: "Survey duration histogram. Use for histogram_quantile() and OpenTelemetry export.",
Buckets: []float64{
0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500,
1.0, 2.5, 5.0, 10.0,
},
}, config.EnableNativeHistograms), []string{"op"})
m.messagesSentCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "messages_sent_count",
Help: "Number of messages sent by node to broker.",
}, []string{"type", "channel_namespace"})
m.messagesReceivedCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "messages_received_count",
Help: "Number of messages received from broker.",
}, []string{"type", "channel_namespace"})
if config.EnableNativeHistograms {
m.commandDurationSummary = noopObserverVec{}
} else {
m.commandDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: metricsNamespace,
Subsystem: metricClientCommandDuration.Subsystem,
Name: metricClientCommandDuration.Name,
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
Help: "DEPRECATED — use command_duration_seconds_histogram. Will be removed in future releases. Client command duration summary.",
}, m.buildMetricLabels([]string{"method", "channel_namespace"}))
}
m.commandDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: metricClientCommandDuration.Subsystem,
Name: metricClientCommandDuration.Name + "_histogram",
Help: "Client command duration histogram. Use for histogram_quantile() and OpenTelemetry export.",
Buckets: []float64{
0.000100, 0.000250, 0.000500,
0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500,
1.0, 2.5, 5.0, 10.0,
},
}, config.EnableNativeHistograms), m.buildMetricLabels([]string{"method", "channel_namespace"}))
m.replyErrorCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricClientNumReplyErrors.Subsystem,
Name: metricClientNumReplyErrors.Name,
Help: "Number of errors in replies sent to clients.",
}, m.buildMetricLabels([]string{"method", "code", "channel_namespace"}))
m.serverUnsubscribeCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricClientNumServerUnsubscribes.Subsystem,
Name: metricClientNumServerUnsubscribes.Name,
Help: "Number of server initiated unsubscribes.",
}, m.buildMetricLabels([]string{"code", "channel_namespace"}))
m.serverDisconnectCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricClientNumServerDisconnects.Subsystem,
Name: metricClientNumServerDisconnects.Name,
Help: "Number of server initiated disconnects.",
}, m.buildMetricLabels([]string{"code"}))
// Note: only server-sent (outgoing) close codes are recorded. They are
// chosen by the server/operator, so the "code" label cardinality is bounded.
// Client-supplied (incoming) close codes are deliberately not recorded - a
// client may send any code in the RFC 6455 application range (3000-4999),
// which would let an unauthenticated peer inflate metric cardinality.
m.transportOutgoingCloseCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricTransportOutgoingClose.Subsystem,
Name: metricTransportOutgoingClose.Name,
Help: "Number of close frames sent to clients, by transport and code.",
}, []string{"transport", "code"})
m.recoverCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "client",
Name: "recover",
Help: "Count of recover operations with success/fail resolution.",
}, m.buildMetricLabels([]string{"recovered", "channel_namespace", "has_recovered_publications"}))
if config.EnableRecoveredPublicationsHistogram {
m.recoveredPublications = prometheus.NewHistogramVec(
nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "client",
Name: "recovered_publications",
Help: "Number of publications recovered during subscription recovery.",
Buckets: []float64{0, 1, 2, 3, 5, 10, 20, 50, 100, 250, 500, 1000, 2000, 5000, 10000},
}, config.EnableNativeHistograms),
m.buildMetricLabels([]string{"channel_namespace"}),
)
}
m.pingPongDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "client",
Name: "ping_pong_duration_seconds",
Help: "Ping/Pong duration in seconds",
Buckets: []float64{
0.000100, 0.000250, 0.000500, // Microsecond resolution.
0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500, // Millisecond resolution.
1.0, 2.5, 5.0, 10.0, // Second resolution.
}}, config.EnableNativeHistograms), m.buildMetricLabels([]string{"transport"}))
m.connectionsAccepted = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricClientConnectionsAccepted.Subsystem,
Name: metricClientConnectionsAccepted.Name,
Help: "Count of accepted transports.",
}, m.buildMetricLabels([]string{"transport", "accept_protocol", "client_name", "client_version"}))
m.connectionsInflight = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: metricClientConnectionsInflight.Subsystem,
Name: metricClientConnectionsInflight.Name,
Help: "Number of inflight client connections.",
}, m.buildMetricLabels([]string{"transport", "accept_protocol", "client_name", "client_version"}))
m.subscriptionsAccepted = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricClientSubscriptionsAccepted.Subsystem,
Name: metricClientSubscriptionsAccepted.Name,
Help: "Count of accepted client subscriptions.",
}, m.buildMetricLabels([]string{"client_name", "channel_namespace"}))
m.subscriptionsInflight = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: metricClientSubscriptionsInflight.Subsystem,
Name: metricClientSubscriptionsInflight.Name,
Help: "Number of inflight client subscriptions.",
}, m.buildMetricLabels([]string{"client_name", "channel_namespace"}))
m.transportMessagesSent = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricTransportMessagesSent.Subsystem,
Name: metricTransportMessagesSent.Name,
Help: "Number of messages sent to client connections over specific transport.",
}, m.buildMetricLabels([]string{"transport", "frame_type", "channel_namespace"}))
m.transportMessagesSentSize = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricTransportMessagesSentSize.Subsystem,
Name: metricTransportMessagesSentSize.Name,
Help: "MaxSize in bytes of messages sent to client connections over specific transport (uncompressed and does not include framing overhead).",
}, m.buildMetricLabels([]string{"transport", "frame_type", "channel_namespace"}))
m.transportMessagesReceived = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricTransportMessagesReceived.Subsystem,
Name: metricTransportMessagesReceived.Name,
Help: "Number of messages received from client connections over specific transport.",
}, m.buildMetricLabels([]string{"transport", "frame_type", "channel_namespace"}))
m.transportMessagesReceivedSize = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricTransportMessagesReceivedSize.Subsystem,
Name: metricTransportMessagesReceivedSize.Name,
Help: "MaxSize in bytes of messages received from client connections over specific transport (uncompressed and does not include framing overhead).",
}, m.buildMetricLabels([]string{"transport", "frame_type", "channel_namespace"}))
m.tagsFilterDroppedCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "tags_filter_dropped_publications",
Help: "Number of publications dropped due to tags filtering.",
}, []string{"channel_namespace"})
m.pubSubLagHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "pub_sub_lag_seconds",
Help: "Pub sub lag in seconds",
Buckets: []float64{0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.200, 0.500, 1.000, 2.000, 5.000, 10.000},
}, config.EnableNativeHistograms), []string{"channel_namespace"})
m.broadcastDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "broadcast_duration_seconds",
Help: "Broadcast duration in seconds",
Buckets: []float64{
0.000001, 0.000005, 0.000010, 0.000050, 0.000100, 0.000250, 0.000500, // Microsecond resolution.
0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500, // Millisecond resolution.
1.0, 2.5, 5.0, 10.0, // Second resolution.
}}, config.EnableNativeHistograms), []string{"type", "channel_namespace"})
m.brokerPublishSuppressedCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "publish_suppressed_count",
Help: "Number of suppressed publish operations.",
}, []string{"reason", "channel_namespace"})
m.mapBrokerPublishSuppressedCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "publish_suppressed_count",
Help: "Number of suppressed map publish operations.",
}, []string{"reason", "channel_namespace"})
m.mapBrokerRemoveSuppressedCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "remove_suppressed_count",
Help: "Number of suppressed map remove operations.",
}, []string{"reason", "channel_namespace"})
m.mapBrokerCleanupLag = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "cleanup_lag_seconds",
Help: "Lag between now and the oldest expired entry awaiting cleanup. 0 means caught up.",
}, []string{"broker_name"})
m.mapBrokerCleanupRemoved = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "cleanup_removed_count",
Help: "Total number of expired entries removed by cleanup.",
}, []string{"broker_name"})
m.mapBrokerCleanupErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "cleanup_errors_count",
Help: "Total number of cleanup errors.",
}, []string{"broker_name"})
sharedPollDurationBuckets := []float64{
0.010, 0.025, 0.050, 0.100, 0.250, 0.500, // Millisecond resolution.
1.0, 2.5, 5.0, 10.0, 30.0, 60.0, // Second resolution.
}
sharedPollHandlerBuckets := []float64{
0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500, // Millisecond resolution.
1.0, 2.5, 5.0, 10.0, 30.0, // Second resolution.
}
sharedPollSemWaitBuckets := []float64{
0.0001, 0.0005, 0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.250, 0.500, // Millisecond resolution.
1.0, 2.5, 5.0, 10.0, 30.0, // Second resolution.
}
m.sharedPollCycleDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "cycle_duration_seconds",
Help: "Full timer cycle duration in seconds.",
Buckets: sharedPollDurationBuckets,
}, config.EnableNativeHistograms), []string{"channel_namespace"})
m.sharedPollCycleWorkDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "cycle_work_duration_seconds",
Help: "Cycle work time in seconds (minus spread delay).",
Buckets: sharedPollDurationBuckets,
}, config.EnableNativeHistograms), []string{"channel_namespace"})
m.sharedPollHandlerDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "handler_duration_seconds",
Help: "Handler call latency in seconds.",
Buckets: sharedPollHandlerBuckets,
}, config.EnableNativeHistograms), []string{"trigger", "channel_namespace"})
m.sharedPollSemWaitDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "sem_wait_duration_seconds",
Help: "Semaphore wait duration in seconds.",
Buckets: sharedPollSemWaitBuckets,
}, config.EnableNativeHistograms), []string{"trigger", "channel_namespace"})
m.sharedPollHandlerErrorCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "handler_error_count",
Help: "Number of handler errors.",
}, []string{"trigger", "channel_namespace"})
m.sharedPollItemsCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "items_count",
Help: "Number of items by result.",
}, []string{"trigger", "result", "channel_namespace"})
m.sharedPollNotifyCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "notify_count",
Help: "Number of notifications received.",
}, []string{"channel_namespace"})
m.sharedPollDroppedNotifyCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "dropped_notify_count",
Help: "Number of notifications dropped due to full buffer.",
}, []string{"channel_namespace"})
m.sharedPollPublishCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "publish_count",
Help: "Number of direct publish operations by result.",
}, []string{"result", "channel_namespace"})
m.sharedPollNumChannelsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "num_channels",
Help: "Number of active shared poll channels.",
})
m.sharedPollNumKeysGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll",
Name: "num_keys",
Help: "Total number of tracked keys.",
})
m.redisBrokerPubSubErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "redis_pub_sub_errors",
Help: "Number of times there was an error in Redis PUB/SUB connection.",
}, []string{"broker_name", "error"})
m.redisBrokerPubSubDroppedMessages = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "redis_pub_sub_dropped_messages",
Help: "Number of dropped messages on application level in Redis PUB/SUB.",
}, []string{"broker_name", "channel_type"})
m.redisBrokerPubSubBufferedMessages = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "redis_pub_sub_buffered_messages",
Help: "Number of messages buffered in Redis PUB/SUB.",
}, []string{"broker_name", "channel_type", "pub_sub_processor"})
m.redisBrokerPubSubDroppedMessages.WithLabelValues("", "control").Add(0)
m.redisBrokerPubSubDroppedMessages.WithLabelValues("", "client").Add(0)
m.brokerPubSub = redisPubSubMetrics{
errors: m.redisBrokerPubSubErrors,
dropped: m.redisBrokerPubSubDroppedMessages,
buffered: m.redisBrokerPubSubBufferedMessages,
}
// RedisMapBroker reports the same Redis PUB/SUB metrics under its own
// map_broker subsystem so its series never collide with the stream broker's.
m.mapBrokerPubSub = redisPubSubMetrics{
errors: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "redis_pub_sub_errors",
Help: "Number of times there was an error in Redis PUB/SUB connection.",
}, []string{"broker_name", "error"}),
dropped: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "redis_pub_sub_dropped_messages",
Help: "Number of dropped messages on application level in Redis PUB/SUB.",
}, []string{"broker_name", "channel_type"}),
buffered: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "redis_pub_sub_buffered_messages",
Help: "Number of messages buffered in Redis PUB/SUB.",
}, []string{"broker_name", "channel_type", "pub_sub_processor"}),
}
m.mapBrokerPubSub.dropped.WithLabelValues("", "client").Add(0)
// Helper to build message labels for node-level broker message metrics
// These metrics don't support client labels as they track node-to-broker communication
buildMessageLabels := func(msgType string) []string {
return []string{msgType, ""}
}
m.messagesReceivedCountPublication = m.messagesReceivedCount.WithLabelValues(buildMessageLabels("publication")...)
m.messagesReceivedCountJoin = m.messagesReceivedCount.WithLabelValues(buildMessageLabels("join")...)
m.messagesReceivedCountLeave = m.messagesReceivedCount.WithLabelValues(buildMessageLabels("leave")...)
m.messagesReceivedCountControl = m.messagesReceivedCount.WithLabelValues(buildMessageLabels("control")...)
m.messagesSentCountPublication = m.messagesSentCount.WithLabelValues(buildMessageLabels("publication")...)
m.messagesSentCountJoin = m.messagesSentCount.WithLabelValues(buildMessageLabels("join")...)
m.messagesSentCountLeave = m.messagesSentCount.WithLabelValues(buildMessageLabels("leave")...)
m.messagesSentCountControl = m.messagesSentCount.WithLabelValues(buildMessageLabels("control")...)
labelForMethod := func(frameType protocol.FrameType) string {
return frameType.String()
}
// Helper to build initial label values with empty client labels if configured.
// Client labels are added as empty strings since these pre-cached observers
// are only used when no per-call ClientLabels lookup is needed.
buildCommandLabels := func(method string) []string {
labels := []string{method, ""}
for range m.config.ClientLabels {
labels = append(labels, "")
}
return labels
}
makeCommandObserver := func(method string) prometheus.Observer {
labels := buildCommandLabels(method)
return dualObserver{
summary: m.commandDurationSummary.WithLabelValues(labels...),
histogram: m.commandDurationHistogram.WithLabelValues(labels...),
}
}
m.commandDurationConnect = makeCommandObserver(labelForMethod(protocol.FrameTypeConnect))
m.commandDurationSubscribe = makeCommandObserver(labelForMethod(protocol.FrameTypeSubscribe))
m.commandDurationUnsubscribe = makeCommandObserver(labelForMethod(protocol.FrameTypeUnsubscribe))
m.commandDurationPublish = makeCommandObserver(labelForMethod(protocol.FrameTypePublish))
m.commandDurationPresence = makeCommandObserver(labelForMethod(protocol.FrameTypePresence))
m.commandDurationPresenceStats = makeCommandObserver(labelForMethod(protocol.FrameTypePresenceStats))
m.commandDurationHistory = makeCommandObserver(labelForMethod(protocol.FrameTypeHistory))
m.commandDurationSend = makeCommandObserver(labelForMethod(protocol.FrameTypeSend))
m.commandDurationRPC = makeCommandObserver(labelForMethod(protocol.FrameTypeRPC))
m.commandDurationRefresh = makeCommandObserver(labelForMethod(protocol.FrameTypeRefresh))
m.commandDurationSubRefresh = makeCommandObserver(labelForMethod(protocol.FrameTypeSubRefresh))
m.commandDurationUnknown = makeCommandObserver("unknown")
var alreadyRegistered prometheus.AlreadyRegisteredError
for _, collector := range []prometheus.Collector{
m.messagesSentCount,
m.messagesReceivedCount,
m.actionCount,
m.numClientsGauge,
m.numUsersGauge,
m.numSubsGauge,
m.numChannelsGauge,
m.numNodesGauge,
m.commandDurationSummary,
m.commandDurationHistogram,
m.replyErrorCount,
m.connectionsAccepted,
m.connectionsInflight,
m.subscriptionsAccepted,
m.subscriptionsInflight,
m.serverUnsubscribeCount,
m.serverDisconnectCount,
m.transportOutgoingCloseCount,
m.recoverCount,
m.pingPongDurationHistogram,
m.transportMessagesSent,