Allow providing a custom MetricsFactory to Counter and friends (#172)

### Motivation:

To allow for things like parallel testing, it'd be useful if we can
explicitly provide a MetricsFactory to the existing Counter/... types.

### Modifications:

Added a `factory: MetricsFactory` parameter to all the initializers of
Counter/... types, and kept the existing methods that continue to
default to `MetricsSystem.factory`.

### Result:

Adopters can use a custom MetricsFactory explicitly passed in at metric
creation time. Existing adopters are not affected, unless you opt in,
you continue to use the global factory.
This commit is contained in:
Honza Dvorsky 2025-04-29 15:07:30 +02:00 committed by GitHub
parent 27ecca7ac1
commit 98d36172c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 329 additions and 39 deletions

View File

@ -23,8 +23,11 @@
/// ///
/// Its behavior depends on the `CounterHandler` implementation. /// Its behavior depends on the `CounterHandler` implementation.
public final class Counter { public final class Counter {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API. /// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: CounterHandler public let _handler: CounterHandler
@usableFromInline
package let _factory: MetricsFactory
public let label: String public let label: String
public let dimensions: [(String, String)] public let dimensions: [(String, String)]
@ -39,10 +42,32 @@ public final class Counter {
/// - label: The label for the `Counter`. /// - label: The label for the `Counter`.
/// - dimensions: The dimensions for the `Counter`. /// - dimensions: The dimensions for the `Counter`.
/// - handler: The custom backend. /// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], handler: CounterHandler) { /// - factory: The custom factory.
public init(label: String, dimensions: [(String, String)], handler: CounterHandler, factory: MetricsFactory) {
self.label = label self.label = label
self.dimensions = dimensions self.dimensions = dimensions
self._handler = handler self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Counter`, while providing an explicit `CounterHandler`.
///
/// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one.
/// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason.
///
/// - SeeAlso: Use `init(label:dimensions:)` to create `Counter` instances using the configured metrics backend.
///
/// - parameters:
/// - label: The label for the `Counter`.
/// - dimensions: The dimensions for the `Counter`.
/// - handler: The custom backend, created by the global metrics factory.
public convenience init(label: String, dimensions: [(String, String)], handler: CounterHandler) {
self.init(
label: label,
dimensions: dimensions,
handler: handler,
factory: MetricsSystem.factory
)
} }
/// Increment the counter. /// Increment the counter.
@ -74,15 +99,25 @@ extension Counter {
/// - label: The label for the `Counter`. /// - label: The label for the `Counter`.
/// - dimensions: The dimensions for the `Counter`. /// - dimensions: The dimensions for the `Counter`.
public convenience init(label: String, dimensions: [(String, String)] = []) { public convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeCounter(label: label, dimensions: dimensions) self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
self.init(label: label, dimensions: dimensions, handler: handler) }
/// Create a new `Counter`.
///
/// - parameters:
/// - label: The label for the `Counter`.
/// - dimensions: The dimensions for the `Counter`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeCounter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
} }
/// Signal the underlying metrics library that this counter will never be updated again. /// Signal the underlying metrics library that this counter will never be updated again.
/// In response the library MAY decide to eagerly release any resources held by this `Counter`. /// In response the library MAY decide to eagerly release any resources held by this `Counter`.
@inlinable @inlinable
public func destroy() { public func destroy() {
MetricsSystem.factory.destroyCounter(self._handler) self._factory.destroyCounter(self._handler)
} }
} }
@ -102,8 +137,11 @@ extension Counter: CustomStringConvertible {
/// ///
/// Its behavior depends on the `FloatingCounterHandler` implementation. /// Its behavior depends on the `FloatingCounterHandler` implementation.
public final class FloatingPointCounter { public final class FloatingPointCounter {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API. /// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: FloatingPointCounterHandler public let _handler: FloatingPointCounterHandler
@usableFromInline
package let _factory: MetricsFactory
public let label: String public let label: String
public let dimensions: [(String, String)] public let dimensions: [(String, String)]
@ -118,10 +156,37 @@ public final class FloatingPointCounter {
/// - label: The label for the `FloatingPointCounter`. /// - label: The label for the `FloatingPointCounter`.
/// - dimensions: The dimensions for the `FloatingPointCounter`. /// - dimensions: The dimensions for the `FloatingPointCounter`.
/// - handler: The custom backend. /// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], handler: FloatingPointCounterHandler) { /// - factory: The custom factory.
public init(
label: String,
dimensions: [(String, String)],
handler: FloatingPointCounterHandler,
factory: MetricsFactory
) {
self.label = label self.label = label
self.dimensions = dimensions self.dimensions = dimensions
self._handler = handler self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `FloatingPointCounter`, while providing an explicit `FloatingPointCounterHandler`.
///
/// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one.
/// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason.
///
/// - SeeAlso: Use `init(label:dimensions:)` to create `FloatingPointCounter` instances using the configured metrics backend.
///
/// - parameters:
/// - label: The label for the `FloatingPointCounter`.
/// - dimensions: The dimensions for the `FloatingPointCounter`.
/// - handler: The custom backend.
public convenience init(label: String, dimensions: [(String, String)], handler: FloatingPointCounterHandler) {
self.init(
label: label,
dimensions: dimensions,
handler: handler,
factory: MetricsSystem.factory
)
} }
/// Increment the FloatingPointCounter. /// Increment the FloatingPointCounter.
@ -153,15 +218,25 @@ extension FloatingPointCounter {
/// - label: The label for the `FloatingPointCounter`. /// - label: The label for the `FloatingPointCounter`.
/// - dimensions: The dimensions for the `FloatingPointCounter`. /// - dimensions: The dimensions for the `FloatingPointCounter`.
public convenience init(label: String, dimensions: [(String, String)] = []) { public convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeFloatingPointCounter(label: label, dimensions: dimensions) self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
self.init(label: label, dimensions: dimensions, handler: handler) }
/// Create a new `FloatingPointCounter`.
///
/// - parameters:
/// - label: The label for the `FloatingPointCounter`.
/// - dimensions: The dimensions for the `FloatingPointCounter`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeFloatingPointCounter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
} }
/// Signal the underlying metrics library that this FloatingPointCounter will never be updated again. /// Signal the underlying metrics library that this FloatingPointCounter will never be updated again.
/// In response the library MAY decide to eagerly release any resources held by this `FloatingPointCounter`. /// In response the library MAY decide to eagerly release any resources held by this `FloatingPointCounter`.
@inlinable @inlinable
public func destroy() { public func destroy() {
MetricsSystem.factory.destroyFloatingPointCounter(self._handler) self._factory.destroyFloatingPointCounter(self._handler)
} }
} }
@ -185,6 +260,16 @@ public final class Gauge: Recorder, @unchecked Sendable {
public convenience init(label: String, dimensions: [(String, String)] = []) { public convenience init(label: String, dimensions: [(String, String)] = []) {
self.init(label: label, dimensions: dimensions, aggregate: false) self.init(label: label, dimensions: dimensions, aggregate: false)
} }
/// Create a new `Gauge`.
///
/// - parameters:
/// - label: The label for the `Gauge`.
/// - dimensions: The dimensions for the `Gauge`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
self.init(label: label, dimensions: dimensions, aggregate: false, factory: factory)
}
} }
// MARK: - Meter // MARK: - Meter
@ -192,8 +277,11 @@ public final class Gauge: Recorder, @unchecked Sendable {
/// A meter is similar to a gauge, it is a metric that represents a single numerical value that can arbitrarily go up and down. /// A meter is similar to a gauge, it is a metric that represents a single numerical value that can arbitrarily go up and down.
/// Meters are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. /// Meters are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads.
public final class Meter { public final class Meter {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API. /// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: MeterHandler public let _handler: MeterHandler
@usableFromInline
package let _factory: MetricsFactory
public let label: String public let label: String
public let dimensions: [(String, String)] public let dimensions: [(String, String)]
@ -208,10 +296,27 @@ public final class Meter {
/// - label: The label for the `Recorder`. /// - label: The label for the `Recorder`.
/// - dimensions: The dimensions for the `Recorder`. /// - dimensions: The dimensions for the `Recorder`.
/// - handler: The custom backend. /// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], handler: MeterHandler) { /// - factory: The custom factory.
public init(label: String, dimensions: [(String, String)], handler: MeterHandler, factory: MetricsFactory) {
self.label = label self.label = label
self.dimensions = dimensions self.dimensions = dimensions
self._handler = handler self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Meter`, while providing an explicit `MeterHandler`.
///
/// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one.
/// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason.
///
/// - SeeAlso: Use `init(label:dimensions:)` to create `Meter` instances using the configured metrics backend.
///
/// - parameters:
/// - label: The label for the `Recorder`.
/// - dimensions: The dimensions for the `Recorder`.
/// - handler: The custom backend.
public convenience init(label: String, dimensions: [(String, String)], handler: MeterHandler) {
self.init(label: label, dimensions: dimensions, handler: handler, factory: MetricsSystem.factory)
} }
/// Set a value. /// Set a value.
@ -264,21 +369,31 @@ public final class Meter {
} }
extension Meter { extension Meter {
/// Create a new `Meter`.
///
/// - parameters:
/// - label: The label for the `Meter`.
/// - dimensions: The dimensions for the `Meter`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeMeter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// Create a new `Meter`. /// Create a new `Meter`.
/// ///
/// - parameters: /// - parameters:
/// - label: The label for the `Meter`. /// - label: The label for the `Meter`.
/// - dimensions: The dimensions for the `Meter`. /// - dimensions: The dimensions for the `Meter`.
public convenience init(label: String, dimensions: [(String, String)] = []) { public convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeMeter(label: label, dimensions: dimensions) self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
self.init(label: label, dimensions: dimensions, handler: handler)
} }
/// Signal the underlying metrics library that this recorder will never be updated again. /// Signal the underlying metrics library that this recorder will never be updated again.
/// In response the library MAY decide to eagerly release any resources held by this `Recorder`. /// In response the library MAY decide to eagerly release any resources held by this `Recorder`.
@inlinable @inlinable
public func destroy() { public func destroy() {
MetricsSystem.factory.destroyMeter(self._handler) self._factory.destroyMeter(self._handler)
} }
} }
@ -296,8 +411,11 @@ extension Meter: CustomStringConvertible {
/// ///
/// Its behavior depends on the `RecorderHandler` implementation. /// Its behavior depends on the `RecorderHandler` implementation.
public class Recorder { public class Recorder {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API. /// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: RecorderHandler public let _handler: RecorderHandler
@usableFromInline
package let _factory: MetricsFactory
public let label: String public let label: String
public let dimensions: [(String, String)] public let dimensions: [(String, String)]
public let aggregate: Bool public let aggregate: Bool
@ -314,11 +432,41 @@ public class Recorder {
/// - dimensions: The dimensions for the `Recorder`. /// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size /// - aggregate: aggregate recorded values to produce statistics across a sample size
/// - handler: The custom backend. /// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) { /// - factory: The custom factory.
public init(
label: String,
dimensions: [(String, String)],
aggregate: Bool,
handler: RecorderHandler,
factory: MetricsFactory
) {
self.label = label self.label = label
self.dimensions = dimensions self.dimensions = dimensions
self.aggregate = aggregate self.aggregate = aggregate
self._handler = handler self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Recorder`, while providing an explicit `RecorderHandler`.
///
/// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one.
/// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason.
///
/// - SeeAlso: Use `init(label:dimensions:)` to create `Recorder` instances using the configured metrics backend.
///
/// - parameters:
/// - label: The label for the `Recorder`.
/// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size
/// - handler: The custom backend.
public convenience init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) {
self.init(
label: label,
dimensions: dimensions,
aggregate: aggregate,
handler: handler,
factory: MetricsSystem.factory
)
} }
/// Record a value. /// Record a value.
@ -354,15 +502,31 @@ extension Recorder {
/// - dimensions: The dimensions for the `Recorder`. /// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size /// - aggregate: aggregate recorded values to produce statistics across a sample size
public convenience init(label: String, dimensions: [(String, String)] = [], aggregate: Bool = true) { public convenience init(label: String, dimensions: [(String, String)] = [], aggregate: Bool = true) {
let handler = MetricsSystem.factory.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) self.init(label: label, dimensions: dimensions, aggregate: aggregate, factory: MetricsSystem.factory)
self.init(label: label, dimensions: dimensions, aggregate: aggregate, handler: handler) }
/// Create a new `Recorder`.
///
/// - parameters:
/// - label: The label for the `Recorder`.
/// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size.
/// - factory: The custom factory.
public convenience init(
label: String,
dimensions: [(String, String)] = [],
aggregate: Bool = true,
factory: MetricsFactory
) {
let handler = factory.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
self.init(label: label, dimensions: dimensions, aggregate: aggregate, handler: handler, factory: factory)
} }
/// Signal the underlying metrics library that this recorder will never be updated again. /// Signal the underlying metrics library that this recorder will never be updated again.
/// In response the library MAY decide to eagerly release any resources held by this `Recorder`. /// In response the library MAY decide to eagerly release any resources held by this `Recorder`.
@inlinable @inlinable
public func destroy() { public func destroy() {
MetricsSystem.factory.destroyRecorder(self._handler) self._factory.destroyRecorder(self._handler)
} }
} }
@ -420,8 +584,11 @@ public struct TimeUnit: Equatable, Sendable {
/// ///
/// Its behavior depends on the `TimerHandler` implementation. /// Its behavior depends on the `TimerHandler` implementation.
public final class Timer { public final class Timer {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API. /// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: TimerHandler public let _handler: TimerHandler
@usableFromInline
package let _factory: MetricsFactory
public let label: String public let label: String
public let dimensions: [(String, String)] public let dimensions: [(String, String)]
@ -436,10 +603,27 @@ public final class Timer {
/// - label: The label for the `Timer`. /// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `Timer`. /// - dimensions: The dimensions for the `Timer`.
/// - handler: The custom backend. /// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], handler: TimerHandler) { /// - factory: The custom factory.
public init(label: String, dimensions: [(String, String)], handler: TimerHandler, factory: MetricsFactory) {
self.label = label self.label = label
self.dimensions = dimensions self.dimensions = dimensions
self._handler = handler self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Timer`, while providing an explicit `TimerHandler`.
///
/// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one.
/// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason.
///
/// - SeeAlso: Use `init(label:dimensions:)` to create `Recorder` instances using the configured metrics backend.
///
/// - parameters:
/// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `Timer`.
/// - handler: The custom backend.
public convenience init(label: String, dimensions: [(String, String)], handler: TimerHandler) {
self.init(label: label, dimensions: dimensions, handler: handler, factory: MetricsSystem.factory)
} }
/// Record a duration in nanoseconds. /// Record a duration in nanoseconds.
@ -541,14 +725,42 @@ public final class Timer {
} }
extension Timer { extension Timer {
/// Create a new `Timer`.
///
/// - parameters:
/// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `Timer`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeTimer(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// Create a new `Timer`. /// Create a new `Timer`.
/// ///
/// - parameters: /// - parameters:
/// - label: The label for the `Timer`. /// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `Timer`. /// - dimensions: The dimensions for the `Timer`.
public convenience init(label: String, dimensions: [(String, String)] = []) { public convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions) self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
self.init(label: label, dimensions: dimensions, handler: handler) }
/// Create a new `Timer`.
///
/// - parameters:
/// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `Timer`.
/// - displayUnit: A hint to the backend responsible for presenting the data of the preferred display unit. This is not guaranteed to be supported by all backends.
/// - factory: The custom factory.
public convenience init(
label: String,
dimensions: [(String, String)] = [],
preferredDisplayUnit displayUnit: TimeUnit,
factory: MetricsFactory
) {
let handler = factory.makeTimer(label: label, dimensions: dimensions)
handler.preferDisplayUnit(displayUnit)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
} }
/// Create a new `Timer`. /// Create a new `Timer`.
@ -562,16 +774,19 @@ extension Timer {
dimensions: [(String, String)] = [], dimensions: [(String, String)] = [],
preferredDisplayUnit displayUnit: TimeUnit preferredDisplayUnit displayUnit: TimeUnit
) { ) {
let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions) self.init(
handler.preferDisplayUnit(displayUnit) label: label,
self.init(label: label, dimensions: dimensions, handler: handler) dimensions: dimensions,
preferredDisplayUnit: displayUnit,
factory: MetricsSystem.factory
)
} }
/// Signal the underlying metrics library that this timer will never be updated again. /// Signal the underlying metrics library that this timer will never be updated again.
/// In response the library MAY decide to eagerly release any resources held by this `Timer`. /// In response the library MAY decide to eagerly release any resources held by this `Timer`.
@inlinable @inlinable
public func destroy() { public func destroy() {
MetricsSystem.factory.destroyTimer(self._handler) self._factory.destroyTimer(self._handler)
} }
} }
@ -756,11 +971,12 @@ public protocol MetricsFactory: _SwiftMetricsSendableProtocol {
internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCounterHandler { internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCounterHandler {
private let lock = Lock() private let lock = Lock()
private let counterHandler: CounterHandler private let counterHandler: CounterHandler
private let factory: MetricsFactory
internal var fraction: Double = 0 internal var fraction: Double = 0
init(label: String, dimensions: [(String, String)]) { init(label: String, dimensions: [(String, String)], factory: MetricsFactory) {
self.counterHandler = MetricsSystem self.counterHandler = factory.makeCounter(label: label, dimensions: dimensions)
.factory.makeCounter(label: label, dimensions: dimensions) self.factory = factory
} }
func increment(by amount: Double) { func increment(by amount: Double) {
@ -813,7 +1029,7 @@ internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCoun
} }
func destroy() { func destroy() {
MetricsSystem.factory.destroyCounter(self.counterHandler) self.factory.destroyCounter(self.counterHandler)
} }
} }
@ -824,10 +1040,11 @@ internal final class AccumulatingMeter: MeterHandler, @unchecked Sendable {
// FIXME: use swift-atomics when floating point support is available // FIXME: use swift-atomics when floating point support is available
private var value: Double = 0 private var value: Double = 0
private let lock = Lock() private let lock = Lock()
private let factory: MetricsFactory
init(label: String, dimensions: [(String, String)]) { init(label: String, dimensions: [(String, String)], factory: MetricsFactory) {
self.recorderHandler = MetricsSystem self.recorderHandler = factory.makeRecorder(label: label, dimensions: dimensions, aggregate: true)
.factory.makeRecorder(label: label, dimensions: dimensions, aggregate: true) self.factory = factory
} }
func set(_ value: Int64) { func set(_ value: Int64) {
@ -898,7 +1115,7 @@ internal final class AccumulatingMeter: MeterHandler, @unchecked Sendable {
} }
func destroy() { func destroy() {
MetricsSystem.factory.destroyRecorder(self.recorderHandler) self.factory.destroyRecorder(self.recorderHandler)
} }
} }
@ -911,7 +1128,7 @@ extension MetricsFactory {
/// - label: The label for the `FloatingPointCounterHandler`. /// - label: The label for the `FloatingPointCounterHandler`.
/// - dimensions: The dimensions for the `FloatingPointCounterHandler`. /// - dimensions: The dimensions for the `FloatingPointCounterHandler`.
public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler {
AccumulatingRoundingFloatingPointCounter(label: label, dimensions: dimensions) AccumulatingRoundingFloatingPointCounter(label: label, dimensions: dimensions, factory: self)
} }
/// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked. /// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked.
@ -935,7 +1152,7 @@ extension MetricsFactory {
/// - label: The label for the `MeterHandler`. /// - label: The label for the `MeterHandler`.
/// - dimensions: The dimensions for the `MeterHandler`. /// - dimensions: The dimensions for the `MeterHandler`.
public func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler { public func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
AccumulatingMeter(label: label, dimensions: dimensions) AccumulatingMeter(label: label, dimensions: dimensions, factory: self)
} }
/// Invoked when the corresponding `Meter`'s `destroy()` function is invoked. /// Invoked when the corresponding `Meter`'s `destroy()` function is invoked.

View File

@ -597,7 +597,7 @@ class MetricsTests: XCTestCase {
} }
} }
func testCustomFactory() { func testCustomHandler() {
final class CustomHandler: CounterHandler { final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {} func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {} func reset() {}
@ -609,6 +609,79 @@ class MetricsTests: XCTestCase {
XCTAssertTrue(counter2._handler is CustomHandler, "expected custom log handler") XCTAssertTrue(counter2._handler is CustomHandler, "expected custom log handler")
} }
func testCustomFactory() {
// @unchecked Sendable is okay here - locking is done manually.
final class CustomFactory: MetricsFactory, @unchecked Sendable {
init(handler: CustomHandler) {
self.handler = handler
}
final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
}
private let handler: CustomHandler
private let lock: NSLock = NSLock()
private var locked_didCallDestroyCounter: Bool = false
var didCallDestroyCounter: Bool {
self.lock.lock()
defer {
lock.unlock()
}
return self.locked_didCallDestroyCounter
}
func makeCounter(label: String, dimensions: [(String, String)]) -> any CoreMetrics.CounterHandler {
handler
}
func makeRecorder(
label: String,
dimensions: [(String, String)],
aggregate: Bool
) -> any CoreMetrics.RecorderHandler {
fatalError("Unsupported")
}
func makeTimer(label: String, dimensions: [(String, String)]) -> any CoreMetrics.TimerHandler {
fatalError("Unsupported")
}
func destroyCounter(_ handler: any CoreMetrics.CounterHandler) {
XCTAssertTrue(
handler === self.handler,
"The handler to be destroyed doesn't match the expected handler."
)
self.lock.lock()
defer {
lock.unlock()
}
self.locked_didCallDestroyCounter = true
}
func destroyRecorder(_ handler: any CoreMetrics.RecorderHandler) {
fatalError("Unsupported")
}
func destroyTimer(_ handler: any CoreMetrics.TimerHandler) {
fatalError("Unsupported")
}
}
let handler = CustomFactory.CustomHandler()
let factory = CustomFactory(handler: handler)
XCTAssertFalse(factory.didCallDestroyCounter)
do {
let counter1 = Counter(label: "foo", factory: factory)
XCTAssertTrue(counter1._handler is CustomFactory.CustomHandler, "expected a custom metrics handler")
XCTAssertTrue(counter1._factory is CustomFactory, "expected a custom metrics factory")
counter1.destroy()
}
XCTAssertTrue(factory.didCallDestroyCounter)
}
func testDestroyingGauge() throws { func testDestroyingGauge() throws {
let metrics = TestMetrics() let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics) MetricsSystem.bootstrapInternal(metrics)