@@ -47,9 +47,22 @@ public final class PrometheusCollectorRegistry: Sendable {
4747 }
4848 }
4949
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+
5064 private enum Metric {
51- case counter( Counter , help: String )
52- case counterWithLabels( [ String ] , [ LabelsKey : Counter ] , help: String )
65+ case counter( CounterGroup )
5366 case gauge( Gauge , help: String )
5467 case gaugeWithLabels( [ String ] , [ LabelsKey : Gauge ] , help: String )
5568 case durationHistogram( DurationHistogram , help: String )
@@ -75,25 +88,7 @@ public final class PrometheusCollectorRegistry: Sendable {
7588 /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
7689 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
7790 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)
9792 }
9893
9994 /// Creates a new ``Counter`` collector or returns the already existing one with the same name.
@@ -104,7 +99,7 @@ public final class PrometheusCollectorRegistry: Sendable {
10499 /// - Parameter name: A name to identify ``Counter``'s value.
105100 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
106101 public func makeCounter( name: String ) -> Counter {
107- return self . makeCounter ( name: name, help: " " )
102+ return self . makeCounter ( name: name, labels : [ ] , help: " " )
108103 }
109104
110105 /// Creates a new ``Counter`` collector or returns the already existing one with the same name,
@@ -116,7 +111,7 @@ public final class PrometheusCollectorRegistry: Sendable {
116111 /// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric.
117112 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
118113 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 ?? " " )
120115 }
121116
122117 /// Creates a new ``Counter`` collector or returns the already existing one with the same name.
@@ -131,51 +126,49 @@ public final class PrometheusCollectorRegistry: Sendable {
131126 /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
132127 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
133128 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-
138129 let name = name. ensureValidMetricName ( )
139130 let labels = labels. ensureValidLabelNames ( )
140131 let help = help. ensureValidHelpText ( )
132+ let key = LabelsKey ( labels)
141133
142134 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.
145137 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]
156141 )
142+ store [ name] = . counter( counterGroup)
143+ return counter
157144 }
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)
158158
159- let key = LabelsKey ( labels)
160- if let counter = dimensionLookup [ key] {
161159 return counter
162- }
163160
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.
166164 fatalError (
167165 """
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.
171169 """
172170 )
173171 }
174-
175- let counter = Counter ( name: name, labels: labels)
176- dimensionLookup [ key] = counter
177- store [ name] = . counterWithLabels( labelNames, dimensionLookup, help: help)
178- return counter
179172 }
180173 }
181174
@@ -703,17 +696,19 @@ public final class PrometheusCollectorRegistry: Sendable {
703696 public func unregisterCounter( _ counter: Counter ) {
704697 self . box. withLockedValue { store in
705698 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 {
714709 store. removeValue ( forKey: counter. name)
715710 } else {
716- store [ counter. name] = . counterWithLabels ( labelNames , dimensions , help : help )
711+ store [ counter. name] = . counter ( counterGroup )
717712 }
718713 default :
719714 return
@@ -806,52 +801,52 @@ public final class PrometheusCollectorRegistry: Sendable {
806801 let prefixHelp = " HELP "
807802 let prefixType = " TYPE "
808803
809- for (label , metric) in metrics {
804+ for (name , metric) in metrics {
810805 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)
821816 }
822817
823818 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 " )
826821 gauge. emit ( into: & buffer)
827822
828823 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 " )
831826 for gauge in gauges. values {
832827 gauge. emit ( into: & buffer)
833828 }
834829
835830 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 " )
838833 histogram. emit ( into: & buffer)
839834
840835 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 " )
843838 for histogram in histograms. values {
844839 histogram. emit ( into: & buffer)
845840 }
846841
847842 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 " )
850845 histogram. emit ( into: & buffer)
851846
852847 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 " )
855850 for histogram in histograms. values {
856851 histogram. emit ( into: & buffer)
857852 }
0 commit comments