Skip to content

Commit 3cab56b

Browse files
committed
new: FQMetricDescriptor override option for Counter
Signed-off-by: Melissa Kilby <[email protected]>
1 parent ba92571 commit 3cab56b

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftPrometheus open source project
4+
//
5+
// Copyright (c) 2018-2025 SwiftPrometheus project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public struct FQMetricDescriptor {
16+
public let namespace: String?
17+
public let subsystem: String?
18+
public let metricName: String
19+
public let unitName: String?
20+
public let helpText: String?
21+
22+
public init(namespace: String? = nil, subsystem: String? = nil, metricName: String, unitName: String? = nil, helpText: String? = nil) {
23+
precondition(!metricName.isEmpty, "metricName must not be empty")
24+
self.namespace = namespace
25+
self.subsystem = subsystem
26+
self.metricName = metricName
27+
self.unitName = unitName
28+
self.helpText = helpText
29+
}
30+
31+
public var FQMetricName: String {
32+
[namespace, subsystem, metricName, unitName]
33+
.compactMap { $0?.isEmpty == false ? $0 : nil }
34+
.joined(separator: "_")
35+
}
36+
}

Sources/Prometheus/PrometheusCollectorRegistry.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ public final class PrometheusCollectorRegistry: Sendable {
9393
}
9494
}
9595

96+
/// Creates a new ``Counter`` collector or returns the already existing one with the same name,
97+
/// based on the provided descriptor.
98+
///
99+
/// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the
100+
/// created ``Counter`` will be part of the export.
101+
///
102+
/// - Parameter descriptor: An ``FQMetricDescriptor`` that provides the fully qualified name for the metric.
103+
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
104+
public func makeCounter(descriptor: FQMetricDescriptor) -> Counter {
105+
return self.makeCounter(name: descriptor.FQMetricName)
106+
}
107+
96108
/// Creates a new ``Counter`` collector or returns the already existing one with the same name.
97109
///
98110
/// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the
@@ -150,6 +162,20 @@ public final class PrometheusCollectorRegistry: Sendable {
150162
}
151163
}
152164

165+
/// Creates a new ``Counter`` collector or returns the already existing one with the same name,
166+
/// based on the provided descriptor.
167+
///
168+
/// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the
169+
/// created ``Counter`` will be part of the export.
170+
///
171+
/// - Parameter descriptor: An ``FQMetricDescriptor`` that provides the fully qualified name for the metric.
172+
/// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize
173+
/// what’s actually being measured in a Prometheus metric.
174+
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
175+
public func makeCounter(descriptor: FQMetricDescriptor, labels: [(String, String)]) -> Counter {
176+
return self.makeCounter(name: descriptor.FQMetricName, labels: labels)
177+
}
178+
153179
/// Creates a new ``Gauge`` collector or returns the already existing one with the same name.
154180
///
155181
/// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the

