Skip to content

Commit 32eef8a

Browse files
authored
add "meter" - a new type of metric and metric handler (#123)
motivation: seperate gauge from recorder in a backwards compatible way changes: * add new meter and meter handler pair * add increment and decrement to meter handler * add default implementation based on recorder for backwards compatibility * add and adjust tests
1 parent 862b99b commit 32eef8a

File tree

13 files changed

+689
-160
lines changed

13 files changed

+689
-160
lines changed

.swiftformat

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010
--stripunusedargs unnamed-only
1111
--ifdef no-indent
1212

13+
# Configure the placement of an extension's access control keyword.
14+
--extensionacl on-declarations
15+
1316
# rules

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ let package = Package(
2828
],
2929
targets: [
3030
.target(
31-
name: "CoreMetrics",
32-
dependencies: []
31+
name: "CoreMetrics"
3332
),
3433
.target(
3534
name: "Metrics",

README.md

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,22 @@ The API supports four metric types:
9898
counter.increment(by: 100)
9999
```
100100

101-
`Recorder`: 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.
101+
`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.
102102

103103
```swift
104-
recorder.record(100)
104+
gauge.record(100)
105105
```
106106

107-
`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.
107+
`Meter`: A Meter is similar to `Gauge` - 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. Unlike `Gauge`, `Meter` also supports atomic incerements and decerements.
108108

109109
```swift
110-
gauge.record(100)
110+
meter.record(100)
111+
```
112+
113+
`Recorder`: 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.
114+
115+
```swift
116+
recorder.record(100)
111117
```
112118

113119
`Timer`: A timer collects observations within a time window (usually things like request duration) 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.
@@ -120,7 +126,7 @@ timer.recordMilliseconds(100)
120126

121127
Note: Unless you need to implement a custom metrics backend, everything in this section is likely not relevant, so please feel free to skip.
122128

123-
As seen above, each constructor for `Counter`, `Timer`, `Recorder` and `Gauge` provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. _Each application_ can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
129+
As seen above, each constructor for `Counter`, `Gauge`, `Meter`, `Recorder` and `Timer` provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. _Each application_ can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
124130

125131
```swift
126132
let metricsImplementation = MyFavoriteMetricsImplementation()
@@ -134,10 +140,12 @@ Given the above, an implementation of a metric backend needs to conform to `prot
134140
```swift
135141
public protocol MetricsFactory {
136142
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
137-
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
143+
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler
144+
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
138145
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
139146

140147
func destroyCounter(_ handler: CounterHandler)
148+
func destroyMeter(_ handler: MeterHandler)
141149
func destroyRecorder(_ handler: RecorderHandler)
142150
func destroyTimer(_ handler: TimerHandler)
143151
}
@@ -154,11 +162,14 @@ public protocol CounterHandler: AnyObject {
154162
}
155163
```
156164

157-
**Timer**
165+
**Meter**
158166

159167
```swift
160-
public protocol TimerHandler: AnyObject {
161-
func recordNanoseconds(_ duration: Int64)
168+
public protocol MeterHandler: AnyObject {
169+
func set(_ value: Int64)
170+
func set(_ value: Double)
171+
func increment(by: Double)
172+
func decrement(by: Double)
162173
}
163174
```
164175

@@ -171,9 +182,17 @@ public protocol RecorderHandler: AnyObject {
171182
}
172183
```
173184

185+
**Timer**
186+
187+
```swift
188+
public protocol TimerHandler: AnyObject {
189+
func recordNanoseconds(_ duration: Int64)
190+
}
191+
```
192+
174193
#### Dealing with Overflows
175194

176-
Implementaton of metric objects that deal with integers, like `Counter` and `Timer` should be careful with overflow. The expected behavior is to cap at `.max`, and never crash the program due to overflow . For example:
195+
Implementation of metric objects that deal with integers, like `Counter` and `Timer` should be careful with overflow. The expected behavior is to cap at `.max`, and never crash the program due to overflow . For example:
177196

178197
```swift
179198
class ExampleCounter: CounterHandler {
@@ -201,9 +220,12 @@ class SimpleMetricsLibrary: MetricsFactory {
201220
return ExampleCounter(label, dimensions)
202221
}
203222

223+
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
224+
return ExampleMeter(label, dimensions)
225+
}
226+
204227
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
205-
let maker: (String, [(String, String)]) -> RecorderHandler = aggregate ? ExampleRecorder.init : ExampleGauge.init
206-
return maker(label, dimensions)
228+
return ExampleRecorder(label, dimensions, aggregate)
207229
}
208230

209231
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
@@ -212,7 +234,8 @@ class SimpleMetricsLibrary: MetricsFactory {
212234

213235
// implementation is stateless, so nothing to do on destroy calls
214236
func destroyCounter(_ handler: CounterHandler) {}
215-
func destroyRecorder(_ handler: RecorderHandler) {}
237+
func destroyMeter(_ handler: TimerHandler) {}
238+
func destroyRecorder(_ handler: RecorderHandler) {}
216239
func destroyTimer(_ handler: TimerHandler) {}
217240

218241
private class ExampleCounter: CounterHandler {
@@ -233,9 +256,32 @@ class SimpleMetricsLibrary: MetricsFactory {
233256
}
234257
}
235258

236-
private class ExampleRecorder: RecorderHandler {
259+
private class ExampleMeter: MeterHandler {
237260
init(_: String, _: [(String, String)]) {}
238261

262+
let lock = NSLock()
263+
var _value: Double = 0
264+
265+
func set(_ value: Int64) {
266+
self.set(Double(value))
267+
}
268+
269+
func set(_ value: Double) {
270+
self.lock.withLock { _value = value }
271+
}
272+
273+
func increment(by value: Double) {
274+
self.lock.withLock { self._value += value }
275+
}
276+
277+
func decrement(by value: Double) {
278+
self.lock.withLock { self._value -= value }
279+
}
280+
}
281+
282+
private class ExampleRecorder: RecorderHandler {
283+
init(_: String, _: [(String, String)], _: Bool) {}
284+
239285
private let lock = NSLock()
240286
var values = [(Int64, Double)]()
241287
func record(_ value: Int64) {
@@ -274,23 +320,14 @@ class SimpleMetricsLibrary: MetricsFactory {
274320
}
275321
}
276322

277-
private class ExampleGauge: RecorderHandler {
323+
private class ExampleTimer: TimerHandler {
278324
init(_: String, _: [(String, String)]) {}
279325

280326
let lock = NSLock()
281-
var _value: Double = 0
282-
func record(_ value: Int64) {
283-
self.record(Double(value))
284-
}
285-
286-
func record(_ value: Double) {
287-
self.lock.withLock { _value = value }
288-
}
289-
}
327+
var _value: Int64 = 0
290328

291-
private class ExampleTimer: ExampleRecorder, TimerHandler {
292329
func recordNanoseconds(_ duration: Int64) {
293-
super.record(duration)
330+
self.lock.withLock { _value = duration }
294331
}
295332
}
296333
}

Sources/CoreMetrics/Docs.docc/index.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ and to your application/library target, add "Metrics" to your dependencies:
2727
.target(
2828
name: "BestExampleApp",
2929
dependencies: [
30-
// ...
30+
// ...
3131
.product(name: "Metrics", package: "swift-metrics"),
3232
]
3333
),
@@ -90,7 +90,8 @@ This API was designed with the contributors to the Swift on Server community and
9090

9191
### Metric types
9292

93-
- ``Gauge``
94-
- ``Recorder``
9593
- ``Counter``
94+
- ``Meter``
95+
- ``Recorder``
9696
- ``Timer``
97+

Sources/CoreMetrics/Locks.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ extension Lock {
114114
/// - Parameter body: The block to execute while holding the lock.
115115
/// - Returns: The value returned by the block.
116116
@inlinable
117-
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
117+
func withLock<T>(_ body: () throws -> T) rethrows -> T {
118118
self.lock()
119119
defer {
120120
self.unlock()
@@ -124,7 +124,7 @@ extension Lock {
124124

125125
// specialise Void return (for performance)
126126
@inlinable
127-
internal func withLockVoid(_ body: () throws -> Void) rethrows {
127+
func withLockVoid(_ body: () throws -> Void) rethrows {
128128
try self.withLock(body)
129129
}
130130
}
@@ -234,7 +234,7 @@ extension ReadWriteLock {
234234
/// - Parameter body: The block to execute while holding the reader lock.
235235
/// - Returns: The value returned by the block.
236236
@inlinable
237-
internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
237+
func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
238238
self.lockRead()
239239
defer {
240240
self.unlock()
@@ -251,7 +251,7 @@ extension ReadWriteLock {
251251
/// - Parameter body: The block to execute while holding the writer lock.
252252
/// - Returns: The value returned by the block.
253253
@inlinable
254-
internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
254+
func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
255255
self.lockWrite()
256256
defer {
257257
self.unlock()
@@ -261,13 +261,13 @@ extension ReadWriteLock {
261261

262262
// specialise Void return (for performance)
263263
@inlinable
264-
internal func withReaderLockVoid(_ body: () throws -> Void) rethrows {
264+
func withReaderLockVoid(_ body: () throws -> Void) rethrows {
265265
try self.withReaderLock(body)
266266
}
267267

268268
// specialise Void return (for performance)
269269
@inlinable
270-
internal func withWriterLockVoid(_ body: () throws -> Void) rethrows {
270+
func withWriterLockVoid(_ body: () throws -> Void) rethrows {
271271
try self.withWriterLock(body)
272272
}
273273
}

0 commit comments

Comments
 (0)