@@ -73,7 +73,8 @@ public final class PrometheusCollectorRegistry: Sendable {
73
73
/// - Parameter name: A name to identify ``Counter``'s value.
74
74
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
75
75
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
77
78
guard let value = store [ name] else {
78
79
let counter = Counter ( name: name, labels: [ ] )
79
80
store [ name] = . counter( counter)
@@ -106,6 +107,9 @@ public final class PrometheusCollectorRegistry: Sendable {
106
107
return self . makeCounter ( name: name)
107
108
}
108
109
110
+ let name = name. ensureValidMetricName ( )
111
+ let labels = labels. ensureValidLabelNames ( )
112
+
109
113
return self . box. withLockedValue { store -> Counter in
110
114
guard let value = store [ name] else {
111
115
let labelNames = labels. allLabelNames
@@ -154,7 +158,8 @@ public final class PrometheusCollectorRegistry: Sendable {
154
158
/// - Parameter name: A name to identify ``Gauge``'s value.
155
159
/// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
156
160
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
158
163
guard let value = store [ name] else {
159
164
let gauge = Gauge ( name: name, labels: [ ] )
160
165
store [ name] = . gauge( gauge)
@@ -187,6 +192,9 @@ public final class PrometheusCollectorRegistry: Sendable {
187
192
return self . makeGauge ( name: name)
188
193
}
189
194
195
+ let name = name. ensureValidMetricName ( )
196
+ let labels = labels. ensureValidLabelNames ( )
197
+
190
198
return self . box. withLockedValue { store -> Gauge in
191
199
guard let value = store [ name] else {
192
200
let labelNames = labels. allLabelNames
@@ -236,7 +244,8 @@ public final class PrometheusCollectorRegistry: Sendable {
236
244
/// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram``
237
245
/// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry``
238
246
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
240
249
guard let value = store [ name] else {
241
250
let gauge = DurationHistogram ( name: name, labels: [ ] , buckets: buckets)
242
251
store [ name] = . durationHistogram( gauge)
@@ -274,6 +283,9 @@ public final class PrometheusCollectorRegistry: Sendable {
274
283
return self . makeDurationHistogram ( name: name, buckets: buckets)
275
284
}
276
285
286
+ let name = name. ensureValidMetricName ( )
287
+ let labels = labels. ensureValidLabelNames ( )
288
+
277
289
return self . box. withLockedValue { store -> DurationHistogram in
278
290
guard let value = store [ name] else {
279
291
let labelNames = labels. allLabelNames
@@ -335,7 +347,8 @@ public final class PrometheusCollectorRegistry: Sendable {
335
347
/// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram``
336
348
/// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry``
337
349
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
339
352
guard let value = store [ name] else {
340
353
let gauge = ValueHistogram ( name: name, labels: [ ] , buckets: buckets)
341
354
store [ name] = . valueHistogram( gauge)
@@ -364,6 +377,9 @@ public final class PrometheusCollectorRegistry: Sendable {
364
377
return self . makeValueHistogram ( name: name, buckets: buckets)
365
378
}
366
379
380
+ let name = name. ensureValidMetricName ( )
381
+ let labels = labels. ensureValidLabelNames ( )
382
+
367
383
return self . box. withLockedValue { store -> ValueHistogram in
368
384
guard let value = store [ name] else {
369
385
let labelNames = labels. allLabelNames
@@ -560,6 +576,14 @@ extension [(String, String)] {
560
576
result = result. sorted ( )
561
577
return result
562
578
}
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
+ }
563
587
}
564
588
565
589
extension [ UInt8 ] {
@@ -595,3 +619,91 @@ extension PrometheusMetric {
595
619
return prerendered
596
620
}
597
621
}
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