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)
}
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<DataType: BinaryInteger>(_ 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<DataType: BinaryInteger>(_ 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)
}
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<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)
}
@inlinable
func recordMicroseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
public func recordMicroseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1000))
}
@inlinable
func recordMilliseconds<DataType: BinaryInteger>(_ duration: DataType) {
public func recordMilliseconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1_000_000)
}
@inlinable
func recordMilliseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
public func recordMilliseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1_000_000))
}
@inlinable
func recordSeconds<DataType: BinaryInteger>(_ duration: DataType) {
public func recordSeconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1_000_000_000)
}
@inlinable
func recordSeconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
public func recordSeconds<DataType: BinaryFloatingPoint>(_ 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<DataType: BinaryInteger>(_ 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<DataType: BinaryInteger>(_ 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
}

View File

@ -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))) {

View File

@ -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)
}

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -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<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
let timer = self.makeTimer(label: label, dimensions: dimensions)
public static func measure<T>(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))

View File

@ -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>(_: 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
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")
}
}

View File

@ -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)]