diff --git a/Sources/CoreMetrics/Metrics.swift b/Sources/CoreMetrics/Metrics.swift index 7804054..5e9604d 100644 --- a/Sources/CoreMetrics/Metrics.swift +++ b/Sources/CoreMetrics/Metrics.swift @@ -12,164 +12,211 @@ // //===----------------------------------------------------------------------===// -public protocol Counter: AnyObject { +/// This is the Counter protocol a metrics library implements +public protocol CounterHandler: AnyObject { func increment(_ 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 - func increment() { + public func increment(_ value: DataType) { + self.handler.increment(value) + } + + @inlinable + public func increment() { 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(_ value: DataType) func record(_ 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(_ value: DataType) { + self.handler.record(value) + } + + @inlinable + public func record(_ 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) } -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 - func recordMicroseconds(_ duration: DataType) { + public func recordNanoseconds(_ duration: Int64) { + self.handler.recordNanoseconds(duration) + } + + @inlinable + public func recordMicroseconds(_ duration: DataType) { self.recordNanoseconds(Int64(duration) * 1000) } @inlinable - func recordMicroseconds(_ duration: DataType) { + public func recordMicroseconds(_ duration: DataType) { self.recordNanoseconds(Int64(duration * 1000)) } @inlinable - func recordMilliseconds(_ duration: DataType) { + public func recordMilliseconds(_ duration: DataType) { self.recordNanoseconds(Int64(duration) * 1_000_000) } @inlinable - func recordMilliseconds(_ duration: DataType) { + public func recordMilliseconds(_ duration: DataType) { self.recordNanoseconds(Int64(duration * 1_000_000)) } @inlinable - func recordSeconds(_ duration: DataType) { + public func recordSeconds(_ duration: DataType) { self.recordNanoseconds(Int64(duration) * 1_000_000_000) } @inlinable - func recordSeconds(_ duration: DataType) { + public func recordSeconds(_ duration: DataType) { self.recordNanoseconds(Int64(duration * 1_000_000_000)) } } -public protocol MetricsHandler { - func makeCounter(label: String, dimensions: [(String, String)]) -> Counter - func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder - func makeTimer(label: String, dimensions: [(String, String)]) -> Timer -} - -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 Timer { + public convenience init(label: String, dimensions: [(String, String)] = []) { + let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions) + self.init(label: label, dimensions: dimensions, handler: handler) } } -public extension MetricsHandler { - @inlinable - func makeGauge(label: String, dimensions: [(String, String)] = []) -> Recorder { - return self.makeRecorder(label: label, dimensions: dimensions, aggregate: false) - } +public protocol MetricsFactory { + func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler + func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler + func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler } -public extension MetricsHandler { - @inlinable - func withCounter(label: String, dimensions: [(String, String)] = [], then: (Counter) -> Void) { - then(self.makeCounter(label: label, dimensions: dimensions)) - } +// This is the metrics system itself, it's mostly used set the type of the `MetricsFactory` implementation. +public enum MetricsSystem { + fileprivate static let lock = ReadWriteLock() + fileprivate static var _factory: MetricsFactory = NOOPMetricsHandler.instance + fileprivate static var initialized = false - @inlinable - func withRecorder(label: String, dimensions: [(String, String)] = [], aggregate: Bool = true, then: (Recorder) -> Void) { - then(self.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)) - } - - @inlinable - 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 + // Configures which `LogHandler` to use in the application. + public static func bootstrap(_ factory: MetricsFactory) { + self.lock.withWriterLock { + precondition(!self.initialized, "metrics system can only be initialized once per process. currently used factory: \(self.factory)") + self._factory = factory + self.initialized = true } } - public static var global: MetricsHandler { - return self.lock.withReaderLock { self._handler } + // for our testing we want to allow multiple bootstraping + 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 { - @inlinable - func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { - return Metrics.global.makeCounter(label: label, dimensions: dimensions) +/// Ships with the metrics module, used to multiplex to multiple metrics handlers +public final class MultiplexMetricsHandler: MetricsFactory { + private let factories: [MetricsFactory] + public init(factories: [MetricsFactory]) { + self.factories = factories } - @inlinable - func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { - return Metrics.global.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) + public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { + return MuxCounter(factories: self.factories, label: label, dimensions: dimensions) } - @inlinable - func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { - 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 makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { + return MuxRecorder(factories: self.factories, label: label, dimensions: dimensions, aggregate: aggregate) } - public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { - return MuxCounter(handlers: self.handlers, label: label, dimensions: dimensions) + public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { + return MuxTimer(factories: self.factories, label: label, dimensions: dimensions) } - public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { - return MuxRecorder(handlers: self.handlers, label: label, dimensions: dimensions, aggregate: aggregate) - } - - 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) } + private class MuxCounter: CounterHandler { + let counters: [CounterHandler] + public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)]) { + self.counters = factories.map { $0.makeCounter(label: label, dimensions: dimensions) } } func increment(_ value: DataType) { @@ -177,10 +224,10 @@ public final class MultiplexMetricsHandler: MetricsHandler { } } - private class MuxRecorder: Recorder { - let recorders: [Recorder] - public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)], aggregate: Bool) { - self.recorders = handlers.map { $0.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) } + private class MuxRecorder: RecorderHandler { + let recorders: [RecorderHandler] + public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)], aggregate: Bool) { + self.recorders = factories.map { $0.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) } } func record(_ value: DataType) { @@ -192,10 +239,10 @@ public final class MultiplexMetricsHandler: MetricsHandler { } } - private class MuxTimer: Timer { - let timers: [Timer] - public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)]) { - self.timers = handlers.map { $0.makeTimer(label: label, dimensions: dimensions) } + private class MuxTimer: TimerHandler { + let timers: [TimerHandler] + public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)]) { + self.timers = factories.map { $0.makeTimer(label: label, dimensions: dimensions) } } 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() private init() {} - public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { + public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { 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 } - public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { + public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { return self } diff --git a/Sources/Examples/Example1.swift b/Sources/Examples/Example1.swift index 0c9a249..3ce1654 100644 --- a/Sources/Examples/Example1.swift +++ b/Sources/Examples/Example1.swift @@ -21,7 +21,7 @@ enum Example1 { static func main() { // bootstrap with our example metrics library let metrics = ExampleMetricsLibrary() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrap(metrics) let server = Server() let client = Client(server: server) @@ -38,7 +38,7 @@ enum Example1 { } class Client { - private let activeRequestsGauge = Metrics.global.makeGauge(label: "Client::ActiveRequests") + private let activeRequestsGauge = Gauge(label: "Client::ActiveRequests") private let server: Server init(server: Server) { self.server = server @@ -46,9 +46,9 @@ enum Example1 { func run(iterations: Int) { let group = DispatchGroup() - let requestsCounter = Metrics.global.makeCounter(label: "Client::TotalRequests") - let requestTimer = Metrics.global.makeTimer(label: "Client::doSomethig") - let resultRecorder = Metrics.global.makeRecorder(label: "Client::doSomethig::result") + let requestsCounter = Counter(label: "Client::TotalRequests") + let requestTimer = Timer(label: "Client::doSomethig") + let resultRecorder = Recorder(label: "Client::doSomethig::result") for _ in 0 ... iterations { group.enter() let start = Date() @@ -78,10 +78,10 @@ enum Example1 { class Server { let library = RandomLibrary() - let requestsCounter = Metrics.global.makeCounter(label: "Server::TotalRequests") + let requestsCounter = Counter(label: "Server::TotalRequests") func doSomethig(callback: @escaping (Int64) -> Void) { - let timer = Metrics.global.makeTimer(label: "Server::doSomethig") + let timer = Timer(label: "Server::doSomethig") let start = Date() requestsCounter.increment() DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) { diff --git a/Sources/Examples/ExampleMetricsLibrary.swift b/Sources/Examples/ExampleMetricsLibrary.swift index 3e122a1..59c16c0 100644 --- a/Sources/Examples/ExampleMetricsLibrary.swift +++ b/Sources/Examples/ExampleMetricsLibrary.swift @@ -17,7 +17,7 @@ import Metrics -class ExampleMetricsLibrary: MetricsHandler { +class ExampleMetricsLibrary: MetricsFactory { private let config: Config private let lock = NSLock() var counters = [ExampleCounter]() @@ -29,16 +29,16 @@ class ExampleMetricsLibrary: MetricsHandler { 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) } - 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 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 { 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) } - 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) } - 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 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 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 dimensions: [(String, String)] let options: [AggregationOption] @@ -226,7 +226,7 @@ class ExampleRecorder: Recorder, CustomStringConvertible { } } -class ExampleGauge: Recorder, CustomStringConvertible { +class ExampleGauge: RecorderHandler, CustomStringConvertible { let label: String let 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) { super.record(duration) } diff --git a/Sources/Examples/RandomLibrary.swift b/Sources/Examples/RandomLibrary.swift index de770f8..101bb2f 100644 --- a/Sources/Examples/RandomLibrary.swift +++ b/Sources/Examples/RandomLibrary.swift @@ -18,7 +18,7 @@ import Metrics class RandomLibrary { - let methodCallsCounter = Metrics.global.makeCounter(label: "RandomLibrary::TotalMethodCalls") + let methodCallsCounter = Counter(label: "RandomLibrary::TotalMethodCalls") func doSomething() { self.methodCallsCounter.increment() @@ -26,12 +26,11 @@ class RandomLibrary { func doSomethingSlow(callback: @escaping () -> Void) { self.methodCallsCounter.increment() - Metrics.global.withTimer(label: "RandomLibrary::doSomethingSlow") { timer in - let start = Date() - DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) { - timer.record(Date().timeIntervalSince(start)) - callback() - } + let timer = Timer(label: "RandomLibrary::doSomethingSlow") + let start = Date() + DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) { + timer.record(Date().timeIntervalSince(start)) + callback() } } } diff --git a/Sources/Examples/SimpleMetricsLibrary.swift b/Sources/Examples/SimpleMetricsLibrary.swift index ccc49a1..6f08eec 100644 --- a/Sources/Examples/SimpleMetricsLibrary.swift +++ b/Sources/Examples/SimpleMetricsLibrary.swift @@ -17,23 +17,23 @@ import Metrics -class SimpleMetricsLibrary: MetricsHandler { +class SimpleMetricsLibrary: MetricsFactory { init() {} - func makeCounter(label: String, dimensions: [(String, String)]) -> Counter { + func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { return ExampleCounter(label, dimensions) } - func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { - let maker: (String, [(String, String)]) -> Recorder = aggregate ? ExampleRecorder.init : ExampleGauge.init + func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { + let maker: (String, [(String, String)]) -> RecorderHandler = aggregate ? ExampleRecorder.init : ExampleGauge.init return maker(label, dimensions) } - func makeTimer(label: String, dimensions: [(String, String)]) -> Timer { + func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { return ExampleTimer(label, dimensions) } - private class ExampleCounter: Counter { + private class ExampleCounter: CounterHandler { init(_: String, _: [(String, String)]) {} let lock = NSLock() @@ -45,7 +45,7 @@ class SimpleMetricsLibrary: MetricsHandler { } } - private class ExampleRecorder: Recorder { + private class ExampleRecorder: RecorderHandler { init(_: String, _: [(String, String)]) {} private let lock = NSLock() @@ -88,7 +88,7 @@ class SimpleMetricsLibrary: MetricsHandler { } } - private class ExampleGauge: Recorder { + private class ExampleGauge: RecorderHandler { init(_: String, _: [(String, String)]) {} let lock = NSLock() @@ -103,7 +103,7 @@ class SimpleMetricsLibrary: MetricsHandler { } } - private class ExampleTimer: ExampleRecorder, Timer { + private class ExampleTimer: ExampleRecorder, TimerHandler { func recordNanoseconds(_ duration: Int64) { super.record(duration) } diff --git a/Sources/Metrics/Metrics.swift b/Sources/Metrics/Metrics.swift index 3b8c22b..61522fe 100644 --- a/Sources/Metrics/Metrics.swift +++ b/Sources/Metrics/Metrics.swift @@ -1,11 +1,12 @@ @_exported import CoreMetrics -@_exported import protocol CoreMetrics.Timer +@_exported import class CoreMetrics.Timer @_exported import Foundation -public extension MetricsHandler { +// Convenience for measuring duration of a closure +public extension Timer { @inlinable - func timed(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T { - let timer = self.makeTimer(label: label, dimensions: dimensions) + public static func measure(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T { + let timer = Timer(label: label, dimensions: dimensions) let start = Date() defer { timer.record(Date().timeIntervalSince(start)) @@ -14,14 +15,15 @@ public extension MetricsHandler { } } +// Convenience for using Foundation and Dispatch public extension Timer { @inlinable - func record(_ duration: TimeInterval) { + public func record(_ duration: TimeInterval) { self.recordSeconds(duration) } @inlinable - func record(_ duration: DispatchTimeInterval) { + public func record(_ duration: DispatchTimeInterval) { switch duration { case .nanoseconds(let value): self.recordNanoseconds(Int64(value)) diff --git a/Tests/MetricsTests/CoreMetricsTests.swift b/Tests/MetricsTests/CoreMetricsTests.swift index 8f59b5b..621795e 100644 --- a/Tests/MetricsTests/CoreMetricsTests.swift +++ b/Tests/MetricsTests/CoreMetricsTests.swift @@ -19,10 +19,11 @@ class MetricsTests: XCTestCase { func testCounters() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) let group = DispatchGroup() 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) for _ in 0 ... total { group.enter() @@ -32,17 +33,17 @@ class MetricsTests: XCTestCase { } } 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 { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // run the test let name = "counter-\(NSUUID().uuidString)" 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 XCTAssertEqual(counter.values.count, 1, "expected number of entries to match") XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match") @@ -51,10 +52,11 @@ class MetricsTests: XCTestCase { func testRecorders() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) let group = DispatchGroup() 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) for _ in 0 ... total { group.enter() @@ -64,47 +66,49 @@ class MetricsTests: XCTestCase { } } 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 { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) - let recorder = Metrics.global.makeRecorder(label: "test-recorder") as! TestRecorder + MetricsSystem.bootstrapInternal(metrics) + 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) } for i in 0 ... values.count - 1 { 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 { - 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 { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) - let recorder = Metrics.global.makeRecorder(label: "test-recorder") as! TestRecorder + MetricsSystem.bootstrapInternal(metrics) + 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)) } for i in 0 ... values.count - 1 { 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 { - 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 { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // run the test let name = "recorder-\(NSUUID().uuidString)" 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 XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match") XCTAssertEqual(recorder.values[0].1, value, "expected value to match") @@ -113,10 +117,11 @@ class MetricsTests: XCTestCase { func testTimers() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) let group = DispatchGroup() 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) for _ in 0 ... total { group.enter() @@ -126,17 +131,17 @@ class MetricsTests: XCTestCase { } } 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 { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // run the test let name = "timer-\(NSUUID().uuidString)" 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 XCTAssertEqual(timer.values.count, 1, "expected number of entries to match") XCTAssertEqual(timer.values[0].1, value, "expected value to match") @@ -145,41 +150,42 @@ class MetricsTests: XCTestCase { func testTimerVariants() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // 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 let nano = Int64.random(in: 0 ... 5) timer.recordNanoseconds(nano) - XCTAssertEqual(timer.values.count, 1, "expected number of entries to match") - XCTAssertEqual(timer.values[0].1, nano, "expected value to match") + XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match") + XCTAssertEqual(testTimer.values[0].1, nano, "expected value to match") // micro let micro = Int64.random(in: 0 ... 5) timer.recordMicroseconds(micro) - XCTAssertEqual(timer.values.count, 2, "expected number of entries to match") - XCTAssertEqual(timer.values[1].1, micro * 1000, "expected value to match") + XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match") + XCTAssertEqual(testTimer.values[1].1, micro * 1000, "expected value to match") // milli let milli = Int64.random(in: 0 ... 5) timer.recordMilliseconds(milli) - XCTAssertEqual(timer.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.count, 3, "expected number of entries to match") + XCTAssertEqual(testTimer.values[2].1, milli * 1_000_000, "expected value to match") // seconds let sec = Int64.random(in: 0 ... 5) timer.recordSeconds(sec) - XCTAssertEqual(timer.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.count, 4, "expected number of entries to match") + XCTAssertEqual(testTimer.values[3].1, sec * 1_000_000_000, "expected value to match") } func testGauge() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // run the test let name = "gauge-\(NSUUID().uuidString)" let value = Double.random(in: -1000 ... 1000) - let gauge = Metrics.global.makeGauge(label: name) + let gauge = Gauge(label: name) 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[0].1, value, "expected value to match") } @@ -187,11 +193,11 @@ class MetricsTests: XCTestCase { func testGaugeBlock() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // run the test let name = "gauge-\(NSUUID().uuidString)" 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 XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match") XCTAssertEqual(recorder.values[0].1, value, "expected value to match") @@ -199,19 +205,28 @@ class MetricsTests: XCTestCase { func testMUX() throws { // bootstrap with our test metrics - let handlers = [TestMetrics(), TestMetrics(), TestMetrics()] - Metrics.bootstrap(MultiplexMetricsHandler(handlers: handlers)) + let factories = [TestMetrics(), TestMetrics(), TestMetrics()] + MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories)) // run the test let name = NSUUID().uuidString let value = Int.random(in: Int.min ... Int.max) - Metrics.global.withCounter(label: name) { counter in - counter.increment(value) - } - handlers.forEach { handler in - let counter = handler.counters.first?.1 as! TestCounter + Counter(label: name).increment(value) + factories.forEach { factory in + let counter = factory.counters.first?.1 as! TestCounter XCTAssertEqual(counter.label, name, "expected label to match") XCTAssertEqual(counter.values.count, 1, "expected number of entries to match") XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match") } } + + func testCustomFactory() { + class CustomHandler: CounterHandler { + func increment(_: 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") + } } diff --git a/Tests/MetricsTests/MetricsTests.swift b/Tests/MetricsTests/MetricsTests.swift index cb4724c..234703f 100644 --- a/Tests/MetricsTests/MetricsTests.swift +++ b/Tests/MetricsTests/MetricsTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +@testable import CoreMetrics @testable import Metrics import XCTest @@ -19,11 +20,11 @@ class MetricsExtensionsTests: XCTestCase { func testTimerBlock() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // run the test let name = "timer-\(NSUUID().uuidString)" let delay = 0.05 - Metrics.global.timed(label: name) { + Timer.measure(label: name) { Thread.sleep(forTimeInterval: delay) } let timer = metrics.timers[name] as! TestTimer @@ -34,44 +35,46 @@ class MetricsExtensionsTests: XCTestCase { func testTimerWithTimeInterval() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // 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)) timer.record(timeInterval) - XCTAssertEqual(1, timer.values.count, "expected number of entries to match") - XCTAssertEqual(timer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match") + XCTAssertEqual(1, testTimer.values.count, "expected number of entries to match") + XCTAssertEqual(testTimer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match") } func testTimerWithDispatchTime() throws { // bootstrap with our test metrics let metrics = TestMetrics() - Metrics.bootstrap(metrics) + MetricsSystem.bootstrapInternal(metrics) // 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 let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500)) timer.record(nano) - XCTAssertEqual(timer.values.count, 1, "expected number of entries to match") - XCTAssertEqual(.nanoseconds(Int(timer.values[0].1)), nano, "expected value to match") + XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match") + XCTAssertEqual(.nanoseconds(Int(testTimer.values[0].1)), nano, "expected value to match") // micro let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500)) timer.record(micro) - XCTAssertEqual(timer.values.count, 2, "expected number of entries to match") - XCTAssertEqual(.nanoseconds(Int(timer.values[1].1)), micro, "expected value to match") + XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match") + XCTAssertEqual(.nanoseconds(Int(testTimer.values[1].1)), micro, "expected value to match") // milli let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500)) timer.record(milli) - XCTAssertEqual(timer.values.count, 3, "expected number of entries to match") - XCTAssertEqual(.nanoseconds(Int(timer.values[2].1)), milli, "expected value to match") + XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match") + XCTAssertEqual(.nanoseconds(Int(testTimer.values[2].1)), milli, "expected value to match") // seconds let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500)) timer.record(sec) - XCTAssertEqual(timer.values.count, 4, "expected number of entries to match") - XCTAssertEqual(.nanoseconds(Int(timer.values[3].1)), sec, "expected value to match") + XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match") + XCTAssertEqual(.nanoseconds(Int(testTimer.values[3].1)), sec, "expected value to match") // never timer.record(DispatchTimeInterval.never) - XCTAssertEqual(timer.values.count, 5, "expected number of entries to match") - XCTAssertEqual(timer.values[4].1, 0, "expected value to match") + XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match") + XCTAssertEqual(testTimer.values[4].1, 0, "expected value to match") } } diff --git a/Tests/MetricsTests/TestMetrics.swift b/Tests/MetricsTests/TestMetrics.swift index 3a179b1..d092f02 100644 --- a/Tests/MetricsTests/TestMetrics.swift +++ b/Tests/MetricsTests/TestMetrics.swift @@ -13,27 +13,27 @@ //===----------------------------------------------------------------------===// @testable import CoreMetrics -@testable import protocol CoreMetrics.Timer +@testable import class CoreMetrics.Timer import Foundation -internal class TestMetrics: MetricsHandler { +internal class TestMetrics: MetricsFactory { private let lock = NSLock() // TODO: consider lock per cache? - var counters = [String: Counter]() - var recorders = [String: Recorder]() - var timers = [String: Timer]() + var counters = [String: CounterHandler]() + var recorders = [String: RecorderHandler]() + 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) } - public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder { - let maker = { (label: String, dimensions: [(String, String)]) -> Recorder in + public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { + let maker = { (label: String, dimensions: [(String, String)]) -> RecorderHandler in TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate) } 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) } @@ -46,7 +46,7 @@ internal class TestMetrics: MetricsHandler { } } -internal class TestCounter: Counter, Equatable { +internal class TestCounter: CounterHandler, Equatable { let id: String let label: 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 label: 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 label: String let dimensions: [(String, String)]