@@ -47,9 +47,23 @@ public final class PrometheusCollectorRegistry: Sendable {
4747 }
4848 }
4949
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+
5065 private enum Metric {
51- case counter( Counter , help: String )
52- case counterWithLabels( [ String ] , [ LabelsKey : Counter ] , help: String )
66+ case counter( name: String , CounterGroup )
5367 case gauge( Gauge , help: String )
5468 case gaugeWithLabels( [ String ] , [ LabelsKey : Gauge ] , help: String )
5569 case durationHistogram( DurationHistogram , help: String )
@@ -75,25 +89,7 @@ public final class PrometheusCollectorRegistry: Sendable {
7589 /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
7690 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
7791 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)
9793 }
9894
9995 /// Creates a new ``Counter`` collector or returns the already existing one with the same name.
@@ -104,7 +100,7 @@ public final class PrometheusCollectorRegistry: Sendable {
104100 /// - Parameter name: A name to identify ``Counter``'s value.
105101 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
106102 public func makeCounter( name: String ) -> Counter {
107- return self . makeCounter ( name: name, help: " " )
103+ return self . makeCounter ( name: name, labels : [ ] , help: " " )
108104 }
109105
110106 /// Creates a new ``Counter`` collector or returns the already existing one with the same name,
@@ -116,7 +112,7 @@ public final class PrometheusCollectorRegistry: Sendable {
116112 /// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric.
117113 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
118114 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 ?? " " )
120116 }
121117
122118 /// Creates a new ``Counter`` collector or returns the already existing one with the same name.
@@ -131,51 +127,49 @@ public final class PrometheusCollectorRegistry: Sendable {
131127 /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric.
132128 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
133129 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-
138130 let name = name. ensureValidMetricName ( )
139131 let labels = labels. ensureValidLabelNames ( )
140132 let help = help. ensureValidHelpText ( )
133+ let key = LabelsKey ( labels)
141134
142135 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.
145138 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]
156142 )
143+ store [ name] = . counter( name: name, counterGroup)
144+ return counter
157145 }
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)
158159
159- let key = LabelsKey ( labels)
160- if let counter = dimensionLookup [ key] {
161160 return counter
162- }
163161
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.
166165 fatalError (
167166 """
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.
171170 """
172171 )
173172 }
174-
175- let counter = Counter ( name: name, labels: labels)
176- dimensionLookup [ key] = counter
177- store [ name] = . counterWithLabels( labelNames, dimensionLookup, help: help)
178- return counter
179173 }
180174 }
181175
@@ -703,17 +697,19 @@ public final class PrometheusCollectorRegistry: Sendable {
703697 public func unregisterCounter( _ counter: Counter ) {
704698 self . box. withLockedValue { store in
705699 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)
715711 } else {
716- store [ counter . name] = . counterWithLabels ( labelNames , dimensions , help : help )
712+ store [ name] = . counter ( name : name , counterGroup )
717713 }
718714 default :
719715 return
@@ -806,52 +802,52 @@ public final class PrometheusCollectorRegistry: Sendable {
806802 let prefixHelp = " HELP "
807803 let prefixType = " TYPE "
808804
809- for (label , metric) in metrics {
805+ for (name , metric) in metrics {
810806 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)
821817 }
822818
823819 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 " )
826822 gauge. emit ( into: & buffer)
827823
828824 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 " )
831827 for gauge in gauges. values {
832828 gauge. emit ( into: & buffer)
833829 }
834830
835831 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 " )
838834 histogram. emit ( into: & buffer)
839835
840836 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 " )
843839 for histogram in histograms. values {
844840 histogram. emit ( into: & buffer)
845841 }
846842
847843 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 " )
850846 histogram. emit ( into: & buffer)
851847
852848 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 " )
855851 for histogram in histograms. values {
856852 histogram. emit ( into: & buffer)
857853 }
0 commit comments