refactor APIs to make them closer to swift-log APIs

motivation: after much discussion around logging API, we settled on a different API style, primairly the fact that we will use initializers instead of factories

changes:
* introduce intermediate classes for Counter, Timer and Recorder which are designed to replace the Metrics.makeCounter, Metrics.makeTimer and Metrics.makeRecorder APIs and wrap corresponding CounterHandler, TimerHandler and ReorderHandler coming from the metrics implmentation
* rename Metrics to MetricsSystem
* rename  MetricsHandler -> MetricsFactory
* remove Metrics.withCounter, Metrics.withTimer and Metrics.withRecorder syntactic sugar
* rename Metrics.timed with Timer.measure
* make sure metrics system can only be initialized/bootstrapped once per process
* adjust and add tests
* add a bit of docs on key APIs
This commit is contained in:
tomer doron 2019-02-25 15:04:43 -08:00 committed by GitHub
parent 2953390316
commit 1f44332af3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 290 additions and 224 deletions

View File

@ -12,164 +12,211 @@
// //
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
public protocol Counter: AnyObject { /// This is the Counter protocol a metrics library implements
public protocol CounterHandler: AnyObject {
func increment<DataType: BinaryInteger>(_ value: DataType) func increment<DataType: BinaryInteger>(_ value: DataType)
} }
public extension Counter { // This is the user facing Counter API. It must have reference semantics, and its behviour depend ons the `CounterHandler` implementation
public class Counter: CounterHandler {
@usableFromInline
var handler: CounterHandler
public let label: String
public let dimensions: [(String, String)]
// this method is public to provide an escape hatch for situations one must use a custom factory instead of the gloabl one
// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason
public init(label: String, dimensions: [(String, String)], handler: CounterHandler) {
self.label = label
self.dimensions = dimensions
self.handler = handler
}
@inlinable @inlinable
func increment() { public func increment<DataType: BinaryInteger>(_ value: DataType) {
self.handler.increment(value)
}
@inlinable
public func increment() {
self.increment(1) self.increment(1)
} }
} }
public protocol Recorder: AnyObject { public extension Counter {
public convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeCounter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler)
}
}
/// This is the Recorder protocol a metrics library implements
public protocol RecorderHandler: AnyObject {
func record<DataType: BinaryInteger>(_ value: DataType) func record<DataType: BinaryInteger>(_ value: DataType)
func record<DataType: BinaryFloatingPoint>(_ value: DataType) func record<DataType: BinaryFloatingPoint>(_ value: DataType)
} }
public protocol Timer: AnyObject { // This is the user facing Recorder API. It must have reference semantics, and its behviour depend ons the `RecorderHandler` implementation
public class Recorder: RecorderHandler {
@usableFromInline
var handler: RecorderHandler
public let label: String
public let dimensions: [(String, String)]
public let aggregate: Bool
// this method is public to provide an escape hatch for situations one must use a custom factory instead of the gloabl one
// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason
public init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) {
self.label = label
self.dimensions = dimensions
self.aggregate = aggregate
self.handler = handler
}
@inlinable
public func record<DataType: BinaryInteger>(_ value: DataType) {
self.handler.record(value)
}
@inlinable
public func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
self.handler.record(value)
}
}
public extension Recorder {
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, handler: handler)
}
}
// A Gauge is a convenience for non-aggregating Recorder
public class Gauge: Recorder {
public convenience init(label: String, dimensions: [(String, String)] = []) {
self.init(label: label, dimensions: dimensions, aggregate: false)
}
}
// This is the Timer protocol a metrics library implements
public protocol TimerHandler: AnyObject {
func recordNanoseconds(_ duration: Int64) func recordNanoseconds(_ duration: Int64)
} }
public extension Timer { // This is the user facing Timer API. It must have reference semantics, and its behviour depend ons the `RecorderHandler` implementation
public class Timer: TimerHandler {
@usableFromInline
var handler: TimerHandler
public let label: String
public let dimensions: [(String, String)]
// this method is public to provide an escape hatch for situations one must use a custom factory instead of the gloabl one
// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason
public init(label: String, dimensions: [(String, String)], handler: TimerHandler) {
self.label = label
self.dimensions = dimensions
self.handler = handler
}
@inlinable @inlinable
func recordMicroseconds<DataType: BinaryInteger>(_ duration: DataType) { public func recordNanoseconds(_ duration: Int64) {
self.handler.recordNanoseconds(duration)
}
@inlinable
public func recordMicroseconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1000) self.recordNanoseconds(Int64(duration) * 1000)
} }
@inlinable @inlinable
func recordMicroseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) { public func recordMicroseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1000)) self.recordNanoseconds(Int64(duration * 1000))
} }
@inlinable @inlinable
func recordMilliseconds<DataType: BinaryInteger>(_ duration: DataType) { public func recordMilliseconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1_000_000) self.recordNanoseconds(Int64(duration) * 1_000_000)
} }
@inlinable @inlinable
func recordMilliseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) { public func recordMilliseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1_000_000)) self.recordNanoseconds(Int64(duration * 1_000_000))
} }
@inlinable @inlinable
func recordSeconds<DataType: BinaryInteger>(_ duration: DataType) { public func recordSeconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1_000_000_000) self.recordNanoseconds(Int64(duration) * 1_000_000_000)
} }
@inlinable @inlinable
func recordSeconds<DataType: BinaryFloatingPoint>(_ duration: DataType) { public func recordSeconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1_000_000_000)) self.recordNanoseconds(Int64(duration * 1_000_000_000))
} }
} }
public protocol MetricsHandler { public extension Timer {
func makeCounter(label: String, dimensions: [(String, String)]) -> Counter public convenience init(label: String, dimensions: [(String, String)] = []) {
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions)
func makeTimer(label: String, dimensions: [(String, String)]) -> Timer self.init(label: label, dimensions: dimensions, handler: handler)
}
public extension MetricsHandler {
@inlinable
func makeCounter(label: String) -> Counter {
return self.makeCounter(label: label, dimensions: [])
}
@inlinable
func makeRecorder(label: String, aggregate: Bool = true) -> Recorder {
return self.makeRecorder(label: label, dimensions: [], aggregate: aggregate)
}
@inlinable
func makeTimer(label: String) -> Timer {
return self.makeTimer(label: label, dimensions: [])
} }
} }
public extension MetricsHandler { public protocol MetricsFactory {
@inlinable func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
func makeGauge(label: String, dimensions: [(String, String)] = []) -> Recorder { func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
return self.makeRecorder(label: label, dimensions: dimensions, aggregate: false) func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
}
} }
public extension MetricsHandler { // This is the metrics system itself, it's mostly used set the type of the `MetricsFactory` implementation.
@inlinable public enum MetricsSystem {
func withCounter(label: String, dimensions: [(String, String)] = [], then: (Counter) -> Void) { fileprivate static let lock = ReadWriteLock()
then(self.makeCounter(label: label, dimensions: dimensions)) fileprivate static var _factory: MetricsFactory = NOOPMetricsHandler.instance
} fileprivate static var initialized = false
@inlinable // Configures which `LogHandler` to use in the application.
func withRecorder(label: String, dimensions: [(String, String)] = [], aggregate: Bool = true, then: (Recorder) -> Void) { public static func bootstrap(_ factory: MetricsFactory) {
then(self.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)) self.lock.withWriterLock {
} precondition(!self.initialized, "metrics system can only be initialized once per process. currently used factory: \(self.factory)")
self._factory = factory
@inlinable self.initialized = true
func withTimer(label: String, dimensions: [(String, String)] = [], then: (Timer) -> Void) {
then(self.makeTimer(label: label, dimensions: dimensions))
}
@inlinable
func withGauge(label: String, dimensions: [(String, String)] = [], then: (Recorder) -> Void) {
then(self.makeGauge(label: label, dimensions: dimensions))
}
}
public enum Metrics {
private static let lock = ReadWriteLock()
private static var _handler: MetricsHandler = NOOPMetricsHandler.instance
public static func bootstrap(_ handler: MetricsHandler) {
self.lock.withWriterLockVoid {
self._handler = handler
} }
} }
public static var global: MetricsHandler { // for our testing we want to allow multiple bootstraping
return self.lock.withReaderLock { self._handler } internal static func bootstrapInternal(_ factory: MetricsFactory) {
self.lock.withWriterLock {
self._factory = factory
}
}
internal static var factory: MetricsFactory {
return self.lock.withReaderLock { self._factory }
} }
} }
public extension Metrics { /// Ships with the metrics module, used to multiplex to multiple metrics handlers
@inlinable public final class MultiplexMetricsHandler: MetricsFactory {
func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { private let factories: [MetricsFactory]
return Metrics.global.makeCounter(label: label, dimensions: dimensions) public init(factories: [MetricsFactory]) {
self.factories = factories
} }
@inlinable public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { return MuxCounter(factories: self.factories, label: label, dimensions: dimensions)
return Metrics.global.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
} }
@inlinable public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { return MuxRecorder(factories: self.factories, label: label, dimensions: dimensions, aggregate: aggregate)
return Metrics.global.makeTimer(label: label, dimensions: dimensions)
}
}
public final class MultiplexMetricsHandler: MetricsHandler {
private let handlers: [MetricsHandler]
public init(handlers: [MetricsHandler]) {
self.handlers = handlers
} }
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return MuxCounter(handlers: self.handlers, label: label, dimensions: dimensions) return MuxTimer(factories: self.factories, label: label, dimensions: dimensions)
} }
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { private class MuxCounter: CounterHandler {
return MuxRecorder(handlers: self.handlers, label: label, dimensions: dimensions, aggregate: aggregate) let counters: [CounterHandler]
} public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)]) {
self.counters = factories.map { $0.makeCounter(label: label, dimensions: dimensions) }
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer {
return MuxTimer(handlers: self.handlers, label: label, dimensions: dimensions)
}
private class MuxCounter: Counter {
let counters: [Counter]
public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)]) {
self.counters = handlers.map { $0.makeCounter(label: label, dimensions: dimensions) }
} }
func increment<DataType: BinaryInteger>(_ value: DataType) { func increment<DataType: BinaryInteger>(_ value: DataType) {
@ -177,10 +224,10 @@ public final class MultiplexMetricsHandler: MetricsHandler {
} }
} }
private class MuxRecorder: Recorder { private class MuxRecorder: RecorderHandler {
let recorders: [Recorder] let recorders: [RecorderHandler]
public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)], aggregate: Bool) { public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)], aggregate: Bool) {
self.recorders = handlers.map { $0.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) } self.recorders = factories.map { $0.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) }
} }
func record<DataType: BinaryInteger>(_ value: DataType) { func record<DataType: BinaryInteger>(_ value: DataType) {
@ -192,10 +239,10 @@ public final class MultiplexMetricsHandler: MetricsHandler {
} }
} }
private class MuxTimer: Timer { private class MuxTimer: TimerHandler {
let timers: [Timer] let timers: [TimerHandler]
public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)]) { public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)]) {
self.timers = handlers.map { $0.makeTimer(label: label, dimensions: dimensions) } self.timers = factories.map { $0.makeTimer(label: label, dimensions: dimensions) }
} }
func recordNanoseconds(_ duration: Int64) { func recordNanoseconds(_ duration: Int64) {
@ -204,20 +251,20 @@ public final class MultiplexMetricsHandler: MetricsHandler {
} }
} }
public final class NOOPMetricsHandler: MetricsHandler, Counter, Recorder, Timer { public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, RecorderHandler, TimerHandler {
public static let instance = NOOPMetricsHandler() public static let instance = NOOPMetricsHandler()
private init() {} private init() {}
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return self return self
} }
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
return self return self
} }
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return self return self
} }

