This is a very raw first draft for a new API proposal.
Motivation
Swift Metrics API currently follows the same design principles as Swift Logging. The Server Metrics API forums thread links out to a PR.
Currently users create new metrics by calling initializers on different Metric types:
Counter(label: String, dimensions: [(String, String)])
When users call this, internally we will reach out to the registered MetricFactory through the global MetricSystem.factory to create a new CounterHandler, so that the new Counter can be backed by the just created CounterHandler.
Generally I think this behavior is great and I do not want to change it.
However whenever I use Swift Metrics, I pass around the MetricsFactory manually instead of going through the global MetricSystem.factory. The reason for this is to allow better testability, when using the TestMetrics from the MetricsTestKit module. If I don't use the global MetricSystem.factory, I can easily create a new TestMetrics instance for each test run, which even allows me to run tests in parallel.
Currently the MetricsFactory protocol creates existential MetricHandlers. For each supported Metric type, the factory returns an existential MetricHandler (CounterHandler, TimerHandler, etc.). The reason for not returning explicitly typed handlers here, is that the MetricsFactory needed to be stored as an existential itself in the MetricsSystem.global. Before Swift 5.7 existentials were not able to have associated types.
However thanks to SE-309 Unlock existentials for all protocols we are now able to make MetricsFactory type safe.
Proposed Solution
Since I don't want to break API, I propose a new Protocol that adopters can implement:
public protocol TypePreservingMetricsFactory: MetricsFactory {
associatedtype Counter: CounterProtocol
associatedtype FloatingPointCounter: FloatingPointCounterProtocol
associatedtype Timer: TimerProtocol
associatedtype Recorder: RecorderProtocol
func makeSomeCounter(label: String, dimensions: [(String, String)]) -> Self.Counter
func makeSomeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> Self.FloatingPointCounter
func makeSomeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Self.Recorder
func makeSomeTimer(label: String, dimensions: [(String, String)]) -> Self.Timer
func destroyCounter(_ handler: Self.Counter)
func destroyFloatingPointCounter(_ handler: Self.FloatingPointCounter)
func destroyRecorder(_ handler: Self.Recorder)
func destroyTimer(_ handler: Self.Timer)
}
I added the Some word into the make function calls to ensure those methods are not overloaded by return type. Other naming suggestions are highly welcome. The new CounterProtocol is very close to the existing CounterHandler protocol. However it adds requirements for the label and the dimensions.
The reason for this is simple: I would like to remove the current Counter, Timer and Recorder wrapper classes, that add one level of indirection, that isn't needed.
public protocol CounterProtocol: CounterHandler {
var label: String { get }
var dimensions: [(String, String)] { get }
}
public protocol FloatingPointCounterProtocol: FloatingPointCounterHandler {
var label: String { get }
var dimensions: [(String, String)] { get }
}
If we want to preserve the option to create an untyped Metric in the future with an init, we could use callAsFunction on top of a protocol.
If a user implements TypePreservingMetricsFactory all methods needed for MetricsFactory are implemented by a default implementation:
// This extension ensures TypePreservingMetricsFactory also implements MetricsFactory
extension TypePreservingMetricsFactory {
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
self.makeSomeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
}
public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
self.makeSomeTimer(label: label, dimensions: dimensions)
}
public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
self.makeCounter(label: label, dimensions: dimensions)
}
public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler {
self.makeSomeFloatingPointCounter(label: label, dimensions: dimensions)
}
public func destroyTimer(_ handler: TimerHandler) {
if let handler = handler as? Self.Timer {
self.destroySomeTimer(handler)
}
}
public func destroyCounter(_ handler: CounterHandler) {
if let handler = handler as? Self.Counter {
self.destroySomeCounter(handler)
}
}
public func destroyRecorder(_ handler: RecorderHandler) {
if let handler = handler as? Self.Recorder {
self.destroySomeRecorder(handler)
}
}
public func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) {
if let handler = handler as? Self.FloatingPointCounter {
self.destroySomeFloatingPointCounter(handler)
}
}
}
Future ideas
- The
make and destroy methods on the TypePreservingMetricsFactory should be async to allow the use of an actor to create, maintain and destroy the created metrics. However this is a breaking change.
- We should make the
Gauge type stand byitself and not be implemented on top of recorder.
- We should potentially offer simple
Counter, Timer and Recorder implementations, that implementors of backends can use right away. So backend developers only have to worry about metric lifecycle management and exporting.
This is a very raw first draft for a new API proposal.
Motivation
Swift Metrics API currently follows the same design principles as Swift Logging. The Server Metrics API forums thread links out to a PR.
Currently users create new metrics by calling initializers on different Metric types:
When users call this, internally we will reach out to the registered
MetricFactorythrough the globalMetricSystem.factoryto create a newCounterHandler, so that the newCountercan be backed by the just createdCounterHandler.Generally I think this behavior is great and I do not want to change it.
However whenever I use Swift Metrics, I pass around the
MetricsFactorymanually instead of going through the globalMetricSystem.factory. The reason for this is to allow better testability, when using theTestMetricsfrom theMetricsTestKitmodule. If I don't use the globalMetricSystem.factory, I can easily create a newTestMetricsinstance for each test run, which even allows me to run tests in parallel.Currently the
MetricsFactoryprotocol creates existential MetricHandlers. For each supported Metric type, the factory returns an existential MetricHandler (CounterHandler,TimerHandler, etc.). The reason for not returning explicitly typed handlers here, is that theMetricsFactoryneeded to be stored as an existential itself in theMetricsSystem.global. Before Swift 5.7 existentials were not able to have associated types.However thanks to SE-309 Unlock existentials for all protocols we are now able to make
MetricsFactorytype safe.Proposed Solution
Since I don't want to break API, I propose a new Protocol that adopters can implement:
I added the
Someword into themakefunction calls to ensure those methods are not overloaded by return type. Other naming suggestions are highly welcome. The newCounterProtocolis very close to the existingCounterHandlerprotocol. However it adds requirements for the label and the dimensions.The reason for this is simple: I would like to remove the current
Counter,TimerandRecorderwrapper classes, that add one level of indirection, that isn't needed.If we want to preserve the option to create an untyped Metric in the future with an init, we could use
callAsFunctionon top of a protocol.If a user implements
TypePreservingMetricsFactoryall methods needed forMetricsFactoryare implemented by a default implementation:Future ideas
makeanddestroymethods on theTypePreservingMetricsFactoryshould be async to allow the use of an actor to create, maintain and destroy the created metrics. However this is a breaking change.Gaugetype stand byitself and not be implemented on top of recorder.Counter,TimerandRecorderimplementations, that implementors of backends can use right away. So backend developers only have to worry about metric lifecycle management and exporting.