Skip to content

Commit 0ab5efd

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

File tree

3 files changed

+240
-0
lines changed

3 files changed

+240
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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(
23+
namespace: String? = nil,
24+
subsystem: String? = nil,
25+
metricName: String,
26+
unitName: String? = nil,
27+
helpText: String? = nil
28+
) {
29+
precondition(!metricName.isEmpty, "metricName must not be empty")
30+
self.namespace = namespace
31+
self.subsystem = subsystem
32+
self.metricName = metricName
33+
self.unitName = unitName
34+
self.helpText = helpText
35+
}
36+
37+
public var FQMetricName: String {
38+
[namespace, subsystem, metricName, unitName]
39+
.compactMap { $0?.isEmpty == false ? $0 : nil }
40+
.joined(separator: "_")
41+
}
42+
}

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: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,176 @@ 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:
177+
[(
178+
namespace: String?, subsystem: String?, unitName: String?, expectedMetricName: String,
179+
description: String
180+
)] = [
181+
(
182+
namespace: "myapp", subsystem: "subsystem", unitName: "total",
183+
expectedMetricName: "myapp_subsystem_foo2_total", description: "All components present"
184+
),
185+
(
186+
namespace: "myapp", subsystem: "subsystem", unitName: nil,
187+
expectedMetricName: "myapp_subsystem_foo2", description: "Unit is nil"
188+
),
189+
(
190+
namespace: "myapp", subsystem: nil, unitName: "total", expectedMetricName: "myapp_foo2_total",
191+
description: "Subsystem is nil"
192+
),
193+
(
194+
namespace: "myapp", subsystem: nil, unitName: nil, expectedMetricName: "myapp_foo2",
195+
description: "Subsystem and Unit are nil"
196+
),
197+
(
198+
namespace: nil, subsystem: "subsystem", unitName: "total",
199+
expectedMetricName: "subsystem_foo2_total", description: "Namespace is nil"
200+
),
201+
(
202+
namespace: nil, subsystem: "subsystem", unitName: nil, expectedMetricName: "subsystem_foo2",
203+
description: "Namespace and Unit are nil"
204+
),
205+
(
206+
namespace: nil, subsystem: nil, unitName: "total", expectedMetricName: "foo2_total",
207+
description: "Namespace and Subsystem are nil"
208+
),
209+
(
210+
namespace: nil, subsystem: nil, unitName: nil, expectedMetricName: "foo2",
211+
description: "Only metric name is present"
212+
),
213+
(
214+
namespace: "", subsystem: "subsystem", unitName: "total",
215+
expectedMetricName: "subsystem_foo2_total", description: "Namespace is empty string"
216+
),
217+
(
218+
namespace: "myapp", subsystem: "", unitName: "total", expectedMetricName: "myapp_foo2_total",
219+
description: "Subsystem is empty string"
220+
),
221+
(
222+
namespace: "myapp", subsystem: "subsystem", unitName: "",
223+
expectedMetricName: "myapp_subsystem_foo2", description: "Unit is empty string"
224+
),
225+
(
226+
namespace: "", subsystem: "", unitName: "total", expectedMetricName: "foo2_total",
227+
description: "Namespace and Subsystem are empty strings"
228+
),
229+
(
230+
namespace: "myapp", subsystem: "", unitName: "", expectedMetricName: "myapp_foo2",
231+
description: "Subsystem and Unit are empty strings"
232+
),
233+
(
234+
namespace: "", subsystem: "subsystem", unitName: "", expectedMetricName: "subsystem_foo2",
235+
description: "Namespace and Unit are empty strings"
236+
),
237+
(
238+
namespace: "", subsystem: "", unitName: "", expectedMetricName: "foo2",
239+
description: "All optional components are empty strings"
240+
),
241+
]
242+
243+
// 2. Define the label combinations to test.
244+
let labelCases: [(labels: [(String, String)], expectedLabelString: String, description: String)] = [
245+
(labels: [], expectedLabelString: "", description: "without labels"),
246+
(labels: [("method", "get")], expectedLabelString: "{method=\"get\"}", description: "with one label"),
247+
(
248+
labels: [("status", "200"), ("path", "/api/v1")],
249+
expectedLabelString: "{status=\"200\",path=\"/api/v1\"}", description: "with two labels"
250+
),
251+
]
252+
253+
// 3. Programmatically generate the final, full matrix by crossing name cases with label cases.
254+
var allTestCases:
255+
[(
256+
descriptor: FQMetricDescriptor, labels: [(String, String)], expectedOutput: String,
257+
failureDescription: String
258+
)] = []
259+
260+
for nameCase in baseNameCases {
261+
for labelCase in labelCases {
262+
let expectedMetricLine =
263+
"\(nameCase.expectedMetricName)\(labelCase.expectedLabelString) \(incrementValue)"
264+
265+
// todo : add help text in next PR
266+
// Case 1: With help text (currently disabled/commented out)
267+
// allTestCases.append((
268+
// descriptor: FQMetricDescriptor(
269+
// namespace: nameCase.namespace, subsystem: nameCase.subsystem, metricName: metricName, unitName: nameCase.unitName, helpText: helpTextValue
270+
// ),
271+
// labels: labelCase.labels,
272+
// expectedOutput: """
273+
// # HELP \(nameCase.expectedMetricName) \(helpTextValue)
274+
// # TYPE \(nameCase.expectedMetricName) counter
275+
// \(expectedMetricLine)
276+
//
277+
// """,
278+
// failureDescription: "\(nameCase.description), \(labelCase.description), with help text"
279+
// ))
280+
281+
// Case 2: Without help text (helpText is nil)
282+
allTestCases.append(
283+
(
284+
descriptor: FQMetricDescriptor(
285+
namespace: nameCase.namespace,
286+
subsystem: nameCase.subsystem,
287+
metricName: metricName,
288+
unitName: nameCase.unitName,
289+
helpText: nil
290+
),
291+
labels: labelCase.labels,
292+
expectedOutput: """
293+
# TYPE \(nameCase.expectedMetricName) counter
294+
\(expectedMetricLine)
295+
296+
""",
297+
failureDescription: "\(nameCase.description), \(labelCase.description), without help text"
298+
)
299+
)
300+
}
301+
}
302+
303+
let expectedTestCaseCount = baseNameCases.count * labelCases.count
304+
XCTAssertEqual(
305+
allTestCases.count,
306+
expectedTestCaseCount,
307+
"Test setup failed: Did not generate the correct number of test cases."
308+
)
309+
310+
// 4. Loop through the complete, generated test matrix.
311+
for testCase in allTestCases {
312+
313+
// The makeCounter overload with labels handles the empty label case correctly.
314+
let counter = client.makeCounter(descriptor: testCase.descriptor, labels: testCase.labels)
315+
counter.increment(by: incrementValue)
316+
317+
var buffer = [UInt8]()
318+
client.emit(into: &buffer)
319+
320+
let actualOutput = String(decoding: buffer, as: Unicode.UTF8.self)
321+
322+
let failureMessage = """
323+
Failed on test case: '\(testCase.failureDescription)'
324+
- Descriptor: \(testCase.descriptor)
325+
- Labels: \(testCase.labels)
326+
- Expected Output:
327+
---
328+
\(testCase.expectedOutput)
329+
---
330+
- Actual Output:
331+
---
332+
\(actualOutput)
333+
---
334+
"""
335+
XCTAssertEqual(actualOutput, testCase.expectedOutput, failureMessage)
336+
client.unregisterCounter(counter)
337+
}
338+
}
167339
}

0 commit comments

Comments
 (0)