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