Tests/PrometheusTests/CounterTests.swift

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,109 @@ final class CounterTests: XCTestCase {
164164
"""
165165
)
166166
}
167+
168+
func testWithMetricDescriptorWithFullComponentMatrix() {
169+
// --- Test Constants ---
170+
// let helpTextValue = "https://help.url/sub"
171+
let metricName = "foo2"
172+
let incrementValue: Int64 = 2
173+
let client = PrometheusCollectorRegistry()
174+
175+
// 1. Define the base naming combinations first.
176+
let baseNameCases: [(namespace: String?, subsystem: String?, unitName: String?, expectedMetricName: String, description: String)] = [
177+
(namespace: "myapp", subsystem: "subsystem", unitName: "total", expectedMetricName: "myapp_subsystem_foo2_total", description: "All components present"),
178+
(namespace: "myapp", subsystem: "subsystem", unitName: nil, expectedMetricName: "myapp_subsystem_foo2", description: "Unit is nil"),
179+
(namespace: "myapp", subsystem: nil, unitName: "total", expectedMetricName: "myapp_foo2_total", description: "Subsystem is nil"),
180+
(namespace: "myapp", subsystem: nil, unitName: nil, expectedMetricName: "myapp_foo2", description: "Subsystem and Unit are nil"),
181+
(namespace: nil, subsystem: "subsystem", unitName: "total", expectedMetricName: "subsystem_foo2_total", description: "Namespace is nil"),
182+
(namespace: nil, subsystem: "subsystem", unitName: nil, expectedMetricName: "subsystem_foo2", description: "Namespace and Unit are nil"),
183+
(namespace: nil, subsystem: nil, unitName: "total", expectedMetricName: "foo2_total", description: "Namespace and Subsystem are nil"),
184+
(namespace: nil, subsystem: nil, unitName: nil, expectedMetricName: "foo2", description: "Only metric name is present"),
185+
(namespace: "", subsystem: "subsystem", unitName: "total", expectedMetricName: "subsystem_foo2_total", description: "Namespace is empty string"),
186+
(namespace: "myapp", subsystem: "", unitName: "total", expectedMetricName: "myapp_foo2_total", description: "Subsystem is empty string"),
187+
(namespace: "myapp", subsystem: "subsystem", unitName: "", expectedMetricName: "myapp_subsystem_foo2", description: "Unit is empty string"),
188+
(namespace: "", subsystem: "", unitName: "total", expectedMetricName: "foo2_total", description: "Namespace and Subsystem are empty strings"),
189+
(namespace: "myapp", subsystem: "", unitName: "", expectedMetricName: "myapp_foo2", description: "Subsystem and Unit are empty strings"),
190+
(namespace: "", subsystem: "subsystem", unitName: "", expectedMetricName: "subsystem_foo2", description: "Namespace and Unit are empty strings"),
191+
(namespace: "", subsystem: "", unitName: "", expectedMetricName: "foo2", description: "All optional components are empty strings")
192+
]
193+
194+
// 2. Define the label combinations to test.
195+
let labelCases: [(labels: [(String, String)], expectedLabelString: String, description: String)] = [
196+
(labels: [], expectedLabelString: "", description: "without labels"),
197+
(labels: [("method", "get")], expectedLabelString: "{method=\"get\"}", description: "with one label"),
198+
(labels: [("status", "200"), ("path", "/api/v1")], expectedLabelString: "{status=\"200\",path=\"/api/v1\"}", description: "with two labels"),
199+
]
200+
201+
// 3. Programmatically generate the final, full matrix by crossing name cases with label cases.
202+
var allTestCases: [(descriptor: FQMetricDescriptor, labels: [(String, String)], expectedOutput: String, failureDescription: String)] = []
203+
204+
for nameCase in baseNameCases {
205+
for labelCase in labelCases {
206+
let expectedMetricLine = "\(nameCase.expectedMetricName)\(labelCase.expectedLabelString) \(incrementValue)"
207+
208+
// todo : add help text in next PR
209+
// Case 1: With help text (currently disabled/commented out)
210+
// allTestCases.append((
211+
// descriptor: FQMetricDescriptor(
212+
// namespace: nameCase.namespace, subsystem: nameCase.subsystem, metricName: metricName, unitName: nameCase.unitName, helpText: helpTextValue
213+
// ),
214+
// labels: labelCase.labels,
215+
// expectedOutput: """
216+
// # HELP \(nameCase.expectedMetricName) \(helpTextValue)
217+
// # TYPE \(nameCase.expectedMetricName) counter
218+
// \(expectedMetricLine)
219+
//
220+
// """,
221+
// failureDescription: "\(nameCase.description), \(labelCase.description), with help text"
222+
// ))
223+
224+
// Case 2: Without help text (helpText is nil)
225+
allTestCases.append((
226+
descriptor: FQMetricDescriptor(
227+
namespace: nameCase.namespace, subsystem: nameCase.subsystem, metricName: metricName, unitName: nameCase.unitName, helpText: nil
228+
),
229+
labels: labelCase.labels,
230+
expectedOutput: """
231+
# TYPE \(nameCase.expectedMetricName) counter
232+
\(expectedMetricLine)
233+
234+
""",
235+
failureDescription: "\(nameCase.description), \(labelCase.description), without help text"
236+
))
237+
}
238+
}
239+
240+
let expectedTestCaseCount = baseNameCases.count * labelCases.count
241+
XCTAssertEqual(allTestCases.count, expectedTestCaseCount, "Test setup failed: Did not generate the correct number of test cases.")
242+
243+
// 4. Loop through the complete, generated test matrix.
244+
for testCase in allTestCases {
245+
246+
// The makeCounter overload with labels handles the empty label case correctly.
247+
let counter = client.makeCounter(descriptor: testCase.descriptor, labels: testCase.labels)
248+
counter.increment(by: incrementValue)
249+
250+
var buffer = [UInt8]()
251+
client.emit(into: &buffer)
252+
253+
let actualOutput = String(decoding: buffer, as: Unicode.UTF8.self)
254+
255+
let failureMessage = """
256+
Failed on test case: '\(testCase.failureDescription)'
257+
- Descriptor: \(testCase.descriptor)
258+
- Labels: \(testCase.labels)
259+
- Expected Output:
260+
---
261+
\(testCase.expectedOutput)
262+
---
263+
- Actual Output:
264+
---
265+
\(actualOutput)
266+
---
267+
"""
268+
XCTAssertEqual(actualOutput, testCase.expectedOutput, failureMessage)
269+
client.unregisterCounter(counter)
270+
}
271+
}
167272
}

0 commit comments

Comments
 (0)