@@ -73,7 +73,8 @@ public final class PrometheusCollectorRegistry: Sendable {
7373 /// - Parameter name: A name to identify ``Counter``'s value.
7474 /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
7575 public func makeCounter( name: String ) -> Counter {
76- self . box. withLockedValue { store -> Counter in
76+ let name = name. ensureValidMetricName ( )
77+ return self . box. withLockedValue { store -> Counter in
7778 guard let value = store [ name] else {
7879 let counter = Counter ( name: name, labels: [ ] )
7980 store [ name] = . counter( counter)
@@ -106,6 +107,9 @@ public final class PrometheusCollectorRegistry: Sendable {
106107 return self . makeCounter ( name: name)
107108 }
108109
110+ let name = name. ensureValidMetricName ( )
111+ let labels = labels. ensureValidLabelNames ( )
112+
109113 return self . box. withLockedValue { store -> Counter in
110114 guard let value = store [ name] else {
111115 let labelNames = labels. allLabelNames
@@ -154,7 +158,8 @@ public final class PrometheusCollectorRegistry: Sendable {
154158 /// - Parameter name: A name to identify ``Gauge``'s value.
155159 /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
156160 public func makeGauge( name: String ) -> Gauge {
157- self . box. withLockedValue { store -> Gauge in
161+ let name = name. ensureValidMetricName ( )
162+ return self . box. withLockedValue { store -> Gauge in
158163 guard let value = store [ name] else {
159164 let gauge = Gauge ( name: name, labels: [ ] )
160165 store [ name] = . gauge( gauge)
@@ -187,6 +192,9 @@ public final class PrometheusCollectorRegistry: Sendable {
187192 return self . makeGauge ( name: name)
188193 }
189194
195+ let name = name. ensureValidMetricName ( )
196+ let labels = labels. ensureValidLabelNames ( )
197+
190198 return self . box. withLockedValue { store -> Gauge in
191199 guard let value = store [ name] else {
192200 let labelNames = labels. allLabelNames
@@ -236,7 +244,8 @@ public final class PrometheusCollectorRegistry: Sendable {
236244 /// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram``
237245 /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry``
238246 public func makeDurationHistogram( name: String , buckets: [ Duration ] ) -> DurationHistogram {
239- self . box. withLockedValue { store -> DurationHistogram in
247+ let name = name. ensureValidMetricName ( )
248+ return self . box. withLockedValue { store -> DurationHistogram in
240249 guard let value = store [ name] else {
241250 let gauge = DurationHistogram ( name: name, labels: [ ] , buckets: buckets)
242251 store [ name] = . durationHistogram( gauge)
@@ -274,6 +283,9 @@ public final class PrometheusCollectorRegistry: Sendable {
274283 return self . makeDurationHistogram ( name: name, buckets: buckets)
275284 }
276285
286+ let name = name. ensureValidMetricName ( )
287+ let labels = labels. ensureValidLabelNames ( )
288+
277289 return self . box. withLockedValue { store -> DurationHistogram in
278290 guard let value = store [ name] else {
279291 let labelNames = labels. allLabelNames
@@ -335,7 +347,8 @@ public final class PrometheusCollectorRegistry: Sendable {
335347 /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram``
336348 /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry``
337349 public func makeValueHistogram( name: String , buckets: [ Double ] ) -> ValueHistogram {
338- self . box. withLockedValue { store -> ValueHistogram in
350+ let name = name. ensureValidMetricName ( )
351+ return self . box. withLockedValue { store -> ValueHistogram in
339352 guard let value = store [ name] else {
340353 let gauge = ValueHistogram ( name: name, labels: [ ] , buckets: buckets)
341354 store [ name] = . valueHistogram( gauge)
@@ -364,6 +377,9 @@ public final class PrometheusCollectorRegistry: Sendable {
364377 return self . makeValueHistogram ( name: name, buckets: buckets)
365378 }
366379
380+ let name = name. ensureValidMetricName ( )
381+ let labels = labels. ensureValidLabelNames ( )
382+
367383 return self . box. withLockedValue { store -> ValueHistogram in
368384 guard let value = store [ name] else {
369385 let labelNames = labels. allLabelNames
@@ -560,6 +576,14 @@ extension [(String, String)] {
560576 result = result. sorted ( )
561577 return result
562578 }
579+
580+ fileprivate func ensureValidLabelNames( ) -> [ ( String , String ) ] {
581+ if self . allSatisfy ( { $0. 0 . isValidLabelName ( ) } ) {
582+ return self
583+ } else {
584+ return self . map { ( $0. ensureValidLabelName ( ) , $1) }
585+ }
586+ }
563587}
564588
565589extension [ UInt8 ] {
@@ -595,3 +619,91 @@ extension PrometheusMetric {
595619 return prerendered
596620 }
597621}
622+
623+ extension String {
624+ fileprivate func isValidMetricName( ) -> Bool {
625+ var isFirstCharacter = true
626+ for ascii in self . utf8 {
627+ defer { isFirstCharacter = false }
628+ switch ascii {
629+ case UInt8 ( ascii: " A " ) ... UInt8 ( ascii: " Z " ) ,
630+ UInt8 ( ascii: " a " ) ... UInt8 ( ascii: " z " ) ,
631+ UInt8 ( ascii: " _ " ) , UInt8 ( ascii: " : " ) :
632+ continue
633+ case UInt8 ( ascii: " 0 " ) , UInt8 ( ascii: " 9 " ) :
634+ if isFirstCharacter {
635+ return false
636+ }
637+ continue
638+ default :
639+ return false
640+ }
641+ }
642+ return true
643+ }
644+
645+ fileprivate func isValidLabelName( ) -> Bool {
646+ var isFirstCharacter = true
647+ for ascii in self . utf8 {
648+ defer { isFirstCharacter = false }
649+ switch ascii {
650+ case UInt8 ( ascii: " A " ) ... UInt8 ( ascii: " Z " ) ,
651+ UInt8 ( ascii: " a " ) ... UInt8 ( ascii: " z " ) ,
652+ UInt8 ( ascii: " _ " ) :
653+ continue
654+ case UInt8 ( ascii: " 0 " ) , UInt8 ( ascii: " 9 " ) :
655+ if isFirstCharacter {
656+ return false
657+ }
658+ continue
659+ default :
660+ return false
661+ }
662+ }
663+ return true
664+ }
665+
666+ fileprivate func ensureValidMetricName( ) -> String {
667+ if self . isValidMetricName ( ) {
668+ return self
669+ } else {
670+ var new = self
671+ new. fixPrometheusName ( allowColon: true )
672+ return new
673+ }
674+ }
675+
676+ fileprivate func ensureValidLabelName( ) -> String {
677+ if self . isValidLabelName ( ) {
678+ return self
679+ } else {
680+ var new = self
681+ new. fixPrometheusName ( allowColon: false )
682+ return new
683+ }
684+ }
685+
686+ fileprivate mutating func fixPrometheusName( allowColon: Bool ) {
687+ var startIndex = self . startIndex
688+ var isFirstCharacter = true
689+ while let fixIndex = self [ startIndex... ] . firstIndex ( where: { character in
690+ defer { isFirstCharacter = false }
691+ switch character {
692+ case " A " ... " Z " , " a " ... " z " , " _ " :
693+ return false
694+ case " : " :
695+ return !allowColon
696+ case " 0 " ... " 9 " :
697+ return isFirstCharacter
698+ default :
699+ return true
700+ }
701+ } ) {
702+ self . replaceSubrange ( fixIndex... fixIndex, with: CollectionOfOne ( " _ " ) )
703+ startIndex = fixIndex
704+ if startIndex == self . endIndex {
705+ break
706+ }
707+ }
708+ }
709+ }
0 commit comments