@@ -47,9 +47,22 @@ public final class PrometheusCollectorRegistry: Sendable {
47
47
}
48
48
}
49
49
50
+ private struct CounterWithHelp {
51
+ var counter : Counter
52
+ let help : String
53
+ }
54
+
55
+ private struct CounterGroup {
56
+ // A collection of Counter metrics, each with a unique label set, that share the same metric name.
57
+ // Distinct help strings for the same metric name are permitted, but Prometheus retains only the
58
+ // most recent one. For an unlabelled Counter, the empty label set is used as the key, and the
59
+ // collection contains only one entry. Finally, for clarification, the same Counter metric name can
60
+ // simultaneously be labeled and unlabeled.
61
+ var countersByLabelSets : [ LabelsKey : CounterWithHelp ]
62
+ }
63
+
50
64
private enum Metric {
51
- case counter( Counter , help: String )
52
- case counterWithLabels( [ String ] , [ LabelsKey : Counter ] , help: String )
65
+ case counter( CounterGroup )
53
66
case gauge( Gauge , help: String )
54
67
case gaugeWithLabels( [ String ] , [ LabelsKey : Gauge ] , help: String )
55
68
case durationHistogram( DurationHistogram , help: String )
@@ -75,25 +88,7 @@ public final class PrometheusCollectorRegistry: Sendable {
75
88
/// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
76
89
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
77
90
public func makeCounter( name: String , help: String ) -> Counter {
78
- let name = name. ensureValidMetricName ( )
79
- let help = help. ensureValidHelpText ( )
80
- return self . box. withLockedValue { store -> Counter in
81
- guard let value = store [ name] else {
82
- let counter = Counter ( name: name, labels: [ ] )
83
- store [ name] = . counter( counter, help: help)
84
- return counter
85
- }
86
- guard case . counter( let counter, _) = value else {
87
- fatalError (
88
- """
89
- Could not make Counter with name: \( name) , since another metric type
90
- already exists for the same name.
91
- """
92
- )
93
- }
94
-
95
- return counter
96
- }
91
+ return self . makeCounter ( name: name, labels: [ ] , help: help)
97
92
}
98
93
99
94
/// Creates a new ``Counter`` collector or returns the already existing one with the same name.
@@ -104,7 +99,7 @@ public final class PrometheusCollectorRegistry: Sendable {
104
99
/// - Parameter name: A name to identify ``Counter``'s value.
105
100
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
106
101
public func makeCounter( name: String ) -> Counter {
107
- return self . makeCounter ( name: name, help: " " )
102
+ return self . makeCounter ( name: name, labels : [ ] , help: " " )
108
103
}
109
104
110
105
/// Creates a new ``Counter`` collector or returns the already existing one with the same name,
@@ -116,7 +111,7 @@ public final class PrometheusCollectorRegistry: Sendable {
116
111
/// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric.
117
112
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
118
113
public func makeCounter( descriptor: MetricNameDescriptor ) -> Counter {
119
- return self . makeCounter ( name: descriptor. name, help: descriptor. helpText ?? " " )
114
+ return self . makeCounter ( name: descriptor. name, labels : [ ] , help: descriptor. helpText ?? " " )
120
115
}
121
116
122
117
/// Creates a new ``Counter`` collector or returns the already existing one with the same name.
@@ -131,51 +126,49 @@ public final class PrometheusCollectorRegistry: Sendable {
131
126
/// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
132
127
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
133
128
public func makeCounter( name: String , labels: [ ( String , String ) ] , help: String ) -> Counter {
134
- guard !labels. isEmpty else {
135
- return self . makeCounter ( name: name, help: help)
136
- }
137
-
138
129
let name = name. ensureValidMetricName ( )
139
130
let labels = labels. ensureValidLabelNames ( )
140
131
let help = help. ensureValidHelpText ( )
132
+ let key = LabelsKey ( labels)
141
133
142
134
return self . box. withLockedValue { store -> Counter in
143
- guard let value = store [ name] else {
144
- let labelNames = labels . allLabelNames
135
+ guard let entry = store [ name] else {
136
+ // First time a Counter is registered with this name.
145
137
let counter = Counter ( name: name, labels: labels)
146
-
147
- store [ name] = . counterWithLabels( labelNames, [ LabelsKey ( labels) : counter] , help: help)
148
- return counter
149
- }
150
- guard case . counterWithLabels( let labelNames, var dimensionLookup, let help) = value else {
151
- fatalError (
152
- """
153
- Could not make Counter with name: \( name) and labels: \( labels) , since another
154
- metric type already exists for the same name.
155
- """
138
+ let counterWithHelp = CounterWithHelp ( counter: counter, help: help)
139
+ let counterGroup = CounterGroup (
140
+ countersByLabelSets: [ key: counterWithHelp]
156
141
)
142
+ store [ name] = . counter( counterGroup)
143
+ return counter
157
144
}
145
+ switch entry {
146
+ case . counter( var existingCounterGroup) :
147
+ if let existingCounterWithHelp = existingCounterGroup. countersByLabelSets [ key] {
148
+ return existingCounterWithHelp. counter
149
+ }
150
+
151
+ // Even if the metric name is identical, each label set defines a unique time series.
152
+ let counter = Counter ( name: name, labels: labels)
153
+ let counterWithHelp = CounterWithHelp ( counter: counter, help: help)
154
+ existingCounterGroup. countersByLabelSets [ key] = counterWithHelp
155
+
156
+ // Write the modified entry back to the store.
157
+ store [ name] = . counter( existingCounterGroup)
158
158
159
- let key = LabelsKey ( labels)
160
- if let counter = dimensionLookup [ key] {
161
159
return counter
162
- }
163
160
164
- // check if all labels match the already existing ones.
165
- if labelNames != labels. allLabelNames {
161
+ default :
162
+ // A metric with this name exists, but it's not a Counter. This is a programming error.
163
+ // While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL.
166
164
fatalError (
167
165
"""
168
- Could not make Counter with name: \( name ) and labels: \( labels ) , since the
169
- label names don't match the label names of previously registered Counters with
170
- the same name.
166
+ Metric type mismatch:
167
+ Could not register a Counter with name ' \( name ) ',
168
+ since a different metric type ( \( entry . self ) ) was already registered with this name.
171
169
"""
172
170
)
173
171
}
174
-
175
- let counter = Counter ( name: name, labels: labels)
176
- dimensionLookup [ key] = counter
177
- store [ name] = . counterWithLabels( labelNames, dimensionLookup, help: help)
178
- return counter
179
172
}
180
173
}
181
174
@@ -703,17 +696,19 @@ public final class PrometheusCollectorRegistry: Sendable {
703
696
public func unregisterCounter( _ counter: Counter ) {
704
697
self . box. withLockedValue { store in
705
698
switch store [ counter. name] {
706
- case . counter( let storedCounter, _) :
707
- guard storedCounter === counter else { return }
708
- store. removeValue ( forKey: counter. name)
709
- case . counterWithLabels( let labelNames, var dimensions, let help) :
710
- let labelsKey = LabelsKey ( counter. labels)
711
- guard dimensions [ labelsKey] === counter else { return }
712
- dimensions. removeValue ( forKey: labelsKey)
713
- if dimensions. isEmpty {
699
+ case . counter( var counterGroup) :
700
+ let key = LabelsKey ( counter. labels)
701
+ guard let existingCounterGroup = counterGroup. countersByLabelSets [ key] ,
702
+ existingCounterGroup. counter === counter
703
+ else {
704
+ return
705
+ }
706
+ counterGroup. countersByLabelSets. removeValue ( forKey: key)
707
+
708
+ if counterGroup. countersByLabelSets. isEmpty {
714
709
store. removeValue ( forKey: counter. name)
715
710
} else {
716
- store [ counter. name] = . counterWithLabels ( labelNames , dimensions , help : help )
711
+ store [ counter. name] = . counter ( counterGroup )
717
712
}
718
713
default :
719
714
return
@@ -806,52 +801,52 @@ public final class PrometheusCollectorRegistry: Sendable {
806
801
let prefixHelp = " HELP "
807
802
let prefixType = " TYPE "
808
803
809
- for (label , metric) in metrics {
804
+ for (name , metric) in metrics {
810
805
switch metric {
811
- case . counter( let counter , let help ) :
812
- help . isEmpty ? ( ) : buffer . addLine ( prefix : prefixHelp , name : label , value : help )
813
- buffer . addLine ( prefix : prefixType , name : label , value: " counter " )
814
- counter . emit ( into : & buffer )
815
-
816
- case . counterWithLabels ( _ , let counters , let help ) :
817
- help . isEmpty ? ( ) : buffer . addLine ( prefix : prefixHelp , name : label , value : help)
818
- buffer. addLine ( prefix: prefixType , name: label , value: " counter " )
819
- for counter in counters . values {
820
- counter. emit ( into: & buffer)
806
+ case . counter( let counterGroup ) :
807
+ // Should not be empty, as a safeguard skip if it is.
808
+ guard let _ = counterGroup . countersByLabelSets . first ? . value else {
809
+ continue
810
+ }
811
+ for counterWithHelp in counterGroup . countersByLabelSets . values {
812
+ let help = counterWithHelp . help
813
+ help . isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp , name: name , value: help )
814
+ buffer . addLine ( prefix : prefixType , name : name , value : " counter " )
815
+ counterWithHelp . counter. emit ( into: & buffer)
821
816
}
822
817
823
818
case . gauge( let gauge, let help) :
824
- help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: label , value: help)
825
- buffer. addLine ( prefix: prefixType, name: label , value: " gauge " )
819
+ help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: name , value: help)
820
+ buffer. addLine ( prefix: prefixType, name: name , value: " gauge " )
826
821
gauge. emit ( into: & buffer)
827
822
828
823
case . gaugeWithLabels( _, let gauges, let help) :
829
- help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: label , value: help)
830
- buffer. addLine ( prefix: prefixType, name: label , value: " gauge " )
824
+ help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: name , value: help)
825
+ buffer. addLine ( prefix: prefixType, name: name , value: " gauge " )
831
826
for gauge in gauges. values {
832
827
gauge. emit ( into: & buffer)
833
828
}
834
829
835
830
case . durationHistogram( let histogram, let help) :
836
- help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: label , value: help)
837
- buffer. addLine ( prefix: prefixType, name: label , value: " histogram " )
831
+ help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: name , value: help)
832
+ buffer. addLine ( prefix: prefixType, name: name , value: " histogram " )
838
833
histogram. emit ( into: & buffer)
839
834
840
835
case . durationHistogramWithLabels( _, let histograms, _, let help) :
841
- help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: label , value: help)
842
- buffer. addLine ( prefix: prefixType, name: label , value: " histogram " )
836
+ help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: name , value: help)
837
+ buffer. addLine ( prefix: prefixType, name: name , value: " histogram " )
843
838
for histogram in histograms. values {
844
839
histogram. emit ( into: & buffer)
845
840
}
846
841
847
842
case . valueHistogram( let histogram, let help) :
848
- help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: label , value: help)
849
- buffer. addLine ( prefix: prefixType, name: label , value: " histogram " )
843
+ help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: name , value: help)
844
+ buffer. addLine ( prefix: prefixType, name: name , value: " histogram " )
850
845
histogram. emit ( into: & buffer)
851
846
852
847
case . valueHistogramWithLabels( _, let histograms, _, let help) :
853
- help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: label , value: help)
854
- buffer. addLine ( prefix: prefixType, name: label , value: " histogram " )
848
+ help. isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp, name: name , value: help)
849
+ buffer. addLine ( prefix: prefixType, name: name , value: " histogram " )
855
850
for histogram in histograms. values {
856
851
histogram. emit ( into: & buffer)
857
852
}
0 commit comments