Compare commits

...

11 Commits
2.5.1 ... main

Author SHA1 Message Date
Honza Dvorsky 4c83e1cdf4
Remove warnings against explicit dependency injection of the metrics factory (#174)
### Motivation:

In the previous PR, we noticed that these warnings seem to discourage
explicit dependency injection, but it's not clear why we should be
discouraging it.

### Modifications:

Removed the warning.

### Result:

N/A
2025-04-29 15:27:10 +02:00
Honza Dvorsky 98d36172c1
Allow providing a custom MetricsFactory to Counter and friends (#172)
### Motivation:

To allow for things like parallel testing, it'd be useful if we can
explicitly provide a MetricsFactory to the existing Counter/... types.

### Modifications:

Added a `factory: MetricsFactory` parameter to all the initializers of
Counter/... types, and kept the existing methods that continue to
default to `MetricsSystem.factory`.

### Result:

Adopters can use a custom MetricsFactory explicitly passed in at metric
creation time. Existing adopters are not affected, unless you opt in,
you continue to use the global factory.
2025-04-29 15:07:30 +02:00
Rick Newton-Rogers 27ecca7ac1
Enable Swift 6.1 jobs in CI (#171)
Motivation:

Swift 6.1 has been released, we should add it to our CI coverage.

Modifications:

Add additional Swift 6.1 jobs where appropriate in main.yml,
pull_request.yml

Result:

Improved test coverage.
2025-04-14 12:15:45 +02:00
Rick Newton-Rogers 071d1cac3c
Enable macOS CI on pull requests (#170)
Motivation:

* Improve test coverage

Modifications:

Enable macOS CI to be run on pull request commits and make the use of
the nightly runner pool for main.yml jobs explicit.

Result:

Improved test coverage.
2025-04-01 12:20:11 +02:00
Rick Newton-Rogers 0556b16079
Enable macOS CI on merge to main and daily timer (#169)
Motivation:

* Improve test coverage
* Check test pass/fail status
* Monitor CI throughput

Modifications:

Enable macOS CI to be run on all merges to main and on a daily timer.

Result:

Improved test coverage run out-of-band at the moment so we can get a
feeling for if any changes need to be made in the repo or in the CI
pipelines to ensure timely and stable checks.
2025-03-31 13:29:44 +01:00
Franz Busch 44491db7cc
Fix 5.10 compile on Ubuntu 24.04 (#168)
Specifically Swift 5.10 on Intel on Ubuntu Noble (24.04) has a crazy bug
which leads to compilation failures in a #if compiler(>=6.0) block:
swiftlang/swift#79285 .

This workaround fixes the compilation by changing the whitespace.

Fixes https://github.com/apple/swift-metrics/issues/166
2025-03-18 16:21:52 +01:00
Franz Busch 3c0f419970
Add `Timer.measure` methods (#140)
# Motivation

This PR supersedes https://github.com/apple/swift-metrics/pull/135. The
goal is to make it easier to measure asynchronous code when using
`Metrics`.

# Modification

This PR does:
- Deprecate the current static method for measuring synchronous code
- Add a new instance method to measure synchronous code
- Add a new instance method to measure asynchronous code
2025-03-10 10:46:27 +01:00
Rick Newton-Rogers cbd39ceaca
Only apply standard swift settings on valid targets (#163)
Only apply standard swift settings on valid targets. The current check
ignores plugins but that is not comprehensive enough.
2025-03-07 19:34:49 +01:00
Rick Newton-Rogers 029e902273
Rename nightly_6_1 params to nightly_next (#162)
Rename nightly_6_1 params to nightly_next; see
https://github.com/apple/swift-nio/pull/3122
2025-03-03 14:46:47 +00:00
Cory Benfield 58f390a873
Tell the truth about the supported metric types (#161)
Our README claims we support 4 metric types, and then lists five. But it
fails to list FloatingPointCounter, which we also support, so the real
number is six.

While I was here I also fixed the DocC topic, which omitted
FloatingPointCounter from the list of metrics but _also_ omitted Gauge
for some weird reason.
2025-01-31 14:31:14 +00:00
Rick Newton-Rogers 53de3bfa9a
CI use 6.1 nightlies (#160)
CI use 6.1 nightlies now that Swift development is happening in the 6.1
branch
2025-01-30 09:59:23 +01:00
9 changed files with 452 additions and 60 deletions

View File

@ -14,5 +14,13 @@ jobs:
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
macos-tests:
name: macOS tests
uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
with:
runner_pool: nightly
build_scheme: swift-metrics-Package

View File

@ -18,9 +18,17 @@ jobs:
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
cxx-interop:
name: Cxx interop
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
macos-tests:
name: macOS tests
uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
with:
runner_pool: general
build_scheme: swift-metrics-Package

View File

@ -49,11 +49,16 @@ for target in package.targets {
// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
for target in package.targets {
if target.type != .plugin {
switch target.type {
case .regular, .test, .executable:
var settings = target.swiftSettings ?? []
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
target.swiftSettings = settings
case .macro, .plugin, .system, .binary:
() // not applicable
@unknown default:
() // we don't know what to do here, do nothing
}
}
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //

View File

@ -90,12 +90,18 @@ This API was designed with the contributors to the Swift on Server community and
### Metric types
The API supports four metric types:
The API supports six metric types:
`Counter`: A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart. For example, you can use a counter to represent the number of requests served, tasks completed, or errors.
```swift
counter.increment(by: 100)
```
- `FloatingPointCounter`: A variation of a `Counter` that records a floating point value, instead of an integer.
```swift
floatingPointCounter.increment(by: 10.5)
```
`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 a `Recorder` with a sample size of 1 that does not perform any aggregation.

View File

@ -91,7 +91,9 @@ This API was designed with the contributors to the Swift on Server community and
### Metric types
- ``Counter``
- ``FloatingPointCounter``
- ``Meter``
- ``Recorder``
- ``Gauge``
- ``Timer``

View File

@ -23,26 +23,45 @@
///
/// Its behavior depends on the `CounterHandler` implementation.
public final class Counter {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API.
/// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: CounterHandler
@usableFromInline
package let _factory: MetricsFactory
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) {
/// - factory: The custom factory.
public init(label: String, dimensions: [(String, String)], handler: CounterHandler, factory: MetricsFactory) {
self.label = label
self.dimensions = dimensions
self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Counter`, while providing an explicit `CounterHandler`.
///
/// - 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, created by the global metrics factory.
public convenience init(label: String, dimensions: [(String, String)], handler: CounterHandler) {
self.init(
label: label,
dimensions: dimensions,
handler: handler,
factory: MetricsSystem.factory
)
}
/// Increment the counter.
@ -74,15 +93,25 @@ extension Counter {
/// - 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)
self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
}
/// Create a new `Counter`.
///
/// - parameters:
/// - label: The label for the `Counter`.
/// - dimensions: The dimensions for the `Counter`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeCounter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// 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)
self._factory.destroyCounter(self._handler)
}
}
@ -102,26 +131,50 @@ extension Counter: CustomStringConvertible {
///
/// Its behavior depends on the `FloatingCounterHandler` implementation.
public final class FloatingPointCounter {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API.
/// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: FloatingPointCounterHandler
@usableFromInline
package let _factory: MetricsFactory
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) {
/// - factory: The custom factory.
public init(
label: String,
dimensions: [(String, String)],
handler: FloatingPointCounterHandler,
factory: MetricsFactory
) {
self.label = label
self.dimensions = dimensions
self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `FloatingPointCounter`, while providing an explicit `FloatingPointCounterHandler`.
///
/// - 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 convenience init(label: String, dimensions: [(String, String)], handler: FloatingPointCounterHandler) {
self.init(
label: label,
dimensions: dimensions,
handler: handler,
factory: MetricsSystem.factory
)
}
/// Increment the FloatingPointCounter.
@ -153,15 +206,25 @@ extension FloatingPointCounter {
/// - 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)
self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
}
/// Create a new `FloatingPointCounter`.
///
/// - parameters:
/// - label: The label for the `FloatingPointCounter`.
/// - dimensions: The dimensions for the `FloatingPointCounter`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeFloatingPointCounter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// 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)
self._factory.destroyFloatingPointCounter(self._handler)
}
}
@ -185,6 +248,16 @@ public final class Gauge: Recorder, @unchecked Sendable {
public convenience init(label: String, dimensions: [(String, String)] = []) {
self.init(label: label, dimensions: dimensions, aggregate: false)
}
/// Create a new `Gauge`.
///
/// - parameters:
/// - label: The label for the `Gauge`.
/// - dimensions: The dimensions for the `Gauge`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
self.init(label: label, dimensions: dimensions, aggregate: false, factory: factory)
}
}
// MARK: - Meter
@ -192,26 +265,40 @@ public final class Gauge: Recorder, @unchecked Sendable {
/// A meter is similar to a gauge, it is a metric that represents a single numerical value that can arbitrarily go up and down.
/// Meters 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.
public final class Meter {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API.
/// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: MeterHandler
@usableFromInline
package let _factory: MetricsFactory
public let label: String
public let dimensions: [(String, String)]
/// Alternative way to create a new `Meter`, while providing an explicit `MeterHandler`.
///
/// - 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 `Meter` 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)], handler: MeterHandler) {
/// - factory: The custom factory.
public init(label: String, dimensions: [(String, String)], handler: MeterHandler, factory: MetricsFactory) {
self.label = label
self.dimensions = dimensions
self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Meter`, while providing an explicit `MeterHandler`.
///
/// - SeeAlso: Use `init(label:dimensions:)` to create `Meter` instances using the configured metrics backend.
///
/// - parameters:
/// - label: The label for the `Recorder`.
/// - dimensions: The dimensions for the `Recorder`.
/// - handler: The custom backend.
public convenience init(label: String, dimensions: [(String, String)], handler: MeterHandler) {
self.init(label: label, dimensions: dimensions, handler: handler, factory: MetricsSystem.factory)
}
/// Set a value.
@ -264,21 +351,31 @@ public final class Meter {
}
extension Meter {
/// Create a new `Meter`.
///
/// - parameters:
/// - label: The label for the `Meter`.
/// - dimensions: The dimensions for the `Meter`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeMeter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// Create a new `Meter`.
///
/// - parameters:
/// - label: The label for the `Meter`.
/// - dimensions: The dimensions for the `Meter`.
public convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeMeter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler)
self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
}
/// 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.destroyMeter(self._handler)
self._factory.destroyMeter(self._handler)
}
}
@ -296,17 +393,17 @@ extension Meter: CustomStringConvertible {
///
/// Its behavior depends on the `RecorderHandler` implementation.
public class Recorder {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API.
/// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: RecorderHandler
@usableFromInline
package let _factory: MetricsFactory
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:
@ -314,11 +411,38 @@ public class Recorder {
/// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size
/// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) {
/// - factory: The custom factory.
public init(
label: String,
dimensions: [(String, String)],
aggregate: Bool,
handler: RecorderHandler,
factory: MetricsFactory
) {
self.label = label
self.dimensions = dimensions
self.aggregate = aggregate
self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Recorder`, while providing an explicit `RecorderHandler`.
///
/// - 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`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size
/// - handler: The custom backend.
public convenience init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) {
self.init(
label: label,
dimensions: dimensions,
aggregate: aggregate,
handler: handler,
factory: MetricsSystem.factory
)
}
/// Record a value.
@ -354,15 +478,31 @@ extension Recorder {
/// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size
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)
self.init(label: label, dimensions: dimensions, aggregate: aggregate, factory: MetricsSystem.factory)
}
/// Create a new `Recorder`.
///
/// - parameters:
/// - label: The label for the `Recorder`.
/// - dimensions: The dimensions for the `Recorder`.
/// - aggregate: aggregate recorded values to produce statistics across a sample size.
/// - factory: The custom factory.
public convenience init(
label: String,
dimensions: [(String, String)] = [],
aggregate: Bool = true,
factory: MetricsFactory
) {
let handler = factory.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
self.init(label: label, dimensions: dimensions, aggregate: aggregate, handler: handler, factory: factory)
}
/// 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)
self._factory.destroyRecorder(self._handler)
}
}
@ -420,26 +560,40 @@ public struct TimeUnit: Equatable, Sendable {
///
/// Its behavior depends on the `TimerHandler` implementation.
public final class Timer {
/// ``_handler`` is only public to allow access from `MetricsTestKit`. Do not consider it part of the public API.
/// `_handler` and `_factory` are only public to allow access from `MetricsTestKit`.
/// Do not consider them part of the public API.
public let _handler: TimerHandler
@usableFromInline
package let _factory: MetricsFactory
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) {
/// - factory: The custom factory.
public init(label: String, dimensions: [(String, String)], handler: TimerHandler, factory: MetricsFactory) {
self.label = label
self.dimensions = dimensions
self._handler = handler
self._factory = factory
}
/// Alternative way to create a new `Timer`, while providing an explicit `TimerHandler`.
///
/// - 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 convenience init(label: String, dimensions: [(String, String)], handler: TimerHandler) {
self.init(label: label, dimensions: dimensions, handler: handler, factory: MetricsSystem.factory)
}
/// Record a duration in nanoseconds.
@ -541,14 +695,42 @@ public final class Timer {
}
extension Timer {
/// Create a new `Timer`.
///
/// - parameters:
/// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `Timer`.
/// - factory: The custom factory.
public convenience init(label: String, dimensions: [(String, String)] = [], factory: MetricsFactory) {
let handler = factory.makeTimer(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// Create a new `Timer`.
///
/// - parameters:
/// - label: The label for the `Timer`.
/// - dimensions: The dimensions for the `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)
self.init(label: label, dimensions: dimensions, factory: MetricsSystem.factory)
}
/// 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.
/// - factory: The custom factory.
public convenience init(
label: String,
dimensions: [(String, String)] = [],
preferredDisplayUnit displayUnit: TimeUnit,
factory: MetricsFactory
) {
let handler = factory.makeTimer(label: label, dimensions: dimensions)
handler.preferDisplayUnit(displayUnit)
self.init(label: label, dimensions: dimensions, handler: handler, factory: factory)
}
/// Create a new `Timer`.
@ -562,16 +744,19 @@ extension Timer {
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)
self.init(
label: label,
dimensions: dimensions,
preferredDisplayUnit: displayUnit,
factory: MetricsSystem.factory
)
}
/// 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
public func destroy() {
MetricsSystem.factory.destroyTimer(self._handler)
self._factory.destroyTimer(self._handler)
}
}
@ -661,8 +846,7 @@ public enum MetricsSystem {
/// * `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`.
/// To use the SwiftMetrics API, please refer to the documentation of `MetricsSystem`.
///
/// # Destroying metrics
///
@ -756,11 +940,12 @@ public protocol MetricsFactory: _SwiftMetricsSendableProtocol {
internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCounterHandler {
private let lock = Lock()
private let counterHandler: CounterHandler
private let factory: MetricsFactory
internal var fraction: Double = 0
init(label: String, dimensions: [(String, String)]) {
self.counterHandler = MetricsSystem
.factory.makeCounter(label: label, dimensions: dimensions)
init(label: String, dimensions: [(String, String)], factory: MetricsFactory) {
self.counterHandler = factory.makeCounter(label: label, dimensions: dimensions)
self.factory = factory
}
func increment(by amount: Double) {
@ -813,7 +998,7 @@ internal final class AccumulatingRoundingFloatingPointCounter: FloatingPointCoun
}
func destroy() {
MetricsSystem.factory.destroyCounter(self.counterHandler)
self.factory.destroyCounter(self.counterHandler)
}
}
@ -824,10 +1009,11 @@ internal final class AccumulatingMeter: MeterHandler, @unchecked Sendable {
// FIXME: use swift-atomics when floating point support is available
private var value: Double = 0
private let lock = Lock()
private let factory: MetricsFactory
init(label: String, dimensions: [(String, String)]) {
self.recorderHandler = MetricsSystem
.factory.makeRecorder(label: label, dimensions: dimensions, aggregate: true)
init(label: String, dimensions: [(String, String)], factory: MetricsFactory) {
self.recorderHandler = factory.makeRecorder(label: label, dimensions: dimensions, aggregate: true)
self.factory = factory
}
func set(_ value: Int64) {
@ -898,7 +1084,7 @@ internal final class AccumulatingMeter: MeterHandler, @unchecked Sendable {
}
func destroy() {
MetricsSystem.factory.destroyRecorder(self.recorderHandler)
self.factory.destroyRecorder(self.recorderHandler)
}
}
@ -911,7 +1097,7 @@ extension MetricsFactory {
/// - label: The label for the `FloatingPointCounterHandler`.
/// - dimensions: The dimensions for the `FloatingPointCounterHandler`.
public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler {
AccumulatingRoundingFloatingPointCounter(label: label, dimensions: dimensions)
AccumulatingRoundingFloatingPointCounter(label: label, dimensions: dimensions, factory: self)
}
/// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked.
@ -935,7 +1121,7 @@ extension MetricsFactory {
/// - label: The label for the `MeterHandler`.
/// - dimensions: The dimensions for the `MeterHandler`.
public func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
AccumulatingMeter(label: label, dimensions: dimensions)
AccumulatingMeter(label: label, dimensions: dimensions, factory: self)
}
/// Invoked when the corresponding `Meter`'s `destroy()` function is invoked.

View File

@ -11,6 +11,9 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
// swift-format-ignore-file
// Note: Whitespace changes are used to workaround compiler bug
// https://github.com/swiftlang/swift/issues/79285
@_exported import CoreMetrics
import Foundation
@ -112,4 +115,46 @@ extension Timer {
self.recordNanoseconds(nanoseconds.partialValue)
}
#if compiler(>=6.0)
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
// DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED
// https://github.com/swiftlang/swift/issues/79285
body: () throws(Failure) -> Result) throws(Failure) -> Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try body()
}
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - isolation: The isolation of the method. Defaults to the isolation of the caller.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
isolation: isolated (any Actor)? = #isolation,
// DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED
// https://github.com/swiftlang/swift/issues/79285
body: () async throws(Failure) -> sending Result) async throws(Failure) -> sending Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try await body()
}
#endif
}

View File

@ -597,7 +597,7 @@ class MetricsTests: XCTestCase {
}
}
func testCustomFactory() {
func testCustomHandler() {
final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
@ -609,6 +609,79 @@ class MetricsTests: XCTestCase {
XCTAssertTrue(counter2._handler is CustomHandler, "expected custom log handler")
}
func testCustomFactory() {
// @unchecked Sendable is okay here - locking is done manually.
final class CustomFactory: MetricsFactory, @unchecked Sendable {
init(handler: CustomHandler) {
self.handler = handler
}
final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
}
private let handler: CustomHandler
private let lock: NSLock = NSLock()
private var locked_didCallDestroyCounter: Bool = false
var didCallDestroyCounter: Bool {
self.lock.lock()
defer {
lock.unlock()
}
return self.locked_didCallDestroyCounter
}
func makeCounter(label: String, dimensions: [(String, String)]) -> any CoreMetrics.CounterHandler {
handler
}
func makeRecorder(
label: String,
dimensions: [(String, String)],
aggregate: Bool
) -> any CoreMetrics.RecorderHandler {
fatalError("Unsupported")
}
func makeTimer(label: String, dimensions: [(String, String)]) -> any CoreMetrics.TimerHandler {
fatalError("Unsupported")
}
func destroyCounter(_ handler: any CoreMetrics.CounterHandler) {
XCTAssertTrue(
handler === self.handler,
"The handler to be destroyed doesn't match the expected handler."
)
self.lock.lock()
defer {
lock.unlock()
}
self.locked_didCallDestroyCounter = true
}
func destroyRecorder(_ handler: any CoreMetrics.RecorderHandler) {
fatalError("Unsupported")
}
func destroyTimer(_ handler: any CoreMetrics.TimerHandler) {
fatalError("Unsupported")
}
}
let handler = CustomFactory.CustomHandler()
let factory = CustomFactory(handler: handler)
XCTAssertFalse(factory.didCallDestroyCounter)
do {
let counter1 = Counter(label: "foo", factory: factory)
XCTAssertTrue(counter1._handler is CustomFactory.CustomHandler, "expected a custom metrics handler")
XCTAssertTrue(counter1._factory is CustomFactory, "expected a custom metrics factory")
counter1.destroy()
}
XCTAssertTrue(factory.didCallDestroyCounter)
}
func testDestroyingGauge() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)

View File

@ -220,6 +220,43 @@ class MetricsExtensionsTests: XCTestCase {
"expected value to match"
)
}
#if compiler(>=6.0)
func testTimerMeasure() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}
let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
@MainActor
func testTimerMeasureFromMainActor() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}
let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
#endif
}
// https://bugs.swift.org/browse/SR-6310
@ -251,3 +288,25 @@ extension DispatchTimeInterval {
}
}
}
#if swift(>=5.7)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
extension Swift.Duration {
fileprivate var nanosecondsClamped: Int64 {
let components = self.components
let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
let attosCompononentNanos = components.attoseconds / 1_000_000_000
let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos)
guard
!secondsComponentNanos.overflow,
!combinedNanos.overflow
else {
return .max
}
return combinedNanos.partialValue
}
}
#endif