Skip to content

Sanitize Dimensions #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
build/
.swiftpm/
.idea
.vscode/
99 changes: 15 additions & 84 deletions Sources/Prometheus/PrometheusMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,84 +124,6 @@ private class MetricsSummary: TimerHandler {
}
}

/// Used to sanitize labels into a format compatible with Prometheus label requirements.
/// Useful when using `PrometheusMetrics` via `SwiftMetrics` with clients which do not necessarily know
/// about prometheus label formats, and may be using e.g. `.` or upper-case letters in labels (which Prometheus
/// does not allow).
///
/// let sanitizer: LabelSanitizer = ...
/// let prometheusLabel = sanitizer.sanitize(nonPrometheusLabel)
///
/// By default `PrometheusLabelSanitizer` is used by `PrometheusMetricsFactory`
public protocol LabelSanitizer {
/// Sanitize the passed in label to a Prometheus accepted value.
///
/// - parameters:
/// - label: The created label that needs to be sanitized.
///
/// - returns: A sanitized string that a Prometheus backend will accept.
func sanitize(_ label: String) -> String
}

/// Default implementation of `LabelSanitizer` that sanitizes any characters not
/// allowed by Prometheus to an underscore (`_`).
///
/// See `https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels` for more info.
public struct PrometheusLabelSanitizer: LabelSanitizer {
private static let uppercaseAThroughZ = UInt8(ascii: "A") ... UInt8(ascii: "Z")
private static let lowercaseAThroughZ = UInt8(ascii: "a") ... UInt8(ascii: "z")
private static let zeroThroughNine = UInt8(ascii: "0") ... UInt8(ascii: "9")

public init() { }

public func sanitize(_ label: String) -> String {
if PrometheusLabelSanitizer.isSanitized(label) {
return label
} else {
return PrometheusLabelSanitizer.sanitizeLabel(label)
}
}

/// Returns a boolean indicating whether the label is already sanitized.
private static func isSanitized(_ label: String) -> Bool {
return label.utf8.allSatisfy(PrometheusLabelSanitizer.isValidCharacter(_:))
}

/// Returns a boolean indicating whether the character may be used in a label.
private static func isValidCharacter(_ codePoint: String.UTF8View.Element) -> Bool {
switch codePoint {
case PrometheusLabelSanitizer.lowercaseAThroughZ,
PrometheusLabelSanitizer.zeroThroughNine,
UInt8(ascii: ":"),
UInt8(ascii: "_"):
return true
default:
return false
}
}

private static func sanitizeLabel(_ label: String) -> String {
let sanitized: [UInt8] = label.utf8.map { character in
if PrometheusLabelSanitizer.isValidCharacter(character) {
return character
} else {
return PrometheusLabelSanitizer.sanitizeCharacter(character)
}
}

return String(decoding: sanitized, as: UTF8.self)
}

private static func sanitizeCharacter(_ character: UInt8) -> UInt8 {
if PrometheusLabelSanitizer.uppercaseAThroughZ.contains(character) {
// Uppercase, so shift to lower case.
return character + (UInt8(ascii: "a") - UInt8(ascii: "A"))
} else {
return UInt8(ascii: "_")
}
}
}

/// Defines the base for a bridge between PrometheusClient and swift-metrics.
/// Used by `SwiftMetrics.prometheus()` to get an instance of `PrometheusClient` from `MetricsSystem`
///
Expand Down Expand Up @@ -260,13 +182,13 @@ public struct PrometheusMetricsFactory: PrometheusWrappedMetricsFactory {
public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
let label = configuration.labelSanitizer.sanitize(label)
let counter = client.createCounter(forType: Int64.self, named: label, withLabelType: DimensionLabels.self)
return MetricsCounter(counter: counter, dimensions: dimensions)
return MetricsCounter(counter: counter, dimensions: dimensions.sanitized())
}

public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler {
let label = configuration.labelSanitizer.sanitize(label)
let counter = client.createCounter(forType: Double.self, named: label, withLabelType: DimensionLabels.self)
return MetricsFloatingPointCounter(counter: counter, dimensions: dimensions)
return MetricsFloatingPointCounter(counter: counter, dimensions: dimensions.sanitized())
}

