//===----------------------------------------------------------------------===// // // This source file is part of the Swift Metrics API open source project // // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// // MARK: - User API // MARK: - Counter extension Counter { /// Create a new `Counter`. /// /// - parameters: /// - label: The label for the `Counter`. /// - dimensions: The dimensions for the `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) } /// Signal the underlying metrics library that this counter will never be updated again. /// In response the library MAY decide to eagerly release any resources held by this `Counter`. @inlinable public func destroy() { MetricsSystem.factory.destroyCounter(self.handler) } } /// A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero. /// For example, you can use a counter to represent the number of requests served, tasks completed, or errors. /// /// This is the user-facing Counter API. /// /// Its behavior depends on the `CounterHandler` implementation. public final class Counter { @usableFromInline let handler: CounterHandler public let label: String public let dimensions: [(String, String)] /// Alternative way to create a new `Counter`, while providing an explicit `CounterHandler`. /// /// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one. /// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason. /// /// - SeeAlso: Use `init(label:dimensions:)` to create `Counter` instances using the configured metrics backend. /// /// - parameters: /// - label: The label for the `Counter`. /// - dimensions: The dimensions for the `Counter`. /// - handler: The custom backend. public init(label: String, dimensions: [(String, String)], handler: CounterHandler) { self.label = label self.dimensions = dimensions self.handler = handler } /// Increment the counter. /// /// - parameters: /// - by: Amount to increment by. @inlinable public func increment(by amount: DataType) { self.handler.increment(by: Int64(amount)) } /// Increment the counter by one. @inlinable public func increment() { self.increment(by: 1) } /// Reset the counter back to zero. @inlinable public func reset() { self.handler.reset() } } extension Counter: CustomStringConvertible { public var description: String { return "Counter(\(self.label), dimensions: \(self.dimensions))" } } // MARK: - FloatingPointCounter extension FloatingPointCounter { /// Create a new `FloatingPointCounter`. /// /// - parameters: /// - label: The label for the `FloatingPointCounter`. /// - dimensions: The dimensions for the `FloatingPointCounter`. public convenience init(label: String, dimensions: [(String, String)] = []) { let handler = MetricsSystem.factory.makeFloatingPointCounter(label: label, dimensions: dimensions) self.init(label: label, dimensions: dimensions, handler: handler) } /// Signal the underlying metrics library that this FloatingPointCounter will never be updated again. /// In response the library MAY decide to eagerly release any resources held by this `FloatingPointCounter`. @inlinable public func destroy() { MetricsSystem.factory.destroyFloatingPointCounter(self.handler) } } /// A FloatingPointCounter is a cumulative metric that represents a single monotonically increasing FloatingPointCounter whose value can only increase or be reset to zero. /// For example, you can use a FloatingPointCounter to represent the number of requests served, tasks completed, or errors. /// FloatingPointCounter is not supported by all metrics backends, however a default implementation is provided which accumulates floating point values and records increments to a standard Counter after crossing integer boundaries. /// /// This is the user-facing FloatingPointCounter API. /// /// Its behavior depends on the `FloatingCounterHandler` implementation. public final class FloatingPointCounter { @usableFromInline let handler: FloatingPointCounterHandler public let label: String public let dimensions: [(String, String)] /// Alternative way to create a new `FloatingPointCounter`, while providing an explicit `FloatingPointCounterHandler`. /// /// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one. /// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason. /// /// - SeeAlso: Use `init(label:dimensions:)` to create `FloatingPointCounter` instances using the configured metrics backend. /// /// - parameters: /// - label: The label for the `FloatingPointCounter`. /// - dimensions: The dimensions for the `FloatingPointCounter`. /// - handler: The custom backend. public init(label: String, dimensions: [(String, String)], handler: FloatingPointCounterHandler) { self.label = label self.dimensions = dimensions self.handler = handler } /// Increment the FloatingPointCounter. /// /// - parameters: /// - by: Amount to increment by. @inlinable public func increment(by amount: DataType) { self.handler.increment(by: Double(amount)) } /// Increment the FloatingPointCounter by one. @inlinable public func increment() { self.increment(by: 1.0) } /// Reset the FloatingPointCounter back to zero. @inlinable public func reset() { self.handler.reset() } } extension FloatingPointCounter: CustomStringConvertible { public var description: String { return "FloatingPointCounter(\(self.label), dimensions: \(self.dimensions))" } } // MARK: - Recorder extension Recorder { /// Create a new `Recorder`. /// /// - parameters: /// - label: The label for the `Recorder`. /// - dimensions: The dimensions for the `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) } /// Signal the underlying metrics library that this recorder will never be updated again. /// In response the library MAY decide to eagerly release any resources held by this `Recorder`. @inlinable public func destroy() { MetricsSystem.factory.destroyRecorder(self.handler) } } /// A recorder collects observations within a time window (usually things like response sizes) and *can* provide aggregated information about the data sample, for example, count, sum, min, max and various quantiles. /// /// This is the user-facing Recorder API. /// /// Its behavior depends on the `RecorderHandler` implementation. public class Recorder { @usableFromInline let handler: RecorderHandler public let label: String public let dimensions: [(String, String)] public let aggregate: Bool /// Alternative way to create a new `Recorder`, while providing an explicit `RecorderHandler`. /// /// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one. /// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason. /// /// - SeeAlso: Use `init(label:dimensions:)` to create `Recorder` instances using the configured metrics backend. /// /// - parameters: /// - label: The label for the `Recorder`. /// - dimensions: The dimensions for the `Recorder`. /// - handler: The custom backend. public init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) { self.label = label self.dimensions = dimensions self.aggregate = aggregate self.handler = handler } /// Record a value. /// /// Recording a value is meant to have "set" semantics, rather than "add" semantics. /// This means that the value of this `Recorder` will match the passed in value, rather than accumulate and sum the values up. /// /// - parameters: /// - value: Value to record. @inlinable public func record(_ value: DataType) { self.handler.record(Int64(value)) } /// Record a value. /// /// Recording a value is meant to have "set" semantics, rather than "add" semantics. /// This means that the value of this `Recorder` will match the passed in value, rather than accumulate and sum the values up. /// /// - parameters: /// - value: Value to record. @inlinable public func record(_ value: DataType) { self.handler.record(Double(value)) } } extension Recorder: CustomStringConvertible { public var description: String { return "\(type(of: self))(\(self.label), dimensions: \(self.dimensions), aggregate: \(self.aggregate))" } } // MARK: - Gauge /// A gauge is a metric that represents a single numerical value that can arbitrarily go up and down. /// Gauges are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. /// Gauges are modeled as `Recorder` with a sample size of 1 and that does not perform any aggregation. public final class Gauge: Recorder { /// Create a new `Gauge`. /// /// - parameters: /// - label: The label for the `Gauge`. /// - dimensions: The dimensions for the `Gauge`. public convenience init(label: String, dimensions: [(String, String)] = []) { self.init(label: label, dimensions: dimensions, aggregate: false) } } // MARK: - Timer public struct TimeUnit: Equatable { private enum Code: Equatable { case nanoseconds case microseconds case milliseconds case seconds case minutes case hours case days } private let code: Code public let scaleFromNanoseconds: UInt64 private init(code: Code, scaleFromNanoseconds: UInt64) { assert(scaleFromNanoseconds > 0, "invalid scale from nanoseconds") self.code = code self.scaleFromNanoseconds = scaleFromNanoseconds } public static let nanoseconds = TimeUnit(code: .nanoseconds, scaleFromNanoseconds: 1) public static let microseconds = TimeUnit(code: .microseconds, scaleFromNanoseconds: 1000) public static let milliseconds = TimeUnit(code: .milliseconds, scaleFromNanoseconds: 1000 * TimeUnit.microseconds.scaleFromNanoseconds) public static let seconds = TimeUnit(code: .seconds, scaleFromNanoseconds: 1000 * TimeUnit.milliseconds.scaleFromNanoseconds) public static let minutes = TimeUnit(code: .minutes, scaleFromNanoseconds: 60 * TimeUnit.seconds.scaleFromNanoseconds) public static let hours = TimeUnit(code: .hours, scaleFromNanoseconds: 60 * TimeUnit.minutes.scaleFromNanoseconds) public static let days = TimeUnit(code: .days, scaleFromNanoseconds: 24 * TimeUnit.hours.scaleFromNanoseconds) } public extension Timer { /// Create a new `Timer`. /// /// - parameters: /// - label: The label for the `Timer`. /// - dimensions: The dimensions for the `Timer`. convenience init(label: String, dimensions: [(String, String)] = []) { let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions) self.init(label: label, dimensions: dimensions, handler: handler) } /// Create a new `Timer`. /// /// - parameters: /// - label: The label for the `Timer`. /// - dimensions: The dimensions for the `Timer`. /// - displayUnit: A hint to the backend responsible for presenting the data of the preferred display unit. This is not guaranteed to be supported by all backends. convenience init(label: String, dimensions: [(String, String)] = [], preferredDisplayUnit displayUnit: TimeUnit) { let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions) handler.preferDisplayUnit(displayUnit) self.init(label: label, dimensions: dimensions, handler: handler) } /// Signal the underlying metrics library that this timer will never be updated again. /// In response the library MAY decide to eagerly release any resources held by this `Timer`. @inlinable func destroy() { MetricsSystem.factory.destroyTimer(self.handler) } } /// A timer collects observations within a time window (usually things like request durations) and provides aggregated information about the data sample, /// for example, min, max and various quantiles. It is similar to a `Recorder` but specialized for values that represent durations. /// /// This is the user-facing Timer API. /// /// Its behavior depends on the `TimerHandler` implementation. public final class Timer { @usableFromInline let handler: TimerHandler public let label: String public let dimensions: [(String, String)] /// Alternative way to create a new `Timer`, while providing an explicit `TimerHandler`. /// /// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one. /// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason. /// /// - SeeAlso: Use `init(label:dimensions:)` to create `Recorder` instances using the configured metrics backend. /// /// - parameters: /// - label: The label for the `Timer`. /// - dimensions: The dimensions for the `Timer`. /// - handler: The custom backend. public init(label: String, dimensions: [(String, String)], handler: TimerHandler) { self.label = label self.dimensions = dimensions self.handler = handler } /// Record a duration in nanoseconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordNanoseconds(_ duration: Int64) { self.handler.recordNanoseconds(duration) } /// Record a duration in nanoseconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordNanoseconds(_ duration: DataType) { self.recordNanoseconds(duration >= Int64.max ? Int64.max : Int64(duration)) } /// Record a duration in microseconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordMicroseconds(_ duration: DataType) { guard duration <= Int64.max else { return self.recordNanoseconds(Int64.max) } let result = Int64(duration).multipliedReportingOverflow(by: 1000) if result.overflow { self.recordNanoseconds(Int64.max) } else { self.recordNanoseconds(result.partialValue) } } /// Record a duration in microseconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordMicroseconds(_ duration: DataType) { self.recordNanoseconds(Double(duration * 1000) < Double(Int64.max) ? Int64(duration * 1000) : Int64.max) } /// Record a duration in milliseconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordMilliseconds(_ duration: DataType) { guard duration <= Int64.max else { return self.recordNanoseconds(Int64.max) } let result = Int64(duration).multipliedReportingOverflow(by: 1_000_000) if result.overflow { self.recordNanoseconds(Int64.max) } else { self.recordNanoseconds(result.partialValue) } } /// Record a duration in milliseconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordMilliseconds(_ duration: DataType) { self.recordNanoseconds(Double(duration * 1_000_000) < Double(Int64.max) ? Int64(duration * 1_000_000) : Int64.max) } /// Record a duration in seconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordSeconds(_ duration: DataType) { guard duration <= Int64.max else { return self.recordNanoseconds(Int64.max) } let result = Int64(duration).multipliedReportingOverflow(by: 1_000_000_000) if result.overflow { self.recordNanoseconds(Int64.max) } else { self.recordNanoseconds(result.partialValue) } } /// Record a duration in seconds. /// /// - parameters: /// - value: Duration to record. @inlinable public func recordSeconds(_ duration: DataType) { self.recordNanoseconds(Double(duration * 1_000_000_000) < Double(Int64.max) ? Int64(duration * 1_000_000_000) : Int64.max) } } extension Timer: CustomStringConvertible { public var description: String { return "Timer(\(self.label), dimensions: \(self.dimensions))" } } // MARK: - MetricsSystem /// The `MetricsSystem` is a global facility where the default metrics backend implementation (`MetricsFactory`) can be /// configured. `MetricsSystem` is set up just once in a given program to set up the desired metrics backend /// implementation. public enum MetricsSystem { private static let _factory = FactoryBox(NOOPMetricsHandler.instance) /// `bootstrap` is an one-time configuration function which globally selects the desired metrics backend /// implementation. `bootstrap` can be called at maximum once in any given program, calling it more than once will /// lead to undefined behavior, most likely a crash. /// /// - parameters: /// - factory: A factory that given an identifier produces instances of metrics handlers such as `CounterHandler`, `RecorderHandler` and `TimerHandler`. public static func bootstrap(_ factory: MetricsFactory) { self._factory.replaceFactory(factory, validate: true) } // for our testing we want to allow multiple bootstrapping internal static func bootstrapInternal(_ factory: MetricsFactory) { self._factory.replaceFactory(factory, validate: false) } /// Returns a reference to the configured factory. public static var factory: MetricsFactory { return self._factory.underlying } /// Acquire a writer lock for the duration of the given block. /// /// - Parameter body: The block to execute while holding the lock. /// - Returns: The value returned by the block. public static func withWriterLock(_ body: () throws -> T) rethrows -> T { return try self._factory.withWriterLock(body) } private final class FactoryBox { private let lock = ReadWriteLock() fileprivate var _underlying: MetricsFactory private var initialized = false init(_ underlying: MetricsFactory) { self._underlying = underlying } func replaceFactory(_ factory: MetricsFactory, validate: Bool) { self.lock.withWriterLock { precondition(!validate || !self.initialized, "metrics system can only be initialized once per process. currently used factory: \(self._underlying)") self._underlying = factory self.initialized = true } } var underlying: MetricsFactory { return self.lock.withReaderLock { return self._underlying } } func withWriterLock(_ body: () throws -> T) rethrows -> T { return try self.lock.withWriterLock(body) } } } // MARK: - Library SPI, intended to be implemented by backend libraries // MARK: - MetricsFactory /// The `MetricsFactory` is the bridge between the `MetricsSystem` and the metrics backend implementation. /// `MetricsFactory`'s role is to initialize concrete implementations of the various metric types: /// * `Counter` -> `CounterHandler` /// * `FloatingPointCounter` -> `FloatingPointCounterHandler` /// * `Recorder` -> `RecorderHandler` /// * `Timer` -> `TimerHandler` /// /// - warning: This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. /// To use the SwiftMetrics API, please refer to the documentation of `MetricsSystem`. /// /// # Destroying metrics /// /// Since _some_ metrics implementations may need to allocate (potentially "heavy") resources for metrics, destroying /// metrics offers a signal to libraries when a metric is "known to never be updated again." /// /// While many metrics are bound to the entire lifetime of an application and thus never need to be destroyed eagerly, /// some metrics have well defined unique life-cycles, and it may be beneficial to release any resources held by them /// more eagerly than awaiting the application's termination. In such cases, a library or application should invoke /// a metric's appropriate `destroy()` method, which in turn results in the corresponding handler that it is backed by /// to be passed to `destroyCounter(handler:)`, `destroyRecorder(handler:)` or `destroyTimer(handler:)` where the factory /// can decide to free any corresponding resources. /// /// While some libraries may not need to implement this destroying as they may be stateless or similar, /// libraries using the metrics API should always assume a library WILL make use of this signal, and shall not /// neglect calling these methods when appropriate. public protocol MetricsFactory { /// Create a backing `CounterHandler`. /// /// - parameters: /// - label: The label for the `CounterHandler`. /// - dimensions: The dimensions for the `CounterHandler`. func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler /// Create a backing `FloatingPointCounterHandler`. /// /// - parameters: /// - label: The label for the `FloatingPointCounterHandler`. /// - dimensions: The dimensions for the `FloatingPointCounterHandler`. func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler /// Create a backing `RecorderHandler`. /// /// - parameters: /// - label: The label for the `RecorderHandler`. /// - dimensions: The dimensions for the `RecorderHandler`. /// - aggregate: Is data aggregation expected. func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler /// Create a backing `TimerHandler`. /// /// - parameters: /// - label: The label for the `TimerHandler`. /// - dimensions: The dimensions for the `TimerHandler`. func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler /// Invoked when the corresponding `Counter`'s `destroy()` function is invoked. /// Upon receiving this signal the factory may eagerly release any resources related to this counter. /// /// - parameters: /// - handler: The handler to be destroyed. func destroyCounter(_ handler: CounterHandler) /// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked. /// Upon receiving this signal the factory may eagerly release any resources related to this counter. /// /// - parameters: /// - handler: The handler to be destroyed. func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) /// Invoked when the corresponding `Recorder`'s `destroy()` function is invoked. /// Upon receiving this signal the factory may eagerly release any resources related to this recorder. /// /// - parameters: /// - handler: The handler to be destroyed. func destroyRecorder(_ handler: RecorderHandler) /// Invoked when the corresponding `Timer`'s `destroy()` function is invoked. /// Upon receiving this signal the factory may eagerly release any resources related to this timer. /// /// - parameters: /// - handler: The handler to be destroyed. func destroyTimer(_ handler: TimerHandler) } /// Wraps a CounterHandler, adding support for incrementing by floating point values by storing an accumulated floating point value and recording increments to the underlying CounterHandler after crossing integer boundaries. internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCounterHandler { private let lock = Lock() private let counterHandler: CounterHandler internal var fraction: Double = 0 init(label: String, dimensions: [(String, String)]) { self.counterHandler = MetricsSystem .factory.makeCounter(label: label, dimensions: dimensions) } func increment(by amount: Double) { // Drop illegal values // - cannot increment by NaN guard !amount.isNaN else { return } // - cannot increment by infinite quantities guard !amount.isInfinite else { return } // - cannot increment by negative values guard amount.sign == .plus else { return } // - cannot increment by zero guard !amount.isZero else { return } if amount.exponent >= 63 { // If amount is in Int64.max..<+Inf, ceil to Int64.max self.lock.withLockVoid { self.counterHandler.increment(by: .max) } } else { // Split amount into integer and fraction components var (increment, fraction) = self.integerAndFractionComponents(of: amount) self.lock.withLockVoid { // Add the fractional component to the accumulated fraction. self.fraction += fraction // self.fraction may have cross an integer boundary, Split it // and add any integer component. let (integer, fraction) = integerAndFractionComponents(of: self.fraction) increment += integer self.fraction = fraction // Increment the handler by the total integer component. if increment > 0 { self.counterHandler.increment(by: increment) } } } } @inline(__always) private func integerAndFractionComponents(of value: Double) -> (Int64, Double) { let integer = Int64(value) let fraction = value - value.rounded(.towardZero) return (integer, fraction) } func reset() { self.lock.withLockVoid { self.fraction = 0 self.counterHandler.reset() } } func destroy() { MetricsSystem.factory.destroyCounter(self.counterHandler) } } extension MetricsFactory { /// Create a default backing `FloatingPointCounterHandler` for backends which do not naively support floating point counters. /// /// The created FloatingPointCounterHandler is a wrapper around a backend's CounterHandler which accumulates floating point values and records increments to an underlying CounterHandler after crossing integer boundaries. /// /// - parameters: /// - label: The label for the `FloatingPointCounterHandler`. /// - dimensions: The dimensions for the `FloatingPointCounterHandler`. public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { return AccumulatingRoundingFloatingPointCounter(label: label, dimensions: dimensions) } /// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked. /// Upon receiving this signal the factory may eagerly release any resources related to this counter. /// /// `destroyFloatingPointCounter` must be implemented if `makeFloatingPointCounter` is implemented. /// /// - parameters: /// - handler: The handler to be destroyed. public func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) { (handler as? AccumulatingRoundingFloatingPointCounter)?.destroy() } } // MARK: - Backend Handlers /// A `CounterHandler` represents a backend implementation of a `Counter`. /// /// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. /// To use the SwiftMetrics API, please refer to the documentation of `Counter`. /// /// # Implementation requirements /// /// To implement your own `CounterHandler` you should respect a few requirements that are necessary so applications work /// as expected regardless of the selected `CounterHandler` implementation. /// /// - The `CounterHandler` must be a `class`. public protocol CounterHandler: AnyObject, _SwiftMetricsSendableProtocol { /// Increment the counter. /// /// - parameters: /// - by: Amount to increment by. func increment(by: Int64) /// Reset the counter back to zero. func reset() } /// A `FloatingPointCounterHandler` represents a backend implementation of a `FloatingPointCounter`. /// /// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. /// To use the SwiftMetrics API, please refer to the documentation of `FloatingPointCounter`. /// /// # Implementation requirements /// /// To implement your own `FloatingPointCounterHandler` you should respect a few requirements that are necessary so applications work /// as expected regardless of the selected `FloatingPointCounterHandler` implementation. /// /// - The `FloatingPointCounterHandler` must be a `class`. public protocol FloatingPointCounterHandler: AnyObject, _SwiftMetricsSendableProtocol { /// Increment the counter. /// /// - parameters: /// - by: Amount to increment by. func increment(by: Double) /// Reset the counter back to zero. func reset() } /// A `RecorderHandler` represents a backend implementation of a `Recorder`. /// /// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. /// To use the SwiftMetrics API, please refer to the documentation of `Recorder`. /// /// # Implementation requirements /// /// To implement your own `RecorderHandler` you should respect a few requirements that are necessary so applications work /// as expected regardless of the selected `RecorderHandler` implementation. /// /// - The `RecorderHandler` must be a `class`. public protocol RecorderHandler: AnyObject, _SwiftMetricsSendableProtocol { /// Record a value. /// /// - parameters: /// - value: Value to record. func record(_ value: Int64) /// Record a value. /// /// - parameters: /// - value: Value to record. func record(_ value: Double) } /// A `TimerHandler` represents a backend implementation of a `Timer`. /// /// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. /// To use the SwiftMetrics API, please refer to the documentation of `Timer`. /// /// # Implementation requirements /// /// To implement your own `TimerHandler` you should respect a few requirements that are necessary so applications work /// as expected regardless of the selected `TimerHandler` implementation. /// /// - The `TimerHandler` must be a `class`. public protocol TimerHandler: AnyObject, _SwiftMetricsSendableProtocol { /// Record a duration in nanoseconds. /// /// - parameters: /// - value: Duration to record. func recordNanoseconds(_ duration: Int64) /// Set the preferred display unit for this TimerHandler. /// /// - parameters: /// - unit: A hint to the backend responsible for presenting the data of the preferred display unit. This is not guaranteed to be supported by all backends. func preferDisplayUnit(_ unit: TimeUnit) } extension TimerHandler { public func preferDisplayUnit(_: TimeUnit) { // NOOP } } // MARK: - Predefined Metrics Handlers /// A pseudo-metrics handler that can be used to send messages to multiple other metrics handlers. public final class MultiplexMetricsHandler: MetricsFactory { private let factories: [MetricsFactory] public init(factories: [MetricsFactory]) { self.factories = factories } public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { return MuxCounter(factories: self.factories, label: label, dimensions: dimensions) } public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { return MuxFloatingPointCounter(factories: self.factories, label: label, dimensions: dimensions) } public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { return MuxRecorder(factories: self.factories, label: label, dimensions: dimensions, aggregate: aggregate) } public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { return MuxTimer(factories: self.factories, label: label, dimensions: dimensions) } public func destroyCounter(_ handler: CounterHandler) { for factory in self.factories { factory.destroyCounter(handler) } } public func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) { for factory in self.factories { factory.destroyFloatingPointCounter(handler) } } public func destroyRecorder(_ handler: RecorderHandler) { for factory in self.factories { factory.destroyRecorder(handler) } } public func destroyTimer(_ handler: TimerHandler) { for factory in self.factories { factory.destroyTimer(handler) } } private final 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(by amount: Int64) { self.counters.forEach { $0.increment(by: amount) } } func reset() { self.counters.forEach { $0.reset() } } } private final class MuxFloatingPointCounter: FloatingPointCounterHandler { let counters: [FloatingPointCounterHandler] public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)]) { self.counters = factories.map { $0.makeFloatingPointCounter(label: label, dimensions: dimensions) } } func increment(by amount: Double) { self.counters.forEach { $0.increment(by: amount) } } func reset() { self.counters.forEach { $0.reset() } } } private final 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: Int64) { self.recorders.forEach { $0.record(value) } } func record(_ value: Double) { self.recorders.forEach { $0.record(value) } } } private final 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) { self.timers.forEach { $0.recordNanoseconds(duration) } } } } /// Ships with the metrics module, used for initial bootstrapping. public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, FloatingPointCounterHandler, RecorderHandler, TimerHandler { public static let instance = NOOPMetricsHandler() private init() {} public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { return self } public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { return self } public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { return self } public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { return self } public func destroyCounter(_: CounterHandler) {} public func destroyFloatingPointCounter(_: FloatingPointCounterHandler) {} public func destroyRecorder(_: RecorderHandler) {} public func destroyTimer(_: TimerHandler) {} public func increment(by: Int64) {} public func increment(by: Double) {} public func reset() {} public func record(_: Int64) {} public func record(_: Double) {} public func recordNanoseconds(_: Int64) {} } // MARK: - Sendable support helpers #if compiler(>=5.6) extension MetricsSystem: Sendable {} extension Counter: Sendable {} extension FloatingPointCounter: Sendable {} // must be @unchecked since Gauge inherits Recorder :( extension Recorder: @unchecked Sendable {} extension Timer: Sendable {} // ideally we would not be using @unchecked here, but concurrency-safety checks do not recognize locks extension AccumulatingRoundingFloatingPointCounter: @unchecked Sendable {} #endif #if compiler(>=5.6) @preconcurrency public protocol _SwiftMetricsSendableProtocol: Sendable {} #else public protocol _SwiftMetricsSendableProtocol {} #endif