diff --git a/.gitignore b/.gitignore index 640228a..c338185 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build/ .swiftpm/ .idea +.vscode/ diff --git a/Package.swift b/Package.swift index ad3d5b5..c8c37ed 100644 --- a/Package.swift +++ b/Package.swift @@ -7,10 +7,7 @@ let package = Package( products: [ .library( name: "SwiftPrometheus", - targets: ["Prometheus"]), - .executable( - name: "PrometheusExample", - targets: ["PrometheusExample"]), + targets: ["Prometheus"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-metrics.git", from: "2.2.0"), diff --git a/README.md b/README.md index df4be3a..0b80c45 100644 --- a/README.md +++ b/README.md @@ -102,20 +102,18 @@ summary.observe(4.7) // Observe the given value ``` ## Labels -All metric types support adding labels, allowing for grouping of related metrics. +All metric types support adding labels, allowing for grouping of related metrics. Labels are passed when recording values to your metric as an instance of `DimensionLabels`, or as an array of `(String, String)`. Example with a counter: ```swift -struct RouteLabels: MetricLabels { - var route: String = "*" -} - -let counter = myProm.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter", withLabelType: RouteLabels.self) +let counter = myProm.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter") -let counter = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter", withLabelType: RouteLabels.self) +let counter = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter") -counter.inc(12, .init(route: "/")) +counter.inc(12, .init([("route", "/users")])) +// OR +counter.inc(12, [("route", "/users")]) ``` # Exporting @@ -125,16 +123,8 @@ Prometheus itself is designed to "pull" metrics from a destination. Following th By default, this should be accessible on your main serving port, at the `/metrics` endpoint. An example in [Vapor](https://vapor.codes) 4 syntax looks like: ```swift -app.get("metrics") { req -> EventLoopFuture in - let promise = req.eventLoop.makePromise(of: String.self) - DispatchQueue.global().async { - do { - try MetricsSystem.prometheus().collect(into: promise) - } catch { - promise.fail(error) - } - } - return promise.futureResult +app.get("metrics") { req async throws -> String in + return try await MetricsSystem.prometheus().collect() } ``` diff --git a/Sources/Prometheus/MetricTypes/Counter.swift b/Sources/Prometheus/MetricTypes/Counter.swift index d87907e..f562a58 100644 --- a/Sources/Prometheus/MetricTypes/Counter.swift +++ b/Sources/Prometheus/MetricTypes/Counter.swift @@ -3,7 +3,7 @@ import NIOConcurrencyHelpers /// Prometheus Counter metric /// /// See: https://prometheus.io/docs/concepts/metric_types/#counter -public class PromCounter: PromMetric, PrometheusHandled { +public class PromCounter: PromMetric, PrometheusHandled { /// Prometheus instance that created this Counter internal weak var prometheus: PrometheusClient? @@ -22,7 +22,7 @@ public class PromCounter: PromMetric, Pr private let initialValue: NumType /// Storage of values that have labels attached - internal var metrics: [Labels: NumType] = [:] + internal var metrics: [DimensionLabels: NumType] = [:] /// Lock used for thread safety internal let lock: Lock @@ -75,7 +75,7 @@ public class PromCounter: PromMetric, Pr /// - labels: Labels to attach to the value /// @discardableResult - public func inc(_ amount: NumType = 1, _ labels: Labels? = nil) -> NumType { + public func inc(_ amount: NumType = 1, _ labels: DimensionLabels? = nil) -> NumType { return self.lock.withLock { if let labels = labels { var val = self.metrics[labels] ?? self.initialValue @@ -95,7 +95,7 @@ public class PromCounter: PromMetric, Pr /// - labels: Labels to get the value for /// /// - Returns: The value of the Counter attached to the provided labels - public func get(_ labels: Labels? = nil) -> NumType { + public func get(_ labels: DimensionLabels? = nil) -> NumType { return self.lock.withLock { if let labels = labels { return self.metrics[labels] ?? initialValue diff --git a/Sources/Prometheus/MetricTypes/Gauge.swift b/Sources/Prometheus/MetricTypes/Gauge.swift index ac57a92..38fe198 100644 --- a/Sources/Prometheus/MetricTypes/Gauge.swift +++ b/Sources/Prometheus/MetricTypes/Gauge.swift @@ -5,7 +5,7 @@ import NIOConcurrencyHelpers /// Prometheus Gauge metric /// /// See https://prometheus.io/docs/concepts/metric_types/#gauge -public class PromGauge: PromMetric, PrometheusHandled { +public class PromGauge: PromMetric, PrometheusHandled { /// Prometheus instance that created this Gauge internal weak var prometheus: PrometheusClient? @@ -24,7 +24,7 @@ public class PromGauge: Prom private let initialValue: NumType /// Storage of values that have labels attached - private var metrics: [Labels: NumType] = [:] + private var metrics: [DimensionLabels: NumType] = [:] /// Lock used for thread safety private let lock: Lock @@ -78,7 +78,7 @@ public class PromGauge: Prom /// /// - Returns: The value of the Gauge attached to the provided labels @discardableResult - public func setToCurrentTime(_ labels: Labels? = nil) -> NumType { + public func setToCurrentTime(_ labels: DimensionLabels? = nil) -> NumType { return self.set(.init(Date().timeIntervalSince1970), labels) } @@ -94,7 +94,7 @@ public class PromGauge: Prom /// /// - Returns: The same type of function passed in for `body`, but wrapped to track progress. @inlinable - public func trackInProgress(_ labels: Labels? = nil, _ body: @escaping () throws -> T) -> (() throws -> T) { + public func trackInProgress(_ labels: DimensionLabels? = nil, _ body: @escaping () throws -> T) -> (() throws -> T) { return { self.inc() defer { @@ -109,7 +109,7 @@ public class PromGauge: Prom /// - labels: Labels to attach to the resulting value. /// - body: Closure to run & record execution time of. @inlinable - public func time(_ labels: Labels? = nil, _ body: @escaping () throws -> T) rethrows -> T { + public func time(_ labels: DimensionLabels? = nil, _ body: @escaping () throws -> T) rethrows -> T { let start = DispatchTime.now().uptimeNanoseconds defer { let delta = Double(DispatchTime.now().uptimeNanoseconds - start) @@ -127,7 +127,7 @@ public class PromGauge: Prom /// /// - Returns: The value of the Gauge attached to the provided labels @discardableResult - public func set(_ amount: NumType, _ labels: Labels? = nil) -> NumType { + public func set(_ amount: NumType, _ labels: DimensionLabels? = nil) -> NumType { return self.lock.withLock { if let labels = labels { self.metrics[labels] = amount @@ -147,7 +147,7 @@ public class PromGauge: Prom /// /// - Returns: The value of the Gauge attached to the provided labels @discardableResult - public func inc(_ amount: NumType, _ labels: Labels? = nil) -> NumType { + public func inc(_ amount: NumType, _ labels: DimensionLabels? = nil) -> NumType { return self.lock.withLock { if let labels = labels { var val = self.metrics[labels] ?? self.initialValue @@ -168,7 +168,7 @@ public class PromGauge: Prom /// /// - Returns: The value of the Gauge attached to the provided labels @discardableResult - public func inc(_ labels: Labels? = nil) -> NumType { + public func inc(_ labels: DimensionLabels? = nil) -> NumType { return self.inc(1, labels) } @@ -180,7 +180,7 @@ public class PromGauge: Prom /// /// - Returns: The value of the Gauge attached to the provided labels @discardableResult - public func dec(_ amount: NumType, _ labels: Labels? = nil) -> NumType { + public func dec(_ amount: NumType, _ labels: DimensionLabels? = nil) -> NumType { return self.lock.withLock { if let labels = labels { var val = self.metrics[labels] ?? self.initialValue @@ -201,7 +201,7 @@ public class PromGauge: Prom /// /// - Returns: The value of the Gauge attached to the provided labels @discardableResult - public func dec(_ labels: Labels? = nil) -> NumType { + public func dec(_ labels: DimensionLabels? = nil) -> NumType { return self.dec(1, labels) } @@ -211,7 +211,7 @@ public class PromGauge: Prom /// - labels: Labels to get the value for /// /// - Returns: The value of the Gauge attached to the provided labels - public func get(_ labels: Labels? = nil) -> NumType { + public func get(_ labels: DimensionLabels? = nil) -> NumType { return self.lock.withLock { if let labels = labels { return self.metrics[labels] ?? initialValue diff --git a/Sources/Prometheus/MetricTypes/Histogram.swift b/Sources/Prometheus/MetricTypes/Histogram.swift index 022ad03..2c1987b 100644 --- a/Sources/Prometheus/MetricTypes/Histogram.swift +++ b/Sources/Prometheus/MetricTypes/Histogram.swift @@ -63,24 +63,10 @@ public struct Buckets: ExpressibleByArrayLiteral { } } -/// Label type Histograms can use -public protocol HistogramLabels: MetricLabels { - /// Bucket - var le: String { get set } -} - -extension HistogramLabels { - /// Creates empty HistogramLabels - init() { - self.init() - self.le = "" - } -} - /// Prometheus Histogram metric /// /// See https://prometheus.io/docs/concepts/metric_types/#Histogram -public class PromHistogram: PromMetric, PrometheusHandled { +public class PromHistogram: PromMetric, PrometheusHandled { /// Prometheus instance that created this Histogram internal weak var prometheus: PrometheusClient? @@ -93,19 +79,16 @@ public class PromHistogram] = [] + private var buckets: [PromCounter] = [] /// Buckets used by this Histogram internal let upperBounds: [Double] - /// Labels for this Histogram - internal let labels: Labels - /// Sub Histograms for this Histogram - fileprivate var subHistograms: [Labels: PromHistogram] = [:] + fileprivate var subHistograms: [DimensionLabels: PromHistogram] = [:] /// Total value of the Histogram - private let sum: PromCounter + private let sum: PromCounter /// Lock used for thread safety private let lock: Lock @@ -115,10 +98,9 @@ public class PromHistogram String { - let (buckets, subHistograms, labels) = self.lock.withLock { - (self.buckets, self.subHistograms, self.labels) + let (buckets, subHistograms) = self.lock.withLock { + (self.buckets, self.subHistograms) } var output = [String]() @@ -157,13 +137,13 @@ public class PromHistogram], + private func collectBuckets(buckets: [PromCounter], upperBounds: [Double], name: String, - labels: Labels, + labels: DimensionLabels?, sum: NumType, into output: inout [String]) { - var labels = labels var acc: NumType = 0 for (i, bound) in upperBounds.enumerated() { acc += buckets[i].get() - labels.le = bound.description - let labelsString = encodeLabels(labels) + let labelsString = encodeLabels(EncodableHistogramLabels(labels: labels, le: bound.description)) output.append("\(name)_bucket\(labelsString) \(acc)") } - let labelsString = encodeLabels(labels, ["le"]) + let labelsString = encodeLabels(EncodableHistogramLabels(labels: labels)) output.append("\(name)_count\(labelsString) \(acc)") output.append("\(name)_sum\(labelsString) \(sum)") @@ -201,8 +179,8 @@ public class PromHistogram(_ labels: Labels? = nil, _ body: @escaping () throws -> T) rethrows -> T { + public func time(_ labels: DimensionLabels? = nil, _ body: @escaping () throws -> T) rethrows -> T { let start = DispatchTime.now().uptimeNanoseconds defer { let delta = Double(DispatchTime.now().uptimeNanoseconds - start) @@ -232,7 +210,7 @@ public class PromHistogram PromHistogram { + fileprivate func getOrCreateHistogram(with labels: DimensionLabels) -> PromHistogram { let subHistograms = lock.withLock { self.subHistograms } if let histogram = subHistograms[labels] { precondition(histogram.name == self.name, @@ -262,7 +240,7 @@ public class PromHistogram: PromMetric, PrometheusHandled { +public class PromSummary: PromMetric, PrometheusHandled { /// Prometheus instance that created this Summary internal weak var prometheus: PrometheusClient? @@ -34,14 +20,11 @@ public class PromSummary: P private var displayUnit: TimeUnit? - /// Labels for this Summary - internal private(set) var labels: Labels - /// Sum of the values in this Summary - private let sum: PromCounter + private let sum: PromCounter /// Amount of values in this Summary - private let count: PromCounter + private let count: PromCounter /// Values in this Summary private var values: CircularBuffer @@ -53,7 +36,7 @@ public class PromSummary: P internal let quantiles: [Double] /// Sub Summaries for this Summary - fileprivate var subSummaries: [Labels: PromSummary] = [:] + fileprivate var subSummaries: [DimensionLabels: PromSummary] = [:] /// Lock used for thread safety private let lock: Lock @@ -63,11 +46,10 @@ public class PromSummary: P /// - Parameters: /// - name: Name of the Summary /// - help: Help text of the Summary - /// - labels: Labels for the Summary /// - capacity: Number of values to keep for calculating quantiles /// - quantiles: Quantiles to use for the Summary /// - p: Prometheus instance creating this Summary - internal init(_ name: String, _ help: String? = nil, _ labels: Labels = Labels(), _ capacity: Int = Prometheus.defaultSummaryCapacity, _ quantiles: [Double] = Prometheus.defaultQuantiles, _ p: PrometheusClient) { + internal init(_ name: String, _ help: String? = nil, _ capacity: Int = Prometheus.defaultSummaryCapacity, _ quantiles: [Double] = Prometheus.defaultQuantiles, _ p: PrometheusClient) { self.name = name self.help = help @@ -84,9 +66,7 @@ public class PromSummary: P self.capacity = capacity self.quantiles = quantiles - - self.labels = labels - + self.lock = Lock() } @@ -107,29 +87,24 @@ public class PromSummary: P output.append("# HELP \(self.name) \(help)") } output.append("# TYPE \(self.name) \(self._type)") - var labels = self.labels calculateQuantiles(quantiles: self.quantiles, values: values.map { $0.doubleValue }).sorted { $0.key < $1.key }.forEach { (arg) in let (q, v) = arg - labels.quantile = "\(q)" - let labelsString = encodeLabels(labels) + let labelsString = encodeLabels(EncodableSummaryLabels(labels: nil, quantile: "\(q)")) output.append("\(self.name)\(labelsString) \(format(v))") } - let labelsString = encodeLabels(labels, ["quantile"]) - output.append("\(self.name)_count\(labelsString) \(self.count.get())") - output.append("\(self.name)_sum\(labelsString) \(format(self.sum.get().doubleValue))") + output.append("\(self.name)_count \(self.count.get())") + output.append("\(self.name)_sum \(format(self.sum.get().doubleValue))") - subSummaries.values.forEach { subSum in - var subSumLabels = subSum.labels + subSummaries.forEach { labels, subSum in let subSumValues = lock.withLock { subSum.values } calculateQuantiles(quantiles: self.quantiles, values: subSumValues.map { $0.doubleValue }).sorted { $0.key < $1.key }.forEach { (arg) in let (q, v) = arg - subSumLabels.quantile = "\(q)" - let labelsString = encodeLabels(subSumLabels) + let labelsString = encodeLabels(EncodableSummaryLabels(labels: labels, quantile: "\(q)")) output.append("\(subSum.name)\(labelsString) \(format(v))") } - let labelsString = encodeLabels(subSumLabels, ["quantile"]) + let labelsString = encodeLabels(EncodableSummaryLabels(labels: labels, quantile: nil)) output.append("\(subSum.name)_count\(labelsString) \(subSum.count.get())") output.append("\(subSum.name)_sum\(labelsString) \(format(subSum.sum.get().doubleValue))") } @@ -164,8 +139,8 @@ public class PromSummary: P /// - Parameters: /// - value: Value to observe /// - labels: Labels to attach to the observed value - public func observe(_ value: NumType, _ labels: Labels? = nil) { - if let labels = labels, type(of: labels) != type(of: EmptySummaryLabels()) { + public func observe(_ value: NumType, _ labels: DimensionLabels? = nil) { + if let labels = labels { let sum = self.getOrCreateSummary(withLabels: labels) sum.observe(value) } @@ -185,7 +160,7 @@ public class PromSummary: P /// - labels: Labels to attach to the resulting value. /// - body: Closure to run & record. @inlinable - public func time(_ labels: Labels? = nil, _ body: @escaping () throws -> T) rethrows -> T { + public func time(_ labels: DimensionLabels? = nil, _ body: @escaping () throws -> T) rethrows -> T { let start = DispatchTime.now().uptimeNanoseconds defer { let delta = Double(DispatchTime.now().uptimeNanoseconds - start) @@ -193,7 +168,7 @@ public class PromSummary: P } return try body() } - fileprivate func getOrCreateSummary(withLabels labels: Labels) -> PromSummary { + fileprivate func getOrCreateSummary(withLabels labels: DimensionLabels) -> PromSummary { let subSummaries = self.lock.withLock { self.subSummaries } if let summary = subSummaries[labels] { precondition(summary.name == self.name, @@ -225,7 +200,7 @@ public class PromSummary: P guard let prometheus = prometheus else { fatalError("Lingering Summary") } - let newSummary = PromSummary(self.name, self.help, labels, self.capacity, self.quantiles, prometheus) + let newSummary = PromSummary(self.name, self.help, self.capacity, self.quantiles, prometheus) self.subSummaries[labels] = newSummary return newSummary } diff --git a/Sources/Prometheus/Prometheus.swift b/Sources/Prometheus/Prometheus.swift index 91aa85e..0133eaa 100644 --- a/Sources/Prometheus/Prometheus.swift +++ b/Sources/Prometheus/Prometheus.swift @@ -19,7 +19,21 @@ public class PrometheusClient { } // MARK: - Collection - + +#if swift(>=5.5) + /// Creates prometheus formatted metrics + /// + /// - returns: A newline separated string with metrics for all Metrics this PrometheusClient handles + @available(macOS 10.15.0, *) + public func collect() async -> String { + let metrics = self.lock.withLock { self.metrics } + let task = Task { + return metrics.isEmpty ? "" : "\(metrics.values.map { $0.collect() }.joined(separator: "\n"))\n" + } + return await task.value + } +#endif + /// Creates prometheus formatted metrics /// /// - Parameters: @@ -36,7 +50,26 @@ public class PrometheusClient { public func collect(into promise: EventLoopPromise) { collect(promise.succeed) } - + +#if swift(>=5.5) + /// Creates prometheus formatted metrics + /// + /// - returns: A `ByteBuffer` containing a newline separated string with metrics for all Metrics this PrometheusClient handles + @available(macOS 10.15.0, *) + public func collect() async -> ByteBuffer { + let metrics = self.lock.withLock { self.metrics } + let task = Task { () -> ByteBuffer in + var buffer = ByteBufferAllocator().buffer(capacity: 0) + metrics.values.forEach { + $0.collect(into: &buffer) + buffer.writeString("\n") + } + return buffer + } + return await task.value + } +#endif + /// Creates prometheus formatted metrics /// /// - Parameters: @@ -92,46 +125,26 @@ public class PrometheusClient { /// - name: Name of the counter /// - helpText: Help text for the counter. Usually a short description /// - initialValue: An initial value to set the counter to, defaults to 0 - /// - labelType: Type of labels this counter can use. Can be left out to default to no labels /// /// - Returns: Counter instance - public func createCounter( + public func createCounter( forType type: T.Type, named name: String, helpText: String? = nil, - initialValue: T = 0, - withLabelType labelType: U.Type) -> PromCounter + initialValue: T = 0) -> PromCounter { return self.lock.withLock { - if let cachedCounter: PromCounter = self._getMetricInstance(with: name, andType: .counter) { + if let cachedCounter: PromCounter = self._getMetricInstance(with: name, andType: .counter) { return cachedCounter } - let counter = PromCounter(name, helpText, initialValue, self) + let counter = PromCounter(name, helpText, initialValue, self) let oldInstrument = self.metrics.updateValue(counter, forKey: name) precondition(oldInstrument == nil, "Label \(oldInstrument!.name) is already associated with a \(oldInstrument!._type).") return counter } } - /// Creates a counter with the given values - /// - /// - Parameters: - /// - type: Type the counter will count - /// - name: Name of the counter - /// - helpText: Help text for the counter. Usually a short description - /// - initialValue: An initial value to set the counter to, defaults to 0 - /// - /// - Returns: Counter instance - public func createCounter( - forType type: T.Type, - named name: String, - helpText: String? = nil, - initialValue: T = 0) -> PromCounter - { - return self.createCounter(forType: type, named: name, helpText: helpText, initialValue: initialValue, withLabelType: EmptyLabels.self) - } - // MARK: - Gauge /// Creates a gauge with the given values @@ -141,46 +154,26 @@ public class PrometheusClient { /// - name: Name of the gauge /// - helpText: Help text for the gauge. Usually a short description /// - initialValue: An initial value to set the gauge to, defaults to 0 - /// - labelType: Type of labels this gauge can use. Can be left out to default to no labels /// /// - Returns: Gauge instance - public func createGauge( + public func createGauge( forType type: T.Type, named name: String, helpText: String? = nil, - initialValue: T = 0, - withLabelType labelType: U.Type) -> PromGauge + initialValue: T = 0) -> PromGauge { return self.lock.withLock { - if let cachedGauge: PromGauge = self._getMetricInstance(with: name, andType: .gauge) { + if let cachedGauge: PromGauge = self._getMetricInstance(with: name, andType: .gauge) { return cachedGauge } - let gauge = PromGauge(name, helpText, initialValue, self) + let gauge = PromGauge(name, helpText, initialValue, self) let oldInstrument = self.metrics.updateValue(gauge, forKey: name) precondition(oldInstrument == nil, "Label \(oldInstrument!.name) is already associated with a \(oldInstrument!._type).") return gauge } } - /// Creates a gauge with the given values - /// - /// - Parameters: - /// - type: Type the gauge will count - /// - name: Name of the gauge - /// - helpText: Help text for the gauge. Usually a short description - /// - initialValue: An initial value to set the gauge to, defaults to 0 - /// - /// - Returns: Gauge instance - public func createGauge( - forType type: T.Type, - named name: String, - helpText: String? = nil, - initialValue: T = 0) -> PromGauge - { - return self.createGauge(forType: type, named: name, helpText: helpText, initialValue: initialValue, withLabelType: EmptyLabels.self) - } - // MARK: - Histogram /// Creates a histogram with the given values @@ -190,46 +183,26 @@ public class PrometheusClient { /// - name: Name of the histogram /// - helpText: Help text for the histogram. Usually a short description /// - buckets: Buckets to divide values over - /// - labels: Labels to give this histogram. Can be left out to default to no labels /// /// - Returns: Histogram instance - public func createHistogram( + public func createHistogram( forType type: T.Type, named name: String, helpText: String? = nil, - buckets: Buckets = .defaultBuckets, - labels: U.Type) -> PromHistogram + buckets: Buckets = .defaultBuckets) -> PromHistogram { return self.lock.withLock { - if let cachedHistogram: PromHistogram = self._getMetricInstance(with: name, andType: .histogram) { + if let cachedHistogram: PromHistogram = self._getMetricInstance(with: name, andType: .histogram) { return cachedHistogram } - let histogram = PromHistogram(name, helpText, U(), buckets, self) + let histogram = PromHistogram(name, helpText, buckets, self) let oldInstrument = self.metrics.updateValue(histogram, forKey: name) precondition(oldInstrument == nil, "Label \(oldInstrument!.name) is already associated with a \(oldInstrument!._type).") return histogram } } - /// Creates a histogram with the given values - /// - /// - Parameters: - /// - type: The type the histogram will observe - /// - name: Name of the histogram - /// - helpText: Help text for the histogram. Usually a short description - /// - buckets: Buckets to divide values over - /// - /// - Returns: Histogram instance - public func createHistogram( - forType type: T.Type, - named name: String, - helpText: String? = nil, - buckets: Buckets = .defaultBuckets) -> PromHistogram - { - return self.createHistogram(forType: type, named: name, helpText: helpText, buckets: buckets, labels: EmptyHistogramLabels.self) - } - // MARK: - Summary /// Creates a summary with the given values @@ -240,45 +213,25 @@ public class PrometheusClient { /// - helpText: Help text for the summary. Usually a short description /// - capacity: Number of observations to keep for calculating quantiles /// - quantiles: Quantiles to calculate - /// - labels: Labels to give this summary. Can be left out to default to no labels /// /// - Returns: Summary instance - public func createSummary( + public func createSummary( forType type: T.Type, named name: String, helpText: String? = nil, capacity: Int = Prometheus.defaultSummaryCapacity, - quantiles: [Double] = Prometheus.defaultQuantiles, - labels: U.Type) -> PromSummary + quantiles: [Double] = Prometheus.defaultQuantiles) -> PromSummary { return self.lock.withLock { - if let cachedSummary: PromSummary = self._getMetricInstance(with: name, andType: .summary) { + if let cachedSummary: PromSummary = self._getMetricInstance(with: name, andType: .summary) { return cachedSummary } - let summary = PromSummary(name, helpText, U(), capacity, quantiles, self) + let summary = PromSummary(name, helpText, capacity, quantiles, self) let oldInstrument = self.metrics.updateValue(summary, forKey: name) precondition(oldInstrument == nil, "Label \(oldInstrument!.name) is already associated with a \(oldInstrument!._type).") return summary } } - - /// Creates a summary with the given values - /// - /// - Parameters: - /// - type: The type the summary will observe - /// - name: Name of the summary - /// - helpText: Help text for the summary. Usually a short description - /// - quantiles: Quantiles to calculate - /// - /// - Returns: Summary instance - public func createSummary( - forType type: T.Type, - named name: String, - helpText: String? = nil, - quantiles: [Double] = Prometheus.defaultQuantiles) -> PromSummary - { - return self.createSummary(forType: type, named: name, helpText: helpText, quantiles: quantiles, labels: EmptySummaryLabels.self) - } } /// Prometheus specific errors diff --git a/Sources/Prometheus/PrometheusMetrics.swift b/Sources/Prometheus/PrometheusMetrics.swift index 50581b2..cdb9261 100644 --- a/Sources/Prometheus/PrometheusMetrics.swift +++ b/Sources/Prometheus/PrometheusMetrics.swift @@ -1,10 +1,10 @@ import CoreMetrics private class MetricsCounter: CounterHandler { - let counter: PromCounter + let counter: PromCounter let labels: DimensionLabels? - - internal init(counter: PromCounter, dimensions: [(String, String)]) { + + internal init(counter: PromCounter, dimensions: [(String, String)]) { self.counter = counter guard !dimensions.isEmpty else { labels = nil @@ -12,19 +12,19 @@ private class MetricsCounter: CounterHandler { } self.labels = DimensionLabels(dimensions) } - + func increment(by: Int64) { self.counter.inc(by, labels) } - + func reset() { } } private class MetricsFloatingPointCounter: FloatingPointCounterHandler { - let counter: PromCounter + let counter: PromCounter let labels: DimensionLabels? - internal init(counter: PromCounter, dimensions: [(String, String)]) { + internal init(counter: PromCounter, dimensions: [(String, String)]) { self.counter = counter guard !dimensions.isEmpty else { labels = nil @@ -41,10 +41,10 @@ private class MetricsFloatingPointCounter: FloatingPointCounterHandler { } private class MetricsGauge: RecorderHandler { - let gauge: PromGauge + let gauge: PromGauge let labels: DimensionLabels? - - internal init(gauge: PromGauge, dimensions: [(String, String)]) { + + internal init(gauge: PromGauge, dimensions: [(String, String)]) { self.gauge = gauge guard !dimensions.isEmpty else { labels = nil @@ -52,46 +52,46 @@ private class MetricsGauge: RecorderHandler { } self.labels = DimensionLabels(dimensions) } - + func record(_ value: Int64) { self.record(value.doubleValue) } - + func record(_ value: Double) { gauge.set(value, labels) } } private class MetricsHistogram: RecorderHandler { - let histogram: PromHistogram - let labels: DimensionHistogramLabels? - - internal init(histogram: PromHistogram, dimensions: [(String, String)]) { + let histogram: PromHistogram + let labels: DimensionLabels? + + internal init(histogram: PromHistogram, dimensions: [(String, String)]) { self.histogram = histogram guard !dimensions.isEmpty else { labels = nil return } - self.labels = DimensionHistogramLabels(dimensions) + self.labels = DimensionLabels(dimensions) } - + func record(_ value: Int64) { histogram.observe(value.doubleValue, labels) } - + func record(_ value: Double) { histogram.observe(value, labels) } } class MetricsHistogramTimer: TimerHandler { - let histogram: PromHistogram - let labels: DimensionHistogramLabels? + let histogram: PromHistogram + let labels: DimensionLabels? - init(histogram: PromHistogram, dimensions: [(String, String)]) { + init(histogram: PromHistogram, dimensions: [(String, String)]) { self.histogram = histogram if !dimensions.isEmpty { - self.labels = DimensionHistogramLabels(dimensions) + self.labels = DimensionLabels(dimensions) } else { self.labels = nil } @@ -103,105 +103,27 @@ class MetricsHistogramTimer: TimerHandler { } private class MetricsSummary: TimerHandler { - let summary: PromSummary - let labels: DimensionSummaryLabels? - + let summary: PromSummary + let labels: DimensionLabels? + func preferDisplayUnit(_ unit: TimeUnit) { self.summary.preferDisplayUnit(unit) } - - internal init(summary: PromSummary, dimensions: [(String, String)]) { + + internal init(summary: PromSummary, dimensions: [(String, String)]) { self.summary = summary guard !dimensions.isEmpty else { labels = nil return } - self.labels = DimensionSummaryLabels(dimensions) + self.labels = DimensionLabels(dimensions) } - + func recordNanoseconds(_ duration: Int64) { return summary.observe(duration, labels) } } -/// 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` /// @@ -236,7 +158,7 @@ public struct PrometheusMetricsFactory: PrometheusWrappedMetricsFactory { guard let handler = handler as? MetricsFloatingPointCounter else { return } client.removeMetric(handler.counter) } - + public func destroyRecorder(_ handler: RecorderHandler) { if let handler = handler as? MetricsGauge { client.removeMetric(handler.gauge) @@ -256,36 +178,36 @@ public struct PrometheusMetricsFactory: PrometheusWrappedMetricsFactory { client.removeMetric(handler.histogram) } } - + 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) + let counter = client.createCounter(forType: Int64.self, named: label) + 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) + let counter = client.createCounter(forType: Double.self, named: label) + return MetricsFloatingPointCounter(counter: counter, dimensions: dimensions.sanitized()) } - + public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { let label = configuration.labelSanitizer.sanitize(label) return aggregate ? makeHistogram(label: label, dimensions: dimensions) : makeGauge(label: label, dimensions: dimensions) } - + 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) + let gauge = client.createGauge(forType: Double.self, named: label) + 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) + let histogram = client.createHistogram(forType: Double.self, named: label) + return MetricsHistogram(histogram: histogram, dimensions: dimensions.sanitized()) } - + public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { switch configuration.timerImplementation._wrapped { case .summary(let quantiles): @@ -299,16 +221,25 @@ public struct PrometheusMetricsFactory: PrometheusWrappedMetricsFactory { /// This method creates `Summary` backed timer implementation 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) + let summary = client.createSummary(forType: Int64.self, named: label, quantiles: quantiles) + 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) + let histogram = client.createHistogram(forType: Int64.self, named: label, buckets: buckets) + 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) + } } } @@ -332,22 +263,22 @@ public extension MetricsSystem { private struct StringCodingKey: CodingKey { /// `CodingKey` conformance. public var stringValue: String - + /// `CodingKey` conformance. public var intValue: Int? { return Int(self.stringValue) } - + /// Creates a new `StringCodingKey`. public init(_ string: String) { self.stringValue = string } - + /// `CodingKey` conformance. public init(stringValue: String) { self.stringValue = stringValue } - + /// `CodingKey` conformance. public init(intValue: Int) { self.stringValue = intValue.description @@ -355,24 +286,21 @@ private struct StringCodingKey: CodingKey { } /// Helper for dimensions -public struct DimensionLabels: MetricLabels { +public struct DimensionLabels: Hashable, ExpressibleByArrayLiteral { let dimensions: [(String, String)] - + public init() { self.dimensions = [] } - + public init(_ dimensions: [(String, String)]) { self.dimensions = dimensions } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: StringCodingKey.self) - for (key, value) in self.dimensions { - try container.encode(value, forKey: .init(key)) - } + + public init(arrayLiteral elements: (String, String)...) { + self.init(elements) } - + public func hash(into hasher: inout Hasher) { for (key, value) in dimensions { hasher.combine(key) @@ -389,60 +317,64 @@ public struct DimensionLabels: MetricLabels { } } +extension DimensionLabels: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + for (key, value) in self.dimensions { + try container.encode(value, forKey: .init(key)) + } + } +} + + + /// Helper for dimensions /// swift-metrics api doesn't allow setting buckets explicitly. /// If default buckets don't fit, this Labels implementation is a nice default to create Prometheus metric types with -public struct DimensionHistogramLabels: HistogramLabels { +struct EncodableHistogramLabels: Encodable { /// Bucket - public var le: String + let le: String? /// Dimensions - let labels: DimensionLabels - - /// Empty init - public init() { - self.le = "" - self.labels = DimensionLabels() - } - - /// Init with dimensions - public init(_ dimensions: [(String, String)]) { - self.le = "" - self.labels = DimensionLabels(dimensions) + let labels: DimensionLabels? + + public init(labels: DimensionLabels?, le: String? = nil) { + self.le = le + self.labels = labels } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - for (key, value) in self.labels.dimensions { - try container.encode(value, forKey: .init(key)) + if let labels = labels { + for (key, value) in labels.dimensions { + try container.encode(value, forKey: .init(key)) + } + } + if let le = le { + try container.encode(le, forKey: .init("le")) } - try container.encode(le, forKey: .init("le")) } } -/// Helper for dimensions -public struct DimensionSummaryLabels: SummaryLabels { +struct EncodableSummaryLabels: Encodable { /// Quantile - public var quantile: String + var quantile: String? /// Dimensions - let labels: DimensionLabels + let labels: DimensionLabels? - /// Empty init - public init() { - self.quantile = "" - self.labels = DimensionLabels() + public init(labels: DimensionLabels?, quantile: String?) { + self.quantile = quantile + self.labels = labels } - - /// Init with dimensions - public init(_ dimensions: [(String, String)]) { - self.quantile = "" - self.labels = DimensionLabels(dimensions) - } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - for (key, value) in self.labels.dimensions { - try container.encode(value, forKey: .init(key)) + if let labels = labels { + for (key, value) in labels.dimensions { + try container.encode(value, forKey: .init(key)) + } + } + if let quantile = quantile { + try container.encode(quantile, forKey: .init("quantile")) } - try container.encode(quantile, forKey: .init("quantile")) } } diff --git a/Sources/Prometheus/Sanitizer/DimensionsSanitizer.swift b/Sources/Prometheus/Sanitizer/DimensionsSanitizer.swift new file mode 100644 index 0000000..3a9422c --- /dev/null +++ b/Sources/Prometheus/Sanitizer/DimensionsSanitizer.swift @@ -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: "_") + } + } +} \ No newline at end of file diff --git a/Sources/Prometheus/Sanitizer/LabelSanitizer.swift b/Sources/Prometheus/Sanitizer/LabelSanitizer.swift new file mode 100644 index 0000000..a9ccf93 --- /dev/null +++ b/Sources/Prometheus/Sanitizer/LabelSanitizer.swift @@ -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 +} \ No newline at end of file diff --git a/Sources/Prometheus/Sanitizer/PrometheusLabelSanitizer.swift b/Sources/Prometheus/Sanitizer/PrometheusLabelSanitizer.swift new file mode 100644 index 0000000..e68d020 --- /dev/null +++ b/Sources/Prometheus/Sanitizer/PrometheusLabelSanitizer.swift @@ -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: "_") + } + } +} diff --git a/Sources/Prometheus/Utils.swift b/Sources/Prometheus/Utils.swift index 7053867..3f6d3f3 100644 --- a/Sources/Prometheus/Utils.swift +++ b/Sources/Prometheus/Utils.swift @@ -1,29 +1,7 @@ import Foundation -/// Empty labels class -public struct EmptyLabels: MetricLabels { - /// Creates empty labels - public init() { } -} - -/// Empty labels class -public struct EmptyHistogramLabels: HistogramLabels { - /// Bucket - public var le: String = "" - /// Creates empty labels - public init() { } -} - -/// Empty labels class -public struct EmptySummaryLabels: SummaryLabels { - /// Quantile - public var quantile: String = "" - /// Creates empty labels - public init() { } -} - /// Creates a Prometheus String representation of a `MetricLabels` instance -func encodeLabels(_ labels: Labels, _ excludingKeys: [String] = []) -> String { +func encodeLabels(_ labels: Labels, _ excludingKeys: [String] = []) -> String { do { let data = try JSONEncoder().encode(labels) guard var dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { diff --git a/Sources/PrometheusExample/main.swift b/Sources/PrometheusExample/main.swift index 6ac829c..fe3aefc 100644 --- a/Sources/PrometheusExample/main.swift +++ b/Sources/PrometheusExample/main.swift @@ -45,71 +45,40 @@ for _ in 0...Int.random(in: 10...100) { // let t = Timer(label: "timer", dimensions: [("abc", "123")]) // t.recordMicroseconds(Double.random(in: 20...150)) //} - - -//struct MyCodable: MetricLabels { -// var thing: String = "*" -//} // //let codable1 = MyCodable(thing: "Thing1") //let codable2 = MyCodable(thing: "Thing2") // -//let counter = myProm.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter", initialValue: 12, withLabelType: MyCodable.self) +//let counter = myProm.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter", initialValue: 12) // //counter.inc(5) -//counter.inc(Int.random(in: 0...100), codable2) -//counter.inc(Int.random(in: 0...100), codable1) +//counter.inc(Int.random(in: 0...100), .init([("thing", "thing2")])) +//counter.inc(Int.random(in: 0...100), .init([("thing", "thing1")])) // -//let gauge = myProm.createGauge(forType: Int.self, named: "my_gauge", helpText: "Just a gauge", initialValue: 12, withLabelType: MyCodable.self) +//let gauge = myProm.createGauge(forType: Int.self, named: "my_gauge", helpText: "Just a gauge", initialValue: 12) // //gauge.inc(100) -//gauge.inc(Int.random(in: 0...100), codable2) -//gauge.inc(Int.random(in: 0...100), codable1) +//gauge.inc(Int.random(in: 0...100), .init([("thing", "thing2")])) +//gauge.inc(Int.random(in: 0...100), .init([("thing", "thing1")])) // -//struct HistogramThing: HistogramLabels { -// var le: String = "" -// let route: String -// -// init() { -// self.route = "*" -// } -// -// init(_ route: String) { -// self.route = route -// } -//} -// -//let histogram = myProm.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Just a histogram", labels: HistogramThing.self) +//let histogram = myProm.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Just a histogram") // //for _ in 0...Int.random(in: 10...50) { // histogram.observe(Double.random(in: 0...1)) //} // //for _ in 0...Int.random(in: 10...50) { -// histogram.observe(Double.random(in: 0...1), HistogramThing("/test")) -//} -// -//struct SummaryThing: SummaryLabels { -// var quantile: String = "" -// let route: String -// -// init() { -// self.route = "*" -// } -// -// init(_ route: String) { -// self.route = route -// } +// histogram.observe(Double.random(in: 0...1), .init([("route", "/test")])) //} // -//let summary = myProm.createSummary(forType: Double.self, named: "my_summary", helpText: "Just a summary", labels: SummaryThing.self) +//let summary = myProm.createSummary(forType: Double.self, named: "my_summary", helpText: "Just a summary") // //for _ in 0...Int.random(in: 100...1000) { // summary.observe(Double.random(in: 0...10000)) //} // //for _ in 0...Int.random(in: 100...1000) { -// summary.observe(Double.random(in: 0...10000), SummaryThing("/test")) +// summary.observe(Double.random(in: 0...10000), .init([("route", "/test")])) //} let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) diff --git a/Tests/SwiftPrometheusTests/GaugeTests.swift b/Tests/SwiftPrometheusTests/GaugeTests.swift index 6619119..4bfa72f 100644 --- a/Tests/SwiftPrometheusTests/GaugeTests.swift +++ b/Tests/SwiftPrometheusTests/GaugeTests.swift @@ -4,18 +4,7 @@ import NIO @testable import CoreMetrics final class GaugeTests: XCTestCase { - struct BaseLabels: MetricLabels { - let myValue: String - - init() { - self.myValue = "*" - } - - init(myValue: String) { - self.myValue = myValue - } - } - + let baseLabels = DimensionLabels([("myValue", "labels")]) var prom: PrometheusClient! var group: EventLoopGroup! var eventLoop: EventLoop { @@ -67,18 +56,18 @@ final class GaugeTests: XCTestCase { } func testGaugeStandalone() { - let gauge = prom.createGauge(forType: Int.self, named: "my_gauge", helpText: "Gauge for testing", initialValue: 10, withLabelType: BaseLabels.self) + let gauge = prom.createGauge(forType: Int.self, named: "my_gauge", helpText: "Gauge for testing", initialValue: 10) XCTAssertEqual(gauge.get(), 10) gauge.inc(10) XCTAssertEqual(gauge.get(), 20) gauge.dec(12) XCTAssertEqual(gauge.get(), 8) gauge.set(20) - gauge.inc(10, BaseLabels(myValue: "labels")) + gauge.inc(10, baseLabels) XCTAssertEqual(gauge.get(), 20) - XCTAssertEqual(gauge.get(BaseLabels(myValue: "labels")), 20) + XCTAssertEqual(gauge.get(baseLabels), 20) - let gaugeTwo = prom.createGauge(forType: Int.self, named: "my_gauge", helpText: "Gauge for testing", initialValue: 10, withLabelType: BaseLabels.self) + let gaugeTwo = prom.createGauge(forType: Int.self, named: "my_gauge", helpText: "Gauge for testing", initialValue: 10) XCTAssertEqual(gaugeTwo.get(), 20) gaugeTwo.inc() XCTAssertEqual(gauge.get(), 21) diff --git a/Tests/SwiftPrometheusTests/HistogramTests.swift b/Tests/SwiftPrometheusTests/HistogramTests.swift index dd82ebe..39815aa 100644 --- a/Tests/SwiftPrometheusTests/HistogramTests.swift +++ b/Tests/SwiftPrometheusTests/HistogramTests.swift @@ -4,18 +4,7 @@ import NIO @testable import CoreMetrics final class HistogramTests: XCTestCase { - struct BaseHistogramLabels: HistogramLabels { - var le: String = "" - let myValue: String - - init() { - self.myValue = "*" - } - - init(myValue: String) { - self.myValue = myValue - } - } + let baseLabels = DimensionLabels([("myValue", "labels")]) var prom: PrometheusClient! var group: EventLoopGroup! @@ -38,14 +27,13 @@ final class HistogramTests: XCTestCase { let prom = PrometheusClient() let histogram = prom.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Histogram for testing", - buckets: Buckets.exponential(start: 1, factor: 2, count: 63), - labels: DimensionHistogramLabels.self) + buckets: Buckets.exponential(start: 1, factor: 2, count: 63)) let elg = MultiThreadedEventLoopGroup(numberOfThreads: 8) let semaphore = DispatchSemaphore(value: 2) _ = elg.next().submit { for _ in 1...1_000 { - let labels = DimensionHistogramLabels([("myValue", "1")]) - let labels2 = DimensionHistogramLabels([("myValue", "2")]) + let labels = DimensionLabels([("myValue", "1")]) + let labels2 = DimensionLabels([("myValue", "2")]) histogram.observe(1.0, labels) histogram.observe(1.0, labels2) @@ -54,8 +42,8 @@ final class HistogramTests: XCTestCase { } _ = elg.next().submit { for _ in 1...1_000 { - let labels = DimensionHistogramLabels([("myValue", "1")]) - let labels2 = DimensionHistogramLabels([("myValue", "2")]) + let labels = DimensionLabels([("myValue", "1")]) + let labels2 = DimensionLabels([("myValue", "2")]) histogram.observe(1.0, labels2) histogram.observe(1.0, labels) @@ -132,26 +120,26 @@ final class HistogramTests: XCTestCase { } func testHistogramStandalone() { - let histogram = prom.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Histogram for testing", buckets: [0.5, 1, 2, 3, 5, Double.greatestFiniteMagnitude], labels: BaseHistogramLabels.self) - let histogramTwo = prom.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Histogram for testing", buckets: [0.5, 1, 2, 3, 5, Double.greatestFiniteMagnitude], labels: BaseHistogramLabels.self) + let histogram = prom.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Histogram for testing", buckets: [0.5, 1, 2, 3, 5, Double.greatestFiniteMagnitude]) + let histogramTwo = prom.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Histogram for testing", buckets: [0.5, 1, 2, 3, 5, Double.greatestFiniteMagnitude]) histogram.observe(1) histogram.observe(2) histogramTwo.observe(3) - histogram.observe(3, .init(myValue: "labels")) + histogram.observe(3, baseLabels) XCTAssertEqual(histogram.collect(), """ # HELP my_histogram Histogram for testing # TYPE my_histogram histogram - my_histogram_bucket{myValue="*", le="0.5"} 0.0 - my_histogram_bucket{myValue="*", le="1.0"} 1.0 - my_histogram_bucket{myValue="*", le="2.0"} 2.0 - my_histogram_bucket{myValue="*", le="3.0"} 4.0 - my_histogram_bucket{myValue="*", le="5.0"} 4.0 - my_histogram_bucket{myValue="*", le="+Inf"} 4.0 - my_histogram_count{myValue="*"} 4.0 - my_histogram_sum{myValue="*"} 9.0 + my_histogram_bucket{le="0.5"} 0.0 + my_histogram_bucket{le="1.0"} 1.0 + my_histogram_bucket{le="2.0"} 2.0 + my_histogram_bucket{le="3.0"} 4.0 + my_histogram_bucket{le="5.0"} 4.0 + my_histogram_bucket{le="+Inf"} 4.0 + my_histogram_count 4.0 + my_histogram_sum 9.0 my_histogram_bucket{myValue="labels", le="0.5"} 0.0 my_histogram_bucket{myValue="labels", le="1.0"} 0.0 my_histogram_bucket{myValue="labels", le="2.0"} 0.0 diff --git a/Tests/SwiftPrometheusTests/PrometheusMetricsTests.swift b/Tests/SwiftPrometheusTests/PrometheusMetricsTests.swift index 62981f7..598d864 100644 --- a/Tests/SwiftPrometheusTests/PrometheusMetricsTests.swift +++ b/Tests/SwiftPrometheusTests/PrometheusMetricsTests.swift @@ -183,7 +183,7 @@ final class PrometheusMetricsTests: XCTestCase { config.timerImplementation = .histogram() let metricsFactory = PrometheusMetricsFactory(client: prom, configuration: config) metricsFactory.makeTimer(label: "duration_nanos", dimensions: []).recordNanoseconds(1) - guard let histogram: PromHistogram = prom.getMetricInstance(with: "duration_nanos", andType: .histogram) else { + guard let histogram: PromHistogram = prom.getMetricInstance(with: "duration_nanos", andType: .histogram) else { XCTFail("Timer should be backed by Histogram") return } @@ -200,7 +200,7 @@ final class PrometheusMetricsTests: XCTestCase { let timer = metricsFactory.makeTimer(label: "duration_nanos", dimensions: []) timer.recordNanoseconds(1) metricsFactory.destroyTimer(timer) - let histogram: PromHistogram? = prom.getMetricInstance(with: "duration_nanos", andType: .histogram) + let histogram: PromHistogram? = prom.getMetricInstance(with: "duration_nanos", andType: .histogram) XCTAssertNil(histogram) } func testDestroySummaryTimer() { @@ -211,7 +211,7 @@ final class PrometheusMetricsTests: XCTestCase { let timer = metricsFactory.makeTimer(label: "duration_nanos", dimensions: []) timer.recordNanoseconds(1) metricsFactory.destroyTimer(timer) - let summary: PromSummary? = prom.getMetricInstance(with: "duration_nanos", andType: .summary) + let summary: PromSummary? = prom.getMetricInstance(with: "duration_nanos", andType: .summary) XCTAssertNil(summary) } diff --git a/Tests/SwiftPrometheusTests/SanitizerTests.swift b/Tests/SwiftPrometheusTests/SanitizerTests.swift index 0c948db..9fb2965 100644 --- a/Tests/SwiftPrometheusTests/SanitizerTests.swift +++ b/Tests/SwiftPrometheusTests/SanitizerTests.swift @@ -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 + """) + } } diff --git a/Tests/SwiftPrometheusTests/SummaryTests.swift b/Tests/SwiftPrometheusTests/SummaryTests.swift index 61e6e0e..b5c75de 100644 --- a/Tests/SwiftPrometheusTests/SummaryTests.swift +++ b/Tests/SwiftPrometheusTests/SummaryTests.swift @@ -4,19 +4,7 @@ import NIO @testable import CoreMetrics final class SummaryTests: XCTestCase { - struct BaseSummaryLabels: SummaryLabels { - var quantile: String = "" - let myValue: String - - init() { - self.myValue = "*" - } - - init(myValue: String) { - self.myValue = myValue - } - } - + let baseLabels = DimensionLabels([("myValue", "labels")]) var prom: PrometheusClient! var group: EventLoopGroup! var eventLoop: EventLoop { @@ -75,14 +63,13 @@ final class SummaryTests: XCTestCase { func testConcurrent() throws { let prom = PrometheusClient() let summary = prom.createSummary(forType: Double.self, named: "my_summary", - helpText: "Summary for testing", - labels: DimensionSummaryLabels.self) + helpText: "Summary for testing") let elg = MultiThreadedEventLoopGroup(numberOfThreads: 8) let semaphore = DispatchSemaphore(value: 2) _ = elg.next().submit { for _ in 1...1_000 { - let labels = DimensionSummaryLabels([("myValue", "1")]) - let labels2 = DimensionSummaryLabels([("myValue", "2")]) + let labels = DimensionLabels([("myValue", "1")]) + let labels2 = DimensionLabels([("myValue", "2")]) summary.observe(1.0, labels) summary.observe(1.0, labels2) @@ -91,8 +78,8 @@ final class SummaryTests: XCTestCase { } _ = elg.next().submit { for _ in 1...1_000 { - let labels = DimensionSummaryLabels([("myValue", "1")]) - let labels2 = DimensionSummaryLabels([("myValue", "2")]) + let labels = DimensionLabels([("myValue", "1")]) + let labels2 = DimensionLabels([("myValue", "2")]) summary.observe(1.0, labels2) summary.observe(1.0, labels) @@ -132,7 +119,7 @@ final class SummaryTests: XCTestCase { } func testSummaryTime() { - let summary = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", quantiles: [0.5, 0.9, 0.99], labels: BaseSummaryLabels.self) + let summary = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", quantiles: [0.5, 0.9, 0.99]) let delay = 0.05 summary.time { Thread.sleep(forTimeInterval: delay) @@ -142,35 +129,36 @@ final class SummaryTests: XCTestCase { let lines = [ "# HELP my_summary Summary for testing", "# TYPE my_summary summary", - #"my_summary{quantile="0.5", myValue="*"} \#(isCITestRun ? "" : "0.05")"#, - #"my_summary{quantile="0.9", myValue="*"} \#(isCITestRun ? "" : "0.05")"#, - #"my_summary{quantile="0.99", myValue="*"} \#(isCITestRun ? "" : "0.05")"#, - #"my_summary_count{myValue="*"} 1.0"#, - #"my_summary_sum{myValue="*"} \#(isCITestRun ? "" : "0.05")"# + #"my_summary{quantile="0.5"} \#(isCITestRun ? "" : "0.05")"#, + #"my_summary{quantile="0.9"} \#(isCITestRun ? "" : "0.05")"#, + #"my_summary{quantile="0.99"} \#(isCITestRun ? "" : "0.05")"#, + #"my_summary_count 1.0"#, + #"my_summary_sum \#(isCITestRun ? "" : "0.05")"# ] - let sections = summary.collect().split(separator: "\n").map(String.init).enumerated().map { i, s in s.starts(with: lines[i]) } + let collect = summary.collect() + let sections = collect.split(separator: "\n").map(String.init).enumerated().map { i, s in s.starts(with: lines[i]) } XCTAssert(sections.filter { !$0 }.isEmpty) } func testSummaryStandalone() { - let summary = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", quantiles: [0.5, 0.9, 0.99], labels: BaseSummaryLabels.self) - let summaryTwo = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", quantiles: [0.5, 0.9, 0.99], labels: BaseSummaryLabels.self) + let summary = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", quantiles: [0.5, 0.9, 0.99]) + let summaryTwo = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", quantiles: [0.5, 0.9, 0.99]) summary.observe(1) summary.observe(2) summary.observe(4) summaryTwo.observe(10000) - summary.observe(123, .init(myValue: "labels")) + summary.observe(123, baseLabels) XCTAssertEqual(summary.collect(), """ # HELP my_summary Summary for testing # TYPE my_summary summary - my_summary{quantile=\"0.5\", myValue=\"*\"} 4.0 - my_summary{quantile=\"0.9\", myValue=\"*\"} 10000.0 - my_summary{quantile=\"0.99\", myValue=\"*\"} 10000.0 - my_summary_count{myValue=\"*\"} 5.0 - my_summary_sum{myValue=\"*\"} 10130.0 + my_summary{quantile=\"0.5\"} 4.0 + my_summary{quantile=\"0.9\"} 10000.0 + my_summary{quantile=\"0.99\"} 10000.0 + my_summary_count 5.0 + my_summary_sum 10130.0 my_summary{quantile=\"0.5\", myValue=\"labels\"} 123.0 my_summary{quantile=\"0.9\", myValue=\"labels\"} 123.0 my_summary{quantile=\"0.99\", myValue=\"labels\"} 123.0 @@ -181,7 +169,7 @@ final class SummaryTests: XCTestCase { func testStandaloneSummaryWithCustomCapacity() { let capacity = 10 - let summary = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", capacity: capacity, quantiles: [0.5, 0.99], labels: BaseSummaryLabels.self) + let summary = prom.createSummary(forType: Double.self, named: "my_summary", helpText: "Summary for testing", capacity: capacity, quantiles: [0.5, 0.99]) for i in 0 ..< capacity { summary.observe(Double(i * 1_000)) } for i in 0 ..< capacity { summary.observe(Double(i)) } @@ -189,10 +177,10 @@ final class SummaryTests: XCTestCase { XCTAssertEqual(summary.collect(), """ # HELP my_summary Summary for testing # TYPE my_summary summary - my_summary{quantile="0.5", myValue="*"} 4.5 - my_summary{quantile="0.99", myValue="*"} 9.0 - my_summary_count{myValue="*"} 20.0 - my_summary_sum{myValue="*"} 45045.0 + my_summary{quantile="0.5"} 4.5 + my_summary{quantile="0.99"} 9.0 + my_summary_count 20.0 + my_summary_sum 45045.0 """) } } diff --git a/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift b/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift index f2f124e..aec2233 100644 --- a/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift +++ b/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift @@ -8,18 +8,8 @@ var isCITestRun: Bool { } final class SwiftPrometheusTests: XCTestCase { - - struct BaseLabels: MetricLabels { - let myValue: String - - init() { - self.myValue = "*" - } - - init(myValue: String) { - self.myValue = myValue - } - } + + let baseLabels = DimensionLabels([("myValue", "labels")]) var prom: PrometheusClient! @@ -32,26 +22,26 @@ final class SwiftPrometheusTests: XCTestCase { } func testCounter() { - let counter = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Counter for testing", initialValue: 10, withLabelType: BaseLabels.self) + let counter = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Counter for testing", initialValue: 10) XCTAssertEqual(counter.get(), 10) counter.inc(10) XCTAssertEqual(counter.get(), 20) - counter.inc(10, BaseLabels(myValue: "labels")) + counter.inc(10, baseLabels) XCTAssertEqual(counter.get(), 20) - XCTAssertEqual(counter.get(BaseLabels(myValue: "labels")), 20) + XCTAssertEqual(counter.get(baseLabels), 20) XCTAssertEqual(counter.collect(), "# HELP my_counter Counter for testing\n# TYPE my_counter counter\nmy_counter 20\nmy_counter{myValue=\"labels\"} 20") } func testMultipleCounter() { - let counter = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Counter for testing", initialValue: 10, withLabelType: BaseLabels.self) + let counter = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Counter for testing", initialValue: 10) counter.inc(10) XCTAssertEqual(counter.get(), 20) - let counterTwo = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Counter for testing", initialValue: 10, withLabelType: BaseLabels.self) + let counterTwo = prom.createCounter(forType: Int.self, named: "my_counter", helpText: "Counter for testing", initialValue: 10) counter.inc(10) XCTAssertEqual(counterTwo.get(), 30) - counterTwo.inc(20, BaseLabels(myValue: "labels")) + counterTwo.inc(20, baseLabels) XCTAssertEqual(counter.collect(), "# HELP my_counter Counter for testing\n# TYPE my_counter counter\nmy_counter 30\nmy_counter{myValue=\"labels\"} 30") self.prom.collect { metricsString in