diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c89850a..d0bc61c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: - swift:5.4 - swift:5.5 - swift:5.6 + - swift:5.7 - swiftlang/swift:nightly-main swiftos: - focal diff --git a/Package.swift b/Package.swift index c8c37ed..52919af 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( targets: ["Prometheus"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-metrics.git", from: "2.2.0"), + .package(url: "https://github.com/apple/swift-metrics.git", from: "2.3.2"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), ], targets: [ diff --git a/Sources/Prometheus/MetricTypes/Counter.swift b/Sources/Prometheus/MetricTypes/Counter.swift index 85dae80..7b9b3e3 100644 --- a/Sources/Prometheus/MetricTypes/Counter.swift +++ b/Sources/Prometheus/MetricTypes/Counter.swift @@ -17,6 +17,9 @@ public class PromCounter: PromMetric { /// Initial value of the counter private let initialValue: NumType + + /// Indicates wether or not metric has been used without labels + private var usedWithoutLabels: Bool /// Storage of values that have labels attached internal var metrics: [DimensionLabels: NumType] = [:] @@ -36,6 +39,7 @@ public class PromCounter: PromMetric { self.help = help self.initialValue = initialValue self.value = initialValue + self.usedWithoutLabels = initialValue != 0 self.lock = Lock() } @@ -44,8 +48,8 @@ public class PromCounter: PromMetric { /// - Returns: /// Newline separated Prometheus formatted metric string public func collect() -> String { - let (value, metrics) = self.lock.withLock { - (self.value, self.metrics) + let (value, metrics, usedWithoutLabels) = self.lock.withLock { + (self.value, self.metrics, self.usedWithoutLabels) } var output = [String]() @@ -54,7 +58,9 @@ public class PromCounter: PromMetric { } output.append("# TYPE \(self.name) \(self._type)") - output.append("\(self.name) \(value)") + if usedWithoutLabels { + output.append("\(self.name) \(value)") + } metrics.forEach { (labels, value) in let labelsString = encodeLabels(labels) @@ -79,6 +85,7 @@ public class PromCounter: PromMetric { self.metrics[labels] = val return val } else { + self.usedWithoutLabels = true self.value += amount return self.value } diff --git a/Sources/Prometheus/MetricTypes/Gauge.swift b/Sources/Prometheus/MetricTypes/Gauge.swift index a7ddbbe..9e71d34 100644 --- a/Sources/Prometheus/MetricTypes/Gauge.swift +++ b/Sources/Prometheus/MetricTypes/Gauge.swift @@ -19,7 +19,10 @@ public class PromGauge: PromMetric { /// Initial value of the Gauge private let initialValue: NumType - + + /// Indicates wether or not metric has been used without labels + private var usedWithoutLabels: Bool + /// Storage of values that have labels attached private var metrics: [DimensionLabels: NumType] = [:] @@ -39,6 +42,7 @@ public class PromGauge: PromMetric { self.help = help self.initialValue = initialValue self.value = initialValue + self.usedWithoutLabels = initialValue != 0 self.lock = Lock() } @@ -47,8 +51,8 @@ public class PromGauge: PromMetric { /// - Returns: /// Newline separated Prometheus formatted metric string public func collect() -> String { - let (value, metrics) = self.lock.withLock { - (self.value, self.metrics) + let (value, metrics, usedWithoutLabels) = self.lock.withLock { + (self.value, self.metrics, self.usedWithoutLabels) } var output = [String]() @@ -57,7 +61,9 @@ public class PromGauge: PromMetric { } output.append("# TYPE \(self.name) \(self._type)") - output.append("\(self.name) \(value)") + if usedWithoutLabels { + output.append("\(self.name) \(value)") + } metrics.forEach { (labels, value) in let labelsString = encodeLabels(labels) @@ -129,6 +135,7 @@ public class PromGauge: PromMetric { self.metrics[labels] = amount return amount } else { + self.usedWithoutLabels = true self.value = amount return self.value } @@ -151,6 +158,7 @@ public class PromGauge: PromMetric { self.metrics[labels] = val return val } else { + self.usedWithoutLabels = true self.value += amount return self.value } @@ -184,6 +192,7 @@ public class PromGauge: PromMetric { self.metrics[labels] = val return val } else { + self.usedWithoutLabels = true self.value -= amount return self.value } diff --git a/Sources/Prometheus/MetricTypes/Histogram.swift b/Sources/Prometheus/MetricTypes/Histogram.swift index ccdc064..e5840cd 100644 --- a/Sources/Prometheus/MetricTypes/Histogram.swift +++ b/Sources/Prometheus/MetricTypes/Histogram.swift @@ -87,6 +87,9 @@ public class PromHistogram: PromMetric { /// Total value of the Histogram private let sum: PromCounter + /// Indicates wether or not metric has been used without labels + private var usedWithoutLabels: Bool + /// Lock used for thread safety private let lock: Lock @@ -102,6 +105,7 @@ public class PromHistogram: PromMetric { self.help = help self.sum = .init("\(self.name)_sum", nil, 0) + self.usedWithoutLabels = false self.upperBounds = buckets.buckets @@ -117,8 +121,8 @@ public class PromHistogram: PromMetric { /// - Returns: /// Newline separated Prometheus formatted metric string public func collect() -> String { - let (buckets, subHistograms) = self.lock.withLock { - (self.buckets, self.subHistograms) + let (buckets, subHistograms, usedWithoutLabels) = self.lock.withLock { + (self.buckets, self.subHistograms, self.usedWithoutLabels) } var output = [String]() @@ -129,12 +133,14 @@ public class PromHistogram: PromMetric { output.append("# HELP \(self.name) \(help)") } output.append("# TYPE \(self.name) \(self._type)") - collectBuckets(buckets: buckets, - upperBounds: self.upperBounds, - name: self.name, - labels: nil, - sum: self.sum.get(), - into: &output) + if usedWithoutLabels { + collectBuckets(buckets: buckets, + upperBounds: self.upperBounds, + name: self.name, + labels: nil, + sum: self.sum.get(), + into: &output) + } subHistograms.forEach { subHistogram in let (subHistogramBuckets, subHistogramLabels) = self.lock.withLock { @@ -178,13 +184,17 @@ public class PromHistogram: PromMetric { if let labels = labels { self.getOrCreateHistogram(with: labels) .observe(value) - } - self.sum.inc(value) - - for (i, bound) in self.upperBounds.enumerated() { - if bound >= value.doubleValue { - self.buckets[i].inc() - return + } else { + self.sum.inc(value) + + self.lock.withLock { + self.usedWithoutLabels = true + for (i, bound) in self.upperBounds.enumerated() { + if bound >= value.doubleValue { + self.buckets[i].inc() + return + } + } } } } diff --git a/Sources/Prometheus/MetricTypes/Summary.swift b/Sources/Prometheus/MetricTypes/Summary.swift index f527557..4bc222f 100644 --- a/Sources/Prometheus/MetricTypes/Summary.swift +++ b/Sources/Prometheus/MetricTypes/Summary.swift @@ -34,6 +34,9 @@ public class PromSummary: PromMetric { /// Sub Summaries for this Summary fileprivate var subSummaries: [DimensionLabels: PromSummary] = [:] + + /// Indicates wether or not metric has been used without labels + private var usedWithoutLabels: Bool /// Lock used for thread safety private let lock: Lock @@ -62,6 +65,8 @@ public class PromSummary: PromMetric { self.quantiles = quantiles + self.usedWithoutLabels = false + self.lock = Lock() } @@ -70,8 +75,8 @@ public class PromSummary: PromMetric { /// - Returns: /// Newline separated Prometheus formatted metric string public func collect() -> String { - let (subSummaries, values) = lock.withLock { - (self.subSummaries, self.values) + let (subSummaries, values, usedWithoutLabels) = lock.withLock { + (self.subSummaries, self.values, self.usedWithoutLabels) } var output = [String]() @@ -82,14 +87,17 @@ public class PromSummary: PromMetric { output.append("# HELP \(self.name) \(help)") } output.append("# TYPE \(self.name) \(self._type)") - calculateQuantiles(quantiles: self.quantiles, values: values.map { $0.doubleValue }).sorted { $0.key < $1.key }.forEach { (arg) in - let (q, v) = arg - let labelsString = encodeLabels(EncodableSummaryLabels(labels: nil, quantile: "\(q)")) - output.append("\(self.name)\(labelsString) \(format(v))") - } - output.append("\(self.name)_count \(self.count.get())") - output.append("\(self.name)_sum \(format(self.sum.get().doubleValue))") + if usedWithoutLabels { + calculateQuantiles(quantiles: self.quantiles, values: values.map { $0.doubleValue }).sorted { $0.key < $1.key }.forEach { (arg) in + let (q, v) = arg + let labelsString = encodeLabels(EncodableSummaryLabels(labels: nil, quantile: "\(q)")) + output.append("\(self.name)\(labelsString) \(format(v))") + } + + output.append("\(self.name)_count \(self.count.get())") + output.append("\(self.name)_sum \(format(self.sum.get().doubleValue))") + } subSummaries.forEach { labels, subSum in let subSumValues = lock.withLock { subSum.values } @@ -138,14 +146,16 @@ public class PromSummary: PromMetric { if let labels = labels { let sum = self.getOrCreateSummary(withLabels: labels) sum.observe(value) - } - self.count.inc(1) - self.sum.inc(value) - self.lock.withLock { - if self.values.count == self.capacity { - _ = self.values.popFirst() + } else { + self.count.inc(1) + self.sum.inc(value) + self.lock.withLock { + self.usedWithoutLabels = true + if self.values.count == self.capacity { + _ = self.values.popFirst() + } + self.values.append(value) } - self.values.append(value) } } diff --git a/Tests/SwiftPrometheusTests/GaugeTests.swift b/Tests/SwiftPrometheusTests/GaugeTests.swift index 4bfa72f..0dcb286 100644 --- a/Tests/SwiftPrometheusTests/GaugeTests.swift +++ b/Tests/SwiftPrometheusTests/GaugeTests.swift @@ -80,4 +80,22 @@ final class GaugeTests: XCTestCase { my_gauge{myValue="labels"} 20 """) } + + func testGaugeDoesNotReportWithNoLabelUsed() { + let gauge = prom.createGauge(forType: Int.self, named: "my_gauge") + gauge.inc(1, [("a", "b")]) + + XCTAssertEqual(gauge.collect(), """ + # TYPE my_gauge gauge + my_gauge{a="b"} 1 + """) + + gauge.inc() + + XCTAssertEqual(gauge.collect(), """ + # TYPE my_gauge gauge + my_gauge 1 + my_gauge{a="b"} 1 + """) + } } diff --git a/Tests/SwiftPrometheusTests/HistogramTests.swift b/Tests/SwiftPrometheusTests/HistogramTests.swift index 443f72f..7059d1b 100644 --- a/Tests/SwiftPrometheusTests/HistogramTests.swift +++ b/Tests/SwiftPrometheusTests/HistogramTests.swift @@ -55,8 +55,8 @@ final class HistogramTests: XCTestCase { try elg.syncShutdownGracefully() let output = histogram.collect() - XCTAssertTrue(output.contains("my_histogram_count 4000.0")) - XCTAssertTrue(output.contains("my_histogram_sum 4000.0")) + XCTAssertFalse(output.contains("my_histogram_count 4000.0")) + XCTAssertFalse(output.contains("my_histogram_sum 4000.0")) XCTAssertTrue(output.contains(#"my_histogram_count{myValue="1"} 2000.0"#)) XCTAssertTrue(output.contains(#"my_histogram_sum{myValue="1"} 2000.0"#)) @@ -88,11 +88,11 @@ final class HistogramTests: XCTestCase { my_histogram_bucket{le="0.5"} 0.0 my_histogram_bucket{le="1.0"} 1.0 my_histogram_bucket{le="2.5"} 2.0 - my_histogram_bucket{le="5.0"} 4.0 - my_histogram_bucket{le="10.0"} 4.0 - my_histogram_bucket{le="+Inf"} 4.0 - my_histogram_count 4.0 - my_histogram_sum 9.0 + my_histogram_bucket{le="5.0"} 3.0 + my_histogram_bucket{le="10.0"} 3.0 + my_histogram_bucket{le="+Inf"} 3.0 + my_histogram_count 3.0 + my_histogram_sum 6.0 my_histogram_bucket{myValue="labels", le="0.005"} 0.0 my_histogram_bucket{myValue="labels", le="0.01"} 0.0 my_histogram_bucket{myValue="labels", le="0.025"} 0.0 @@ -144,11 +144,11 @@ final class HistogramTests: XCTestCase { 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{le="3.0"} 3.0 + my_histogram_bucket{le="5.0"} 3.0 + my_histogram_bucket{le="+Inf"} 3.0 + my_histogram_count 3.0 + my_histogram_sum 6.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 @@ -159,4 +159,43 @@ final class HistogramTests: XCTestCase { my_histogram_sum{myValue="labels"} 3.0 """) } + + func testHistogramDoesNotReportWithNoLabelUsed() { + let histogram = prom.createHistogram(forType: Double.self, named: "my_histogram", buckets: [0.5, 1, 2, 3, 5, Double.greatestFiniteMagnitude]) + histogram.observe(3, [("a", "b")]) + + XCTAssertEqual(histogram.collect(), """ + # TYPE my_histogram histogram + my_histogram_bucket{le="0.5", a="b"} 0.0 + my_histogram_bucket{le="1.0", a="b"} 0.0 + my_histogram_bucket{le="2.0", a="b"} 0.0 + my_histogram_bucket{le="3.0", a="b"} 1.0 + my_histogram_bucket{le="5.0", a="b"} 1.0 + my_histogram_bucket{le="+Inf", a="b"} 1.0 + my_histogram_count{a="b"} 1.0 + my_histogram_sum{a="b"} 3.0 + """) + + histogram.observe(3) + + XCTAssertEqual(histogram.collect(), """ + # TYPE my_histogram histogram + my_histogram_bucket{le="0.5"} 0.0 + my_histogram_bucket{le="1.0"} 0.0 + my_histogram_bucket{le="2.0"} 0.0 + my_histogram_bucket{le="3.0"} 1.0 + my_histogram_bucket{le="5.0"} 1.0 + my_histogram_bucket{le="+Inf"} 1.0 + my_histogram_count 1.0 + my_histogram_sum 3.0 + my_histogram_bucket{le="0.5", a="b"} 0.0 + my_histogram_bucket{le="1.0", a="b"} 0.0 + my_histogram_bucket{le="2.0", a="b"} 0.0 + my_histogram_bucket{le="3.0", a="b"} 1.0 + my_histogram_bucket{le="5.0", a="b"} 1.0 + my_histogram_bucket{le="+Inf", a="b"} 1.0 + my_histogram_count{a="b"} 1.0 + my_histogram_sum{a="b"} 3.0 + """) + } } diff --git a/Tests/SwiftPrometheusTests/SanitizerTests.swift b/Tests/SwiftPrometheusTests/SanitizerTests.swift index 9fb2965..537652f 100644 --- a/Tests/SwiftPrometheusTests/SanitizerTests.swift +++ b/Tests/SwiftPrometheusTests/SanitizerTests.swift @@ -61,7 +61,6 @@ final class SanitizerTests: XCTestCase { 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 b5c75de..d2cd3af 100644 --- a/Tests/SwiftPrometheusTests/SummaryTests.swift +++ b/Tests/SwiftPrometheusTests/SummaryTests.swift @@ -24,7 +24,7 @@ final class SummaryTests: XCTestCase { func testSummary() { let summary = Timer(label: "my_summary") - summary.handler.preferDisplayUnit(.nanoseconds) + summary._handler.preferDisplayUnit(.nanoseconds) summary.recordNanoseconds(1) summary.recordNanoseconds(2) @@ -41,13 +41,13 @@ final class SummaryTests: XCTestCase { # TYPE my_summary summary my_summary{quantile="0.01"} 1.0 my_summary{quantile="0.05"} 1.0 - my_summary{quantile="0.5"} 4.0 + my_summary{quantile="0.5"} 3.0 my_summary{quantile="0.9"} 10000.0 my_summary{quantile="0.95"} 10000.0 my_summary{quantile="0.99"} 10000.0 my_summary{quantile="0.999"} 10000.0 - my_summary_count 5 - my_summary_sum 10130.0 + my_summary_count 4 + my_summary_sum 10007.0 my_summary{quantile="0.01", myValue="labels"} 123.0 my_summary{quantile="0.05", myValue="labels"} 123.0 my_summary{quantile="0.5", myValue="labels"} 123.0 @@ -88,8 +88,11 @@ final class SummaryTests: XCTestCase { } semaphore.wait() try elg.syncShutdownGracefully() - XCTAssertTrue(summary.collect().contains("my_summary_count 4000.0")) - XCTAssertTrue(summary.collect().contains("my_summary_sum 4000.0")) + XCTAssertTrue(summary.collect().contains(#"my_summary_count{myValue="1"} 2000.0"#)) + XCTAssertTrue(summary.collect().contains(#"my_summary_sum{myValue="1"} 2000.0"#)) + XCTAssertTrue(summary.collect().contains(#"my_summary_count{myValue="2"} 2000.0"#)) + XCTAssertTrue(summary.collect().contains(#"my_summary_sum{myValue="2"} 2000.0"#)) + XCTAssertFalse(summary.collect().contains(#"my_summary_count 4000.0"#)) } func testSummaryWithPreferredDisplayUnit() { @@ -154,16 +157,16 @@ final class SummaryTests: XCTestCase { XCTAssertEqual(summary.collect(), """ # HELP my_summary Summary for testing # TYPE my_summary summary - 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 - my_summary_count{myValue=\"labels\"} 1.0 - my_summary_sum{myValue=\"labels\"} 123.0 + my_summary{quantile="0.5"} 3.0 + my_summary{quantile="0.9"} 10000.0 + my_summary{quantile="0.99"} 10000.0 + my_summary_count 4.0 + my_summary_sum 10007.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 + my_summary_count{myValue="labels"} 1.0 + my_summary_sum{myValue="labels"} 123.0 """) } @@ -183,4 +186,32 @@ final class SummaryTests: XCTestCase { my_summary_sum 45045.0 """) } + + func testSummaryDoesNotReportWithNoLabelUsed() { + let summary = prom.createSummary(forType: Double.self, named: "my_summary", quantiles: [0.5, 0.99]) + summary.observe(3, [("a", "b")]) + + XCTAssertEqual(summary.collect(), """ + # TYPE my_summary summary + my_summary{quantile="0.5", a="b"} 3.0 + my_summary{quantile="0.99", a="b"} 3.0 + my_summary_count{a="b"} 1.0 + my_summary_sum{a="b"} 3.0 + """) + + summary.observe(3) + + XCTAssertEqual(summary.collect(), """ + # TYPE my_summary summary + my_summary{quantile="0.5"} 3.0 + my_summary{quantile="0.99"} 3.0 + my_summary_count 1.0 + my_summary_sum 3.0 + my_summary{quantile="0.5", a="b"} 3.0 + my_summary{quantile="0.99", a="b"} 3.0 + my_summary_count{a="b"} 1.0 + my_summary_sum{a="b"} 3.0 + """) + } + } diff --git a/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift b/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift index aec2233..e6f0962 100644 --- a/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift +++ b/Tests/SwiftPrometheusTests/SwiftPrometheusTests.swift @@ -48,4 +48,23 @@ final class SwiftPrometheusTests: XCTestCase { XCTAssertEqual(metricsString, "# HELP my_counter Counter for testing\n# TYPE my_counter counter\nmy_counter 30\nmy_counter{myValue=\"labels\"} 30\n") } } + + func testCounterDoesNotReportWithNoLabelUsed() { + let counter = prom.createCounter(forType: Int.self, named: "my_counter") + counter.inc(1, [("a", "b")]) + + XCTAssertEqual(counter.collect(), """ + # TYPE my_counter counter + my_counter{a="b"} 1 + """) + + counter.inc() + + XCTAssertEqual(counter.collect(), """ + # TYPE my_counter counter + my_counter 1 + my_counter{a="b"} 1 + """) + } + }