public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
Expand All @@ -277,13 +199,13 @@ public struct PrometheusMetricsFactory: PrometheusWrappedMetricsFactory {
private func makeGauge(label: String, dimensions: [(String, String)]) -> RecorderHandler {
let label = configuration.labelSanitizer.sanitize(label)
let gauge = client.createGauge(forType: Double.self, named: label, withLabelType: DimensionLabels.self)
return MetricsGauge(gauge: gauge, dimensions: dimensions)
return MetricsGauge(gauge: gauge, dimensions: dimensions.sanitized())
}

private func makeHistogram(label: String, dimensions: [(String, String)]) -> RecorderHandler {
let label = configuration.labelSanitizer.sanitize(label)
let histogram = client.createHistogram(forType: Double.self, named: label, labels: DimensionHistogramLabels.self)
return MetricsHistogram(histogram: histogram, dimensions: dimensions)
return MetricsHistogram(histogram: histogram, dimensions: dimensions.sanitized())
}

public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
Expand All @@ -300,15 +222,24 @@ public struct PrometheusMetricsFactory: PrometheusWrappedMetricsFactory {
private func makeSummaryTimer(label: String, dimensions: [(String, String)], quantiles: [Double]) -> TimerHandler {
let label = configuration.labelSanitizer.sanitize(label)
let summary = client.createSummary(forType: Int64.self, named: label, quantiles: quantiles, labels: DimensionSummaryLabels.self)
return MetricsSummary(summary: summary, dimensions: dimensions)
return MetricsSummary(summary: summary, dimensions: dimensions.sanitized())
}

/// There's two different ways to back swift-api `Timer` with Prometheus classes.
/// This method creates `Histogram` backed timer implementation
private func makeHistogramTimer(label: String, dimensions: [(String, String)], buckets: Buckets) -> TimerHandler {
let label = configuration.labelSanitizer.sanitize(label)
let histogram = client.createHistogram(forType: Int64.self, named: label, buckets: buckets, labels: DimensionHistogramLabels.self)
return MetricsHistogramTimer(histogram: histogram, dimensions: dimensions)
return MetricsHistogramTimer(histogram: histogram, dimensions: dimensions.sanitized())
}
}

extension Array where Element == (String, String) {
func sanitized() -> [(String, String)] {
let sanitizer = DimensionsSanitizer()
return self.map {
(sanitizer.sanitize($0.0), $0.1)
}
}
}

Expand Down
55 changes: 55 additions & 0 deletions Sources/Prometheus/Sanitizer/DimensionsSanitizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
struct DimensionsSanitizer: LabelSanitizer {
private static let uppercaseAThroughZ = UInt8(ascii: "A") ... UInt8(ascii: "Z")
private static let lowercaseAThroughZ = UInt8(ascii: "a") ... UInt8(ascii: "z")
private static let zeroThroughNine = UInt8(ascii: "0") ... UInt8(ascii: "9")

public init() { }

public func sanitize(_ label: String) -> String {
if DimensionsSanitizer.isSanitized(label) {
return label
} else {
return DimensionsSanitizer.sanitizeLabel(label)
}
}

/// Returns a boolean indicating whether the label is already sanitized.
private static func isSanitized(_ label: String) -> Bool {
return label.utf8.allSatisfy(DimensionsSanitizer.isValidCharacter(_:))
}

/// Returns a boolean indicating whether the character may be used in a label.
private static func isValidCharacter(_ codePoint: String.UTF8View.Element) -> Bool {
switch codePoint {
case DimensionsSanitizer.lowercaseAThroughZ,
DimensionsSanitizer.uppercaseAThroughZ,
DimensionsSanitizer.zeroThroughNine,
UInt8(ascii: ":"),
UInt8(ascii: "_"):
return true
default:
return false
}
}

private static func sanitizeLabel(_ label: String) -> String {
let sanitized: [UInt8] = label.utf8.map { character in
if DimensionsSanitizer.isValidCharacter(character) {
return character
} else {
return DimensionsSanitizer.sanitizeCharacter(character)
}
}

return String(decoding: sanitized, as: UTF8.self)
}

private static func sanitizeCharacter(_ character: UInt8) -> UInt8 {
if DimensionsSanitizer.uppercaseAThroughZ.contains(character) {
// Uppercase, so shift to lower case.
return character + (UInt8(ascii: "a") - UInt8(ascii: "A"))
} else {
return UInt8(ascii: "_")
}
}
}
18 changes: 18 additions & 0 deletions Sources/Prometheus/Sanitizer/LabelSanitizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// Used to sanitize labels into a format compatible with Prometheus label requirements.
/// Useful when using `PrometheusMetrics` via `SwiftMetrics` with clients which do not necessarily know
/// about prometheus label formats, and may be using e.g. `.` or upper-case letters in labels (which Prometheus
/// does not allow).
///
/// let sanitizer: LabelSanitizer = ...
/// let prometheusLabel = sanitizer.sanitize(nonPrometheusLabel)
///
/// By default `PrometheusLabelSanitizer` is used by `PrometheusMetricsFactory`
public protocol LabelSanitizer {
/// Sanitize the passed in label to a Prometheus accepted value.
///
/// - parameters:
/// - label: The created label that needs to be sanitized.
///
/// - returns: A sanitized string that a Prometheus backend will accept.
func sanitize(_ label: String) -> String
}
58 changes: 58 additions & 0 deletions Sources/Prometheus/Sanitizer/PrometheusLabelSanitizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// Default implementation of `LabelSanitizer` that sanitizes any characters not
/// allowed by Prometheus to an underscore (`_`).
///
/// See `https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels` for more info.
public struct PrometheusLabelSanitizer: LabelSanitizer {
private static let uppercaseAThroughZ = UInt8(ascii: "A") ... UInt8(ascii: "Z")
private static let lowercaseAThroughZ = UInt8(ascii: "a") ... UInt8(ascii: "z")
private static let zeroThroughNine = UInt8(ascii: "0") ... UInt8(ascii: "9")

public init() { }

public func sanitize(_ label: String) -> String {
if PrometheusLabelSanitizer.isSanitized(label) {
return label
} else {
return PrometheusLabelSanitizer.sanitizeLabel(label)
}
}

/// Returns a boolean indicating whether the label is already sanitized.
private static func isSanitized(_ label: String) -> Bool {
return label.utf8.allSatisfy(PrometheusLabelSanitizer.isValidCharacter(_:))
}

/// Returns a boolean indicating whether the character may be used in a label.
private static func isValidCharacter(_ codePoint: String.UTF8View.Element) -> Bool {
switch codePoint {
case PrometheusLabelSanitizer.lowercaseAThroughZ,
PrometheusLabelSanitizer.zeroThroughNine,
UInt8(ascii: ":"),
UInt8(ascii: "_"):
return true
default:
return false
}
}

private static func sanitizeLabel(_ label: String) -> String {
let sanitized: [UInt8] = label.utf8.map { character in
if PrometheusLabelSanitizer.isValidCharacter(character) {
return character
} else {
return PrometheusLabelSanitizer.sanitizeCharacter(character)
}
}

return String(decoding: sanitized, as: UTF8.self)
}

private static func sanitizeCharacter(_ character: UInt8) -> UInt8 {
if PrometheusLabelSanitizer.uppercaseAThroughZ.contains(character) {
// Uppercase, so shift to lower case.
return character + (UInt8(ascii: "a") - UInt8(ascii: "A"))
} else {
return UInt8(ascii: "_")
}
}
}
16 changes: 16 additions & 0 deletions Tests/SwiftPrometheusTests/SanitizerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,20 @@ final class SanitizerTests: XCTestCase {
test_counter 10\n
""")
}

func testIntegratedSanitizerForDimensions() throws {
let prom = PrometheusClient()
MetricsSystem.bootstrapInternal(PrometheusMetricsFactory(client: prom))

let dimensions: [(String, String)] = [("invalid-service.dimension", "something")]
CoreMetrics.Counter(label: "dimensions_total", dimensions: dimensions).increment()

let promise = eventLoop.makePromise(of: String.self)
prom.collect(into: promise)
XCTAssertEqual(try! promise.futureResult.wait(), """
# TYPE dimensions_total counter
dimensions_total 0
dimensions_total{invalid_service_dimension="something"} 1\n
""")
}
}