@@ -47,11 +47,17 @@ public final class PrometheusCollectorRegistry: Sendable {
47
47
}
48
48
}
49
49
50
+ // Note: In order to support Sendable, need to explicitely define the types.
50
51
private struct CounterWithHelp {
51
52
var counter : Counter
52
53
let help : String
53
54
}
54
55
56
+ private struct GaugeWithHelp {
57
+ var gauge : Gauge
58
+ let help : String
59
+ }
60
+
55
61
private struct CounterGroup {
56
62
// A collection of Counter metrics, each with a unique label set, that share the same metric name.
57
63
// Distinct help strings for the same metric name are permitted, but Prometheus retains only the
@@ -61,10 +67,13 @@ public final class PrometheusCollectorRegistry: Sendable {
61
67
var countersByLabelSets : [ LabelsKey : CounterWithHelp ]
62
68
}
63
69
70
+ private struct GaugeGroup {
71
+ var gaugesByLabelSets : [ LabelsKey : GaugeWithHelp ]
72
+ }
73
+
64
74
private enum Metric {
65
75
case counter( CounterGroup )
66
- case gauge( Gauge , help: String )
67
- case gaugeWithLabels( [ String ] , [ LabelsKey : Gauge ] , help: String )
76
+ case gauge( GaugeGroup )
68
77
case durationHistogram( DurationHistogram , help: String )
69
78
case durationHistogramWithLabels( [ String ] , [ LabelsKey : DurationHistogram ] , [ Duration ] , help: String )
70
79
case valueHistogram( ValueHistogram , help: String )
@@ -209,25 +218,7 @@ public final class PrometheusCollectorRegistry: Sendable {
209
218
/// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
210
219
/// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
211
220
public func makeGauge( name: String , help: String ) -> Gauge {
212
- let name = name. ensureValidMetricName ( )
213
- let help = help. ensureValidHelpText ( )
214
- return self . box. withLockedValue { store -> Gauge in
215
- guard let value = store [ name] else {
216
- let gauge = Gauge ( name: name, labels: [ ] )
217
- store [ name] = . gauge( gauge, help: help)
218
- return gauge
219
- }
220
- guard case . gauge( let gauge, _) = value else {
221
- fatalError (
222
- """
223
- Could not make Gauge with name: \( name) , since another metric type already
224
- exists for the same name.
225
- """
226
- )
227
- }
228
-
229
- return gauge
230
- }
221
+ return self . makeGauge ( name: name, labels: [ ] , help: help)
231
222
}
232
223
233
224
/// Creates a new ``Gauge`` collector or returns the already existing one with the same name.
@@ -238,7 +229,7 @@ public final class PrometheusCollectorRegistry: Sendable {
238
229
/// - Parameter name: A name to identify ``Gauge``'s value.
239
230
/// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
240
231
public func makeGauge( name: String ) -> Gauge {
241
- return self . makeGauge ( name: name, help: " " )
232
+ return self . makeGauge ( name: name, labels : [ ] , help: " " )
242
233
}
243
234
244
235
/// Creates a new ``Gauge`` collector or returns the already existing one with the same name,
@@ -250,7 +241,7 @@ public final class PrometheusCollectorRegistry: Sendable {
250
241
/// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric.
251
242
/// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
252
243
public func makeGauge( descriptor: MetricNameDescriptor ) -> Gauge {
253
- return self . makeGauge ( name: descriptor. name, help: descriptor. helpText ?? " " )
244
+ return self . makeGauge ( name: descriptor. name, labels : [ ] , help: descriptor. helpText ?? " " )
254
245
}
255
246
256
247
/// Creates a new ``Gauge`` collector or returns the already existing one with the same name.
@@ -265,51 +256,49 @@ public final class PrometheusCollectorRegistry: Sendable {
265
256
/// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
266
257
/// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
267
258
public func makeGauge( name: String , labels: [ ( String , String ) ] , help: String ) -> Gauge {
268
- guard !labels. isEmpty else {
269
- return self . makeGauge ( name: name, help: help)
270
- }
271
-
272
259
let name = name. ensureValidMetricName ( )
273
260
let labels = labels. ensureValidLabelNames ( )
274
261
let help = help. ensureValidHelpText ( )
262
+ let key = LabelsKey ( labels)
275
263
276
264
return self . box. withLockedValue { store -> Gauge in
277
- guard let value = store [ name] else {
278
- let labelNames = labels . allLabelNames
265
+ guard let entry = store [ name] else {
266
+ // First time a Gauge is registered with this name.
279
267
let gauge = Gauge ( name: name, labels: labels)
280
-
281
- store [ name] = . gaugeWithLabels( labelNames, [ LabelsKey ( labels) : gauge] , help: help)
282
- return gauge
283
- }
284
- guard case . gaugeWithLabels( let labelNames, var dimensionLookup, let help) = value else {
285
- fatalError (
286
- """
287
- Could not make Gauge with name: \( name) and labels: \( labels) , since another
288
- metric type already exists for the same name.
289
- """
268
+ let gaugeWithHelp = GaugeWithHelp ( gauge: gauge, help: help)
269
+ let gaugeGroup = GaugeGroup (
270
+ gaugesByLabelSets: [ key: gaugeWithHelp]
290
271
)
272
+ store [ name] = . gauge( gaugeGroup)
273
+ return gauge
291
274
}
275
+ switch entry {
276
+ case . gauge( var existingGaugeGroup) :
277
+ if let existingGaugeWithHelp = existingGaugeGroup. gaugesByLabelSets [ key] {
278
+ return existingGaugeWithHelp. gauge
279
+ }
280
+
281
+ // Even if the metric name is identical, each label set defines a unique time series.
282
+ let gauge = Gauge ( name: name, labels: labels)
283
+ let gaugeWithHelp = GaugeWithHelp ( gauge: gauge, help: help)
284
+ existingGaugeGroup. gaugesByLabelSets [ key] = gaugeWithHelp
285
+
286
+ // Write the modified entry back to the store.
287
+ store [ name] = . gauge( existingGaugeGroup)
292
288
293
- let key = LabelsKey ( labels)
294
- if let gauge = dimensionLookup [ key] {
295
289
return gauge
296
- }
297
290
298
- // check if all labels match the already existing ones.
299
- if labelNames != labels. allLabelNames {
291
+ default :
292
+ // A metric with this name exists, but it's not a Gauge. This is a programming error.
293
+ // While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL.
300
294
fatalError (
301
295
"""
302
- Could not make Gauge with name: \( name ) and labels: \( labels ) , since the
303
- label names don't match the label names of previously registered Gauges with
304
- the same name.
296
+ Metric type mismatch:
297
+ Could not register a Gauge with name ' \( name ) ',
298
+ since a different metric type ( \( entry . self ) ) was already registered with this name.
305
299
"""
306
300
)
307
301
}
308
-
309
- let gauge = Gauge ( name: name, labels: labels)
310
- dimensionLookup [ key] = gauge
311
- store [ name] = . gaugeWithLabels( labelNames, dimensionLookup, help: help)
312
- return gauge
313
302
}
314
303
}
315
304
@@ -724,17 +713,19 @@ public final class PrometheusCollectorRegistry: Sendable {
724
713
public func unregisterGauge( _ gauge: Gauge ) {
725
714
self . box. withLockedValue { store in
726
715
switch store [ gauge. name] {
727
- case . gauge( let storedGauge, _) :
728
- guard storedGauge === gauge else { return }
729
- store. removeValue ( forKey: gauge. name)
730
- case . gaugeWithLabels( let labelNames, var dimensions, let help) :
731
- let dimensionsKey = LabelsKey ( gauge. labels)
732
- guard dimensions [ dimensionsKey] === gauge else { return }
733
- dimensions. removeValue ( forKey: dimensionsKey)
734
- if dimensions. isEmpty {
716
+ case . gauge( var gaugeGroup) :
717
+ let key = LabelsKey ( gauge. labels)
718
+ guard let existingGaugeGroup = gaugeGroup. gaugesByLabelSets [ key] ,
719
+ existingGaugeGroup. gauge === gauge
720
+ else {
721
+ return
722
+ }
723
+ gaugeGroup. gaugesByLabelSets. removeValue ( forKey: key)
724
+
725
+ if gaugeGroup. gaugesByLabelSets. isEmpty {
735
726
store. removeValue ( forKey: gauge. name)
736
727
} else {
737
- store [ gauge. name] = . gaugeWithLabels ( labelNames , dimensions , help : help )
728
+ store [ gauge. name] = . gauge ( gaugeGroup )
738
729
}
739
730
default :
740
731
return
@@ -815,16 +806,16 @@ public final class PrometheusCollectorRegistry: Sendable {
815
806
counterWithHelp. counter. emit ( into: & buffer)
816
807
}
817
808
818
- case . gauge( let gauge , let help ) :
819
- help . isEmpty ? ( ) : buffer . addLine ( prefix : prefixHelp , name : name , value : help )
820
- buffer . addLine ( prefix : prefixType , name : name , value: " gauge " )
821
- gauge . emit ( into : & buffer )
822
-
823
- case . gaugeWithLabels ( _ , let gauges , let help ) :
824
- help . isEmpty ? ( ) : buffer . addLine ( prefix : prefixHelp , name : name , value : help)
825
- buffer. addLine ( prefix: prefixType , name: name, value: " gauge " )
826
- for gauge in gauges . values {
827
- gauge. emit ( into: & buffer)
809
+ case . gauge( let gaugeGroup ) :
810
+ // Should not be empty, as a safeguard skip if it is.
811
+ guard let _ = gaugeGroup . gaugesByLabelSets . first ? . value else {
812
+ continue
813
+ }
814
+ for gaugeWithHelp in gaugeGroup . gaugesByLabelSets . values {
815
+ let help = gaugeWithHelp . help
816
+ help . isEmpty ? ( ) : buffer. addLine ( prefix: prefixHelp , name: name, value: help )
817
+ buffer . addLine ( prefix : prefixType , name : name , value : " gauge " )
818
+ gaugeWithHelp . gauge. emit ( into: & buffer)
828
819
}
829
820
830
821
case . durationHistogram( let histogram, let help) :
0 commit comments