View File

@ -21,7 +21,7 @@ enum Example1 {
static func main() { static func main() {
// bootstrap with our example metrics library // bootstrap with our example metrics library
let metrics = ExampleMetricsLibrary() let metrics = ExampleMetricsLibrary()
Metrics.bootstrap(metrics) MetricsSystem.bootstrap(metrics)
let server = Server() let server = Server()
let client = Client(server: server) let client = Client(server: server)
@ -38,7 +38,7 @@ enum Example1 {
} }
class Client { class Client {
private let activeRequestsGauge = Metrics.global.makeGauge(label: "Client::ActiveRequests") private let activeRequestsGauge = Gauge(label: "Client::ActiveRequests")
private let server: Server private let server: Server
init(server: Server) { init(server: Server) {
self.server = server self.server = server
@ -46,9 +46,9 @@ enum Example1 {
func run(iterations: Int) { func run(iterations: Int) {
let group = DispatchGroup() let group = DispatchGroup()
let requestsCounter = Metrics.global.makeCounter(label: "Client::TotalRequests") let requestsCounter = Counter(label: "Client::TotalRequests")
let requestTimer = Metrics.global.makeTimer(label: "Client::doSomethig") let requestTimer = Timer(label: "Client::doSomethig")
let resultRecorder = Metrics.global.makeRecorder(label: "Client::doSomethig::result") let resultRecorder = Recorder(label: "Client::doSomethig::result")
for _ in 0 ... iterations { for _ in 0 ... iterations {
group.enter() group.enter()
let start = Date() let start = Date()
@ -78,10 +78,10 @@ enum Example1 {
class Server { class Server {
let library = RandomLibrary() let library = RandomLibrary()
let requestsCounter = Metrics.global.makeCounter(label: "Server::TotalRequests") let requestsCounter = Counter(label: "Server::TotalRequests")
func doSomethig(callback: @escaping (Int64) -> Void) { func doSomethig(callback: @escaping (Int64) -> Void) {
let timer = Metrics.global.makeTimer(label: "Server::doSomethig") let timer = Timer(label: "Server::doSomethig")
let start = Date() let start = Date()
requestsCounter.increment() requestsCounter.increment()
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) { DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) {

View File

@ -17,7 +17,7 @@
import Metrics import Metrics
class ExampleMetricsLibrary: MetricsHandler { class ExampleMetricsLibrary: MetricsFactory {
private let config: Config private let config: Config
private let lock = NSLock() private let lock = NSLock()
var counters = [ExampleCounter]() var counters = [ExampleCounter]()
@ -29,16 +29,16 @@ class ExampleMetricsLibrary: MetricsHandler {
self.config = config self.config = config
} }
func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return self.register(label: label, dimensions: dimensions, registry: &self.counters, maker: ExampleCounter.init) return self.register(label: label, dimensions: dimensions, registry: &self.counters, maker: ExampleCounter.init)
} }
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let options = aggregate ? self.config.recorder.aggregationOptions : nil let options = aggregate ? self.config.recorder.aggregationOptions : nil
return self.makeRecorder(label: label, dimensions: dimensions, options: options) return self.makeRecorder(label: label, dimensions: dimensions, options: options)
} }
func makeRecorder(label: String, dimensions: [(String, String)], options: [AggregationOption]?) -> Recorder { func makeRecorder(label: String, dimensions: [(String, String)], options: [AggregationOption]?) -> RecorderHandler {
guard let options = options else { guard let options = options else {
return self.register(label: label, dimensions: dimensions, registry: &self.gauges, maker: ExampleGauge.init) return self.register(label: label, dimensions: dimensions, registry: &self.gauges, maker: ExampleGauge.init)
} }
@ -48,11 +48,11 @@ class ExampleMetricsLibrary: MetricsHandler {
return self.register(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker) return self.register(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker)
} }
func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return self.makeTimer(label: label, dimensions: dimensions, options: self.config.timer.aggregationOptions) return self.makeTimer(label: label, dimensions: dimensions, options: self.config.timer.aggregationOptions)
} }
func makeTimer(label: String, dimensions: [(String, String)], options: [AggregationOption]) -> Timer { func makeTimer(label: String, dimensions: [(String, String)], options: [AggregationOption]) -> TimerHandler {
let maker = { (label: String, dimensions: [(String, String)]) -> ExampleTimer in let maker = { (label: String, dimensions: [(String, String)]) -> ExampleTimer in
ExampleTimer(label: label, dimensions: dimensions, options: options) ExampleTimer(label: label, dimensions: dimensions, options: options)
} }
@ -99,7 +99,7 @@ class ExampleMetricsLibrary: MetricsHandler {
} }
} }
class ExampleCounter: Counter, CustomStringConvertible { class ExampleCounter: CounterHandler, CustomStringConvertible {
let label: String let label: String
let dimensions: [(String, String)] let dimensions: [(String, String)]
init(label: String, dimensions: [(String, String)]) { init(label: String, dimensions: [(String, String)]) {
@ -120,7 +120,7 @@ class ExampleCounter: Counter, CustomStringConvertible {
} }
} }
class ExampleRecorder: Recorder, CustomStringConvertible { class ExampleRecorder: RecorderHandler, CustomStringConvertible {
let label: String let label: String
let dimensions: [(String, String)] let dimensions: [(String, String)]
let options: [AggregationOption] let options: [AggregationOption]
@ -226,7 +226,7 @@ class ExampleRecorder: Recorder, CustomStringConvertible {
} }
} }
class ExampleGauge: Recorder, CustomStringConvertible { class ExampleGauge: RecorderHandler, CustomStringConvertible {
let label: String let label: String
let dimensions: [(String, String)] let dimensions: [(String, String)]
init(label: String, dimensions: [(String, String)]) { init(label: String, dimensions: [(String, String)]) {
@ -250,7 +250,7 @@ class ExampleGauge: Recorder, CustomStringConvertible {
} }
} }
class ExampleTimer: ExampleRecorder, Timer { class ExampleTimer: ExampleRecorder, TimerHandler {
func recordNanoseconds(_ duration: Int64) { func recordNanoseconds(_ duration: Int64) {
super.record(duration) super.record(duration)
} }

View File

@ -18,7 +18,7 @@
import Metrics import Metrics
class RandomLibrary { class RandomLibrary {
let methodCallsCounter = Metrics.global.makeCounter(label: "RandomLibrary::TotalMethodCalls") let methodCallsCounter = Counter(label: "RandomLibrary::TotalMethodCalls")
func doSomething() { func doSomething() {
self.methodCallsCounter.increment() self.methodCallsCounter.increment()
@ -26,12 +26,11 @@ class RandomLibrary {
func doSomethingSlow(callback: @escaping () -> Void) { func doSomethingSlow(callback: @escaping () -> Void) {
self.methodCallsCounter.increment() self.methodCallsCounter.increment()
Metrics.global.withTimer(label: "RandomLibrary::doSomethingSlow") { timer in let timer = Timer(label: "RandomLibrary::doSomethingSlow")
let start = Date() let start = Date()
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) { DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) {
timer.record(Date().timeIntervalSince(start)) timer.record(Date().timeIntervalSince(start))
callback() callback()
}
} }
} }
} }

View File

@ -17,23 +17,23 @@
import Metrics import Metrics
class SimpleMetricsLibrary: MetricsHandler { class SimpleMetricsLibrary: MetricsFactory {
init() {} init() {}
func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return ExampleCounter(label, dimensions) return ExampleCounter(label, dimensions)
} }
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let maker: (String, [(String, String)]) -> Recorder = aggregate ? ExampleRecorder.init : ExampleGauge.init let maker: (String, [(String, String)]) -> RecorderHandler = aggregate ? ExampleRecorder.init : ExampleGauge.init
return maker(label, dimensions) return maker(label, dimensions)
} }
func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return ExampleTimer(label, dimensions) return ExampleTimer(label, dimensions)
} }
private class ExampleCounter: Counter { private class ExampleCounter: CounterHandler {
init(_: String, _: [(String, String)]) {} init(_: String, _: [(String, String)]) {}
let lock = NSLock() let lock = NSLock()
@ -45,7 +45,7 @@ class SimpleMetricsLibrary: MetricsHandler {
} }
} }
private class ExampleRecorder: Recorder { private class ExampleRecorder: RecorderHandler {
init(_: String, _: [(String, String)]) {} init(_: String, _: [(String, String)]) {}
private let lock = NSLock() private let lock = NSLock()
@ -88,7 +88,7 @@ class SimpleMetricsLibrary: MetricsHandler {
} }
} }
private class ExampleGauge: Recorder { private class ExampleGauge: RecorderHandler {
init(_: String, _: [(String, String)]) {} init(_: String, _: [(String, String)]) {}
let lock = NSLock() let lock = NSLock()
@ -103,7 +103,7 @@ class SimpleMetricsLibrary: MetricsHandler {
} }
} }
private class ExampleTimer: ExampleRecorder, Timer { private class ExampleTimer: ExampleRecorder, TimerHandler {
func recordNanoseconds(_ duration: Int64) { func recordNanoseconds(_ duration: Int64) {
super.record(duration) super.record(duration)
} }

View File

@ -1,11 +1,12 @@
@_exported import CoreMetrics @_exported import CoreMetrics
@_exported import protocol CoreMetrics.Timer @_exported import class CoreMetrics.Timer
@_exported import Foundation @_exported import Foundation
public extension MetricsHandler { // Convenience for measuring duration of a closure
public extension Timer {
@inlinable @inlinable
func timed<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T { public static func measure<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
let timer = self.makeTimer(label: label, dimensions: dimensions) let timer = Timer(label: label, dimensions: dimensions)
let start = Date() let start = Date()
defer { defer {
timer.record(Date().timeIntervalSince(start)) timer.record(Date().timeIntervalSince(start))
@ -14,14 +15,15 @@ public extension MetricsHandler {
} }
} }
// Convenience for using Foundation and Dispatch
public extension Timer { public extension Timer {
@inlinable @inlinable
func record(_ duration: TimeInterval) { public func record(_ duration: TimeInterval) {
self.recordSeconds(duration) self.recordSeconds(duration)
} }
@inlinable @inlinable
func record(_ duration: DispatchTimeInterval) { public func record(_ duration: DispatchTimeInterval) {
switch duration { switch duration {
case .nanoseconds(let value): case .nanoseconds(let value):
self.recordNanoseconds(Int64(value)) self.recordNanoseconds(Int64(value))

View File

@ -19,10 +19,11 @@ class MetricsTests: XCTestCase {
func testCounters() throws { func testCounters() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup() let group = DispatchGroup()
let name = "counter-\(NSUUID().uuidString)" let name = "counter-\(NSUUID().uuidString)"
let counter = Metrics.global.makeCounter(label: name) as! TestCounter let counter = Counter(label: name)
let testCounter = counter.handler as! TestCounter
let total = Int.random(in: 500 ... 1000) let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total { for _ in 0 ... total {
group.enter() group.enter()
@ -32,17 +33,17 @@ class MetricsTests: XCTestCase {
} }
} }
group.wait() group.wait()
XCTAssertEqual(counter.values.count - 1, total, "expected number of entries to match") XCTAssertEqual(testCounter.values.count - 1, total, "expected number of entries to match")
} }
func testCounterBlock() throws { func testCounterBlock() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let name = "counter-\(NSUUID().uuidString)" let name = "counter-\(NSUUID().uuidString)"
let value = Int.random(in: Int.min ... Int.max) let value = Int.random(in: Int.min ... Int.max)
Metrics.global.withCounter(label: name) { $0.increment(value) } Counter(label: name).increment(value)
let counter = metrics.counters[name] as! TestCounter let counter = metrics.counters[name] as! TestCounter
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match") XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match") XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
@ -51,10 +52,11 @@ class MetricsTests: XCTestCase {
func testRecorders() throws { func testRecorders() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup() let group = DispatchGroup()
let name = "recorder-\(NSUUID().uuidString)" let name = "recorder-\(NSUUID().uuidString)"
let recorder = Metrics.global.makeRecorder(label: name) as! TestRecorder let recorder = Recorder(label: name)
let testRecorder = recorder.handler as! TestRecorder
let total = Int.random(in: 500 ... 1000) let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total { for _ in 0 ... total {
group.enter() group.enter()
@ -64,47 +66,49 @@ class MetricsTests: XCTestCase {
} }
} }
group.wait() group.wait()
XCTAssertEqual(recorder.values.count - 1, total, "expected number of entries to match") XCTAssertEqual(testRecorder.values.count - 1, total, "expected number of entries to match")
} }
func testRecordersInt() throws { func testRecordersInt() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
let recorder = Metrics.global.makeRecorder(label: "test-recorder") as! TestRecorder let recorder = Recorder(label: "test-recorder")
let testRecorder = recorder.handler as! TestRecorder
let values = (0 ... 999).map { _ in Int32.random(in: Int32.min ... Int32.max) } let values = (0 ... 999).map { _ in Int32.random(in: Int32.min ... Int32.max) }
for i in 0 ... values.count - 1 { for i in 0 ... values.count - 1 {
recorder.record(values[i]) recorder.record(values[i])
} }
XCTAssertEqual(values.count, recorder.values.count, "expected number of entries to match") XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
for i in 0 ... values.count - 1 { for i in 0 ... values.count - 1 {
XCTAssertEqual(Int32(recorder.values[i].1), values[i], "expected value #\(i) to match.") XCTAssertEqual(Int32(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
} }
} }
func testRecordersFloat() throws { func testRecordersFloat() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
let recorder = Metrics.global.makeRecorder(label: "test-recorder") as! TestRecorder let recorder = Recorder(label: "test-recorder")
let testRecorder = recorder.handler as! TestRecorder
let values = (0 ... 999).map { _ in Float.random(in: Float(Int32.min) ... Float(Int32.max)) } let values = (0 ... 999).map { _ in Float.random(in: Float(Int32.min) ... Float(Int32.max)) }
for i in 0 ... values.count - 1 { for i in 0 ... values.count - 1 {
recorder.record(values[i]) recorder.record(values[i])
} }
XCTAssertEqual(values.count, recorder.values.count, "expected number of entries to match") XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
for i in 0 ... values.count - 1 { for i in 0 ... values.count - 1 {
XCTAssertEqual(Float(recorder.values[i].1), values[i], "expected value #\(i) to match.") XCTAssertEqual(Float(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
} }
} }
func testRecorderBlock() throws { func testRecorderBlock() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let name = "recorder-\(NSUUID().uuidString)" let name = "recorder-\(NSUUID().uuidString)"
let value = Double.random(in: Double(Int.min) ... Double(Int.max)) let value = Double.random(in: Double(Int.min) ... Double(Int.max))
Metrics.global.withRecorder(label: name) { $0.record(value) } Recorder(label: name).record(value)
let recorder = metrics.recorders[name] as! TestRecorder let recorder = metrics.recorders[name] as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match") XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match") XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
@ -113,10 +117,11 @@ class MetricsTests: XCTestCase {
func testTimers() throws { func testTimers() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup() let group = DispatchGroup()
let name = "timer-\(NSUUID().uuidString)" let name = "timer-\(NSUUID().uuidString)"
let timer = Metrics.global.makeTimer(label: name) as! TestTimer let timer = Timer(label: name)
let testTimer = timer.handler as! TestTimer
let total = Int.random(in: 500 ... 1000) let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total { for _ in 0 ... total {
group.enter() group.enter()
@ -126,17 +131,17 @@ class MetricsTests: XCTestCase {
} }
} }
group.wait() group.wait()
XCTAssertEqual(timer.values.count - 1, total, "expected number of entries to match") XCTAssertEqual(testTimer.values.count - 1, total, "expected number of entries to match")
} }
func testTimerBlock() throws { func testTimerBlock() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let name = "timer-\(NSUUID().uuidString)" let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: Int64.min ... Int64.max) let value = Int64.random(in: Int64.min ... Int64.max)
Metrics.global.withTimer(label: name) { $0.recordNanoseconds(value) } Timer(label: name).recordNanoseconds(value)
let timer = metrics.timers[name] as! TestTimer let timer = metrics.timers[name] as! TestTimer
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match") XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(timer.values[0].1, value, "expected value to match") XCTAssertEqual(timer.values[0].1, value, "expected value to match")
@ -145,41 +150,42 @@ class MetricsTests: XCTestCase {
func testTimerVariants() throws { func testTimerVariants() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let timer = Metrics.global.makeTimer(label: "test-timer") as! TestTimer let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
// nano // nano
let nano = Int64.random(in: 0 ... 5) let nano = Int64.random(in: 0 ... 5)
timer.recordNanoseconds(nano) timer.recordNanoseconds(nano)
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(timer.values[0].1, nano, "expected value to match") XCTAssertEqual(testTimer.values[0].1, nano, "expected value to match")
// micro // micro
let micro = Int64.random(in: 0 ... 5) let micro = Int64.random(in: 0 ... 5)
timer.recordMicroseconds(micro) timer.recordMicroseconds(micro)
XCTAssertEqual(timer.values.count, 2, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(timer.values[1].1, micro * 1000, "expected value to match") XCTAssertEqual(testTimer.values[1].1, micro * 1000, "expected value to match")
// milli // milli
let milli = Int64.random(in: 0 ... 5) let milli = Int64.random(in: 0 ... 5)
timer.recordMilliseconds(milli) timer.recordMilliseconds(milli)
XCTAssertEqual(timer.values.count, 3, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(timer.values[2].1, milli * 1_000_000, "expected value to match") XCTAssertEqual(testTimer.values[2].1, milli * 1_000_000, "expected value to match")
// seconds // seconds
let sec = Int64.random(in: 0 ... 5) let sec = Int64.random(in: 0 ... 5)
timer.recordSeconds(sec) timer.recordSeconds(sec)
XCTAssertEqual(timer.values.count, 4, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(timer.values[3].1, sec * 1_000_000_000, "expected value to match") XCTAssertEqual(testTimer.values[3].1, sec * 1_000_000_000, "expected value to match")
} }
func testGauge() throws { func testGauge() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let name = "gauge-\(NSUUID().uuidString)" let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000) let value = Double.random(in: -1000 ... 1000)
let gauge = Metrics.global.makeGauge(label: name) let gauge = Gauge(label: name)
gauge.record(value) gauge.record(value)
let recorder = gauge as! TestRecorder let recorder = gauge.handler as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match") XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match") XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
} }
@ -187,11 +193,11 @@ class MetricsTests: XCTestCase {
func testGaugeBlock() throws { func testGaugeBlock() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let name = "gauge-\(NSUUID().uuidString)" let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000) let value = Double.random(in: -1000 ... 1000)
Metrics.global.withGauge(label: name) { $0.record(value) } Gauge(label: name).record(value)
let recorder = metrics.recorders[name] as! TestRecorder let recorder = metrics.recorders[name] as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match") XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match") XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
@ -199,19 +205,28 @@ class MetricsTests: XCTestCase {
func testMUX() throws { func testMUX() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let handlers = [TestMetrics(), TestMetrics(), TestMetrics()] let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
Metrics.bootstrap(MultiplexMetricsHandler(handlers: handlers)) MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test // run the test
let name = NSUUID().uuidString let name = NSUUID().uuidString
let value = Int.random(in: Int.min ... Int.max) let value = Int.random(in: Int.min ... Int.max)
Metrics.global.withCounter(label: name) { counter in Counter(label: name).increment(value)
counter.increment(value) factories.forEach { factory in
} let counter = factory.counters.first?.1 as! TestCounter
handlers.forEach { handler in
let counter = handler.counters.first?.1 as! TestCounter
XCTAssertEqual(counter.label, name, "expected label to match") XCTAssertEqual(counter.label, name, "expected label to match")
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match") XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match") XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
} }
} }
func testCustomFactory() {
class CustomHandler: CounterHandler {
func increment<DataType>(_: DataType) where DataType: BinaryInteger {}
}
let counter1 = Counter(label: "foo")
XCTAssertFalse(counter1.handler is CustomHandler, "expected non-custom log handler")
let counter2 = Counter(label: "foo", dimensions: [], handler: CustomHandler())
XCTAssertTrue(counter2.handler is CustomHandler, "expected custom log handler")
}
} }

View File

@ -12,6 +12,7 @@
// //
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
@testable import CoreMetrics
@testable import Metrics @testable import Metrics
import XCTest import XCTest
@ -19,11 +20,11 @@ class MetricsExtensionsTests: XCTestCase {
func testTimerBlock() throws { func testTimerBlock() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let name = "timer-\(NSUUID().uuidString)" let name = "timer-\(NSUUID().uuidString)"
let delay = 0.05 let delay = 0.05
Metrics.global.timed(label: name) { Timer.measure(label: name) {
Thread.sleep(forTimeInterval: delay) Thread.sleep(forTimeInterval: delay)
} }
let timer = metrics.timers[name] as! TestTimer let timer = metrics.timers[name] as! TestTimer
@ -34,44 +35,46 @@ class MetricsExtensionsTests: XCTestCase {
func testTimerWithTimeInterval() throws { func testTimerWithTimeInterval() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let timer = Metrics.global.makeTimer(label: "test-timer") as! TestTimer let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
let timeInterval = TimeInterval(Double.random(in: 1 ... 500)) let timeInterval = TimeInterval(Double.random(in: 1 ... 500))
timer.record(timeInterval) timer.record(timeInterval)
XCTAssertEqual(1, timer.values.count, "expected number of entries to match") XCTAssertEqual(1, testTimer.values.count, "expected number of entries to match")
XCTAssertEqual(timer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match") XCTAssertEqual(testTimer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match")
} }
func testTimerWithDispatchTime() throws { func testTimerWithDispatchTime() throws {
// bootstrap with our test metrics // bootstrap with our test metrics
let metrics = TestMetrics() let metrics = TestMetrics()
Metrics.bootstrap(metrics) MetricsSystem.bootstrapInternal(metrics)
// run the test // run the test
let timer = Metrics.global.makeTimer(label: "test-timer") as! TestTimer let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
// nano // nano
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500)) let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500))
timer.record(nano) timer.record(nano)
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(timer.values[0].1)), nano, "expected value to match") XCTAssertEqual(.nanoseconds(Int(testTimer.values[0].1)), nano, "expected value to match")
// micro // micro
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500)) let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500))
timer.record(micro) timer.record(micro)
XCTAssertEqual(timer.values.count, 2, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(timer.values[1].1)), micro, "expected value to match") XCTAssertEqual(.nanoseconds(Int(testTimer.values[1].1)), micro, "expected value to match")
// milli // milli
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500)) let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500))
timer.record(milli) timer.record(milli)
XCTAssertEqual(timer.values.count, 3, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(timer.values[2].1)), milli, "expected value to match") XCTAssertEqual(.nanoseconds(Int(testTimer.values[2].1)), milli, "expected value to match")
// seconds // seconds
let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500)) let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500))
timer.record(sec) timer.record(sec)
XCTAssertEqual(timer.values.count, 4, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(timer.values[3].1)), sec, "expected value to match") XCTAssertEqual(.nanoseconds(Int(testTimer.values[3].1)), sec, "expected value to match")
// never // never
timer.record(DispatchTimeInterval.never) timer.record(DispatchTimeInterval.never)
XCTAssertEqual(timer.values.count, 5, "expected number of entries to match") XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match")
XCTAssertEqual(timer.values[4].1, 0, "expected value to match") XCTAssertEqual(testTimer.values[4].1, 0, "expected value to match")
} }
} }

View File

@ -13,27 +13,27 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
@testable import CoreMetrics @testable import CoreMetrics
@testable import protocol CoreMetrics.Timer @testable import class CoreMetrics.Timer
import Foundation import Foundation
internal class TestMetrics: MetricsHandler { internal class TestMetrics: MetricsFactory {
private let lock = NSLock() // TODO: consider lock per cache? private let lock = NSLock() // TODO: consider lock per cache?
var counters = [String: Counter]() var counters = [String: CounterHandler]()
var recorders = [String: Recorder]() var recorders = [String: RecorderHandler]()
var timers = [String: Timer]() var timers = [String: TimerHandler]()
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return self.make(label: label, dimensions: dimensions, registry: &self.counters, maker: TestCounter.init) return self.make(label: label, dimensions: dimensions, registry: &self.counters, maker: TestCounter.init)
} }
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let maker = { (label: String, dimensions: [(String, String)]) -> Recorder in let maker = { (label: String, dimensions: [(String, String)]) -> RecorderHandler in
TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate) TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
} }
return self.make(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker) return self.make(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker)
} }
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return self.make(label: label, dimensions: dimensions, registry: &self.timers, maker: TestTimer.init) return self.make(label: label, dimensions: dimensions, registry: &self.timers, maker: TestTimer.init)
} }
@ -46,7 +46,7 @@ internal class TestMetrics: MetricsHandler {
} }
} }
internal class TestCounter: Counter, Equatable { internal class TestCounter: CounterHandler, Equatable {
let id: String let id: String
let label: String let label: String
let dimensions: [(String, String)] let dimensions: [(String, String)]
@ -72,7 +72,7 @@ internal class TestCounter: Counter, Equatable {
} }
} }
internal class TestRecorder: Recorder, Equatable { internal class TestRecorder: RecorderHandler, Equatable {
let id: String let id: String
let label: String let label: String
let dimensions: [(String, String)] let dimensions: [(String, String)]
@ -105,7 +105,7 @@ internal class TestRecorder: Recorder, Equatable {
} }
} }
internal class TestTimer: Timer, Equatable { internal class TestTimer: TimerHandler, Equatable {
let id: String let id: String
let label: String let label: String
let dimensions: [(String, String)] let dimensions: [(String, String)]