@@ -86,6 +86,41 @@ private class MetricsSummary: TimerHandler {
8686 }
8787}
8888
89+ /// Used to sanitize labels into a format compatible with Prometheus label requirements.
90+ /// Useful when using `PrometheusMetrics` via `SwiftMetrics` with clients which do not necessarily know
91+ /// about prometheus label formats, and may be using e.g. `.` or upper-case letters in labels (which Prometheus
92+ /// does not allow).
93+ ///
94+ /// let sanitizer: LabelSanitizer = ...
95+ /// let prometheusLabel = sanitizer.sanitize(nonPrometheusLabel)
96+ ///
97+ /// By default `PrometheusLabelSanitizer` is used by `PrometheusClient`
98+ public protocol LabelSanitizer {
99+ /// Sanitize the passed in label to a Prometheus accepted value.
100+ ///
101+ /// - parameters:
102+ /// - label: The created label that needs to be sanitized.
103+ ///
104+ /// - returns: A sanitized string that a Prometheus backend will accept.
105+ func sanitize( _ label: String ) -> String
106+ }
107+
108+ /// Default implementation of `LabelSanitizer` that sanitizes any characters not
109+ /// allowed by Prometheus to an underscore (`_`).
110+ ///
111+ /// See `https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels` for more info.
112+ public struct PrometheusLabelSanitizer : LabelSanitizer {
113+ let allowedCharacters = " abcdefghijklmnopqrstuvwxyz0123456789_: "
114+
115+ public init ( ) { }
116+
117+ public func sanitize( _ label: String ) -> String {
118+ return String ( label
119+ . lowercased ( )
120+ . map { ( c: Character ) -> Character in if allowedCharacters. contains ( c) { return c } ; return " _ " } )
121+ }
122+ }
123+
89124extension PrometheusClient : MetricsFactory {
90125 public func destroyCounter( _ handler: CounterHandler ) {
91126 guard let handler = handler as? MetricsCounter else { return }
@@ -107,6 +142,7 @@ extension PrometheusClient: MetricsFactory {
107142 }
108143
109144 public func makeCounter( label: String , dimensions: [ ( String , String ) ] ) -> CounterHandler {
145+ let label = self . sanitizer. sanitize ( label)
110146 let createHandler = { ( counter: PromCounter ) -> CounterHandler in
111147 return MetricsCounter ( counter: counter, dimensions: dimensions)
112148 }
@@ -117,10 +153,12 @@ extension PrometheusClient: MetricsFactory {
117153 }
118154
119155 public func makeRecorder( label: String , dimensions: [ ( String , String ) ] , aggregate: Bool ) -> RecorderHandler {
156+ let label = self . sanitizer. sanitize ( label)
120157 return aggregate ? makeHistogram ( label: label, dimensions: dimensions) : makeGauge ( label: label, dimensions: dimensions)
121158 }
122159
123160 private func makeGauge( label: String , dimensions: [ ( String , String ) ] ) -> RecorderHandler {
161+ let label = self . sanitizer. sanitize ( label)
124162 let createHandler = { ( gauge: PromGauge ) -> RecorderHandler in
125163 return MetricsGauge ( gauge: gauge, dimensions: dimensions)
126164 }
@@ -131,6 +169,7 @@ extension PrometheusClient: MetricsFactory {
131169 }
132170
133171 private func makeHistogram( label: String , dimensions: [ ( String , String ) ] ) -> RecorderHandler {
172+ let label = self . sanitizer. sanitize ( label)
134173 let createHandler = { ( histogram: PromHistogram ) -> RecorderHandler in
135174 return MetricsHistogram ( histogram: histogram, dimensions: dimensions)
136175 }
@@ -141,6 +180,7 @@ extension PrometheusClient: MetricsFactory {
141180 }
142181
143182 public func makeTimer( label: String , dimensions: [ ( String , String ) ] ) -> TimerHandler {
183+ let label = self . sanitizer. sanitize ( label)
144184 let createHandler = { ( summary: PromSummary ) -> TimerHandler in
145185 return MetricsSummary ( summary: summary, dimensions: dimensions)
146186 }
0 commit comments