- Notifications
You must be signed in to change notification settings - Fork68
apple/swift-metrics
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A Metrics API package for Swift.
Almost all production server software needs to emit metrics information for observability. Because it's unlikely that all parties can agree on one specific metrics backend implementation, this API is designed to establish a standard that can be implemented by various metrics libraries which then post the metrics data to backends likePrometheus,Graphite, publish overstatsd, write to disk, etc.
This is the beginning of a community-driven open-source project actively seeking contributions, be it code, documentation, or ideas. Apart from contributing to SwiftMetrics itself, we need metrics compatible libraries which send the metrics over to backend such as the ones mentioned above. What SwiftMetrics provides today is covered in theAPI docs, but it will continue to evolve with community input.
If you have a server-side Swift application, or maybe a cross-platform (e.g. Linux, macOS) application or library, and you would like to emit metrics, targeting this metrics API package is a great idea. Below you'll find all you need to know to get started.
To add a dependency on the metrics API package, you need to declare it in yourPackage.swift
:
// swift-metrics 1.x and 2.x are almost API compatible, so most clients should use.package(url:"https://github.com/apple/swift-metrics.git","1.0.0"..<"3.0.0"),
and to your application/library target, add "Metrics" to your dependencies:
.target( name:"BestExampleApp", dependencies:[ // ....product(name:"Metrics",package:"swift-metrics"),]),
// 1) let's import the metrics API packageimport Metrics// 2) we need to create a concrete metric object, the label works similarly to a `DispatchQueue` labelletcounter=Counter(label:"com.example.BestExampleApp.numberOfRequests")// 3) we're now ready to use itcounter.increment()
Note: If you are building a library, you don't need to concern yourself with this section. It is the end users of your library (the applications) who will decide which metrics backend to use. Libraries should never change the metrics implementation as that is something owned by the application.
SwiftMetrics only provides the metrics system API. As an application owner, you need to select a metrics backend (such as the ones mentioned above) to make the metrics information useful.
Selecting a backend is done by adding a dependency on the desired backend client implementation and invoking theMetricsSystem.bootstrap
function at the beginning of the program:
MetricsSystem.bootstrap(SelectedMetricsImplementation())
This instructs theMetricsSystem
to installSelectedMetricsImplementation
(actual name will differ) as the metrics backend to use.
As the API has just launched, not many implementations exist yet. If you are interested in implementing one see the "Implementing a metrics backend" section below explaining how to do so. List of existing SwiftMetrics API compatible libraries:
- SwiftPrometheus, support forPrometheus
- StatsD Client, support for StatsD
- OpenTelemetry Swift, support forOpenTelemetry which also implements other metrics and tracing backends
- Your library?Get in touch!
You may also be interested in some "extra" modules which are collected in theSwift Metrics Extras repository.
We believe that for the Swift on Server ecosystem, it's crucial to have a metrics API that can be adopted by anybody so a multitude of libraries from different parties can all provide metrics information. More concretely this means that we believe all the metrics events from all libraries should end up in the same place, be one of the backends mentioned above or wherever else the application owner may choose.
In the real world, there are so many opinions over how exactly a metrics system should behave, how metrics should be aggregated and calculated, and where/how to persist them. We think it's not feasible to wait for one metrics package to support everything that a specific deployment needs while still being simple enough to use and remain performant. That's why we decided to split the problem into two:
- a metrics API
- a metrics backend implementation
This package only provides the metrics API itself, and therefore, SwiftMetrics is a "metrics API package." SwiftMetrics can be configured (usingMetricsSystem.bootstrap
) to choose any compatible metrics backend implementation. This way, packages can adopt the API, and the application can choose any compatible metrics backend implementation without requiring any changes from any of the libraries.
This API was designed with the contributors to the Swift on Server community and approved by the SSWG (Swift Server Work Group) to the "sandbox level" of the SSWG's incubation process.
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.
counter.increment(by:100)
FloatingPointCounter
: A variation of aCounter
that records a floating point value, instead of an integer.
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 aRecorder
with a sample size of 1 that does not perform any aggregation.
gauge.record(100)
Meter
: A Meter is similar toGauge
- 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. UnlikeGauge
,Meter
also supports atomic incerements and decerements.
meter.record(100)
Recorder
: A recorder collects observations within a time window (usually things like response sizes) andcan provide aggregated information about the data sample, for example count, sum, min, max and various quantiles.
recorder.record(100)
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 aRecorder
but specialized for values that represent durations.
timer.recordMilliseconds(100)
Note: Unless you need to implement a custom metrics backend, everything in this section is likely not relevant, so please feel free to skip.
As seen above, each constructor forCounter
,Gauge
,Meter
,Recorder
andTimer
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:
letmetricsImplementation=MyFavoriteMetricsImplementation()MetricsSystem.bootstrap(metricsImplementation)
This instructs theMetricsSystem
to installMyFavoriteMetricsImplementation
as the metrics backend (MetricsFactory
) to use. This should only be done once at the beginning of the program.
Given the above, an implementation of a metric backend needs to conform toprotocol MetricsFactory
:
publicprotocolMetricsFactory{func makeCounter(label:String, dimensions:[(String,String)])->CounterHandlerfunc makeMeter(label:String, dimensions:[(String,String)])->MeterHandlerfunc makeRecorder(label:String, dimensions:[(String,String)], aggregate:Bool)->RecorderHandlerfunc makeTimer(label:String, dimensions:[(String,String)])->TimerHandlerfunc destroyCounter(_ handler:CounterHandler)func destroyMeter(_ handler:MeterHandler)func destroyRecorder(_ handler:RecorderHandler)func destroyTimer(_ handler:TimerHandler)}
TheMetricsFactory
is responsible for instantiating the concrete metrics classes that capture the metrics and perform aggregation and calculation of various quantiles as needed.
Counter
publicprotocolCounterHandler:AnyObject{func increment(by:Int64)func reset()}
Meter
publicprotocolMeterHandler:AnyObject{func set(_ value:Int64)func set(_ value:Double)func increment(by:Double)func decrement(by:Double)}
Recorder
publicprotocolRecorderHandler:AnyObject{func record(_ value:Int64)func record(_ value:Double)}
Timer
publicprotocolTimerHandler:AnyObject{func recordNanoseconds(_ duration:Int64)}
Implementation of metric objects that deal with integers, likeCounter
andTimer
should be careful with overflow. The expected behavior is to cap at.max
, and never crash the program due to overflow . For example:
classExampleCounter:CounterHandler{varvalue:Int64=0func increment(by amount:Int64){letresult=self.value.addingReportingOverflow(amount)if result.overflow{self.value=Int64.max}else{self.value= result.partialValue}}}
Here is a full, but contrived, example of an in-memory implementation:
classSimpleMetricsLibrary:MetricsFactory{init(){}func makeCounter(label:String, dimensions:[(String,String)])->CounterHandler{returnExampleCounter(label, dimensions)}func makeMeter(label:String, dimensions:[(String,String)])->MeterHandler{returnExampleMeter(label, dimensions)}func makeRecorder(label:String, dimensions:[(String,String)], aggregate:Bool)->RecorderHandler{returnExampleRecorder(label, dimensions, aggregate)}func makeTimer(label:String, dimensions:[(String,String)])->TimerHandler{returnExampleTimer(label, dimensions)} // implementation is stateless, so nothing to do on destroy callsfunc destroyCounter(_ handler:CounterHandler){}func destroyMeter(_ handler:TimerHandler){}func destroyRecorder(_ handler:RecorderHandler){}func destroyTimer(_ handler:TimerHandler){}privateclassExampleCounter:CounterHandler{init(_:String, _:[(String,String)]){}letlock=NSLock()varvalue:Int64=0func increment(by amount:Int64){self.lock.withLock{self.value+= amount}}func reset(){self.lock.withLock{self.value=0}}}privateclassExampleMeter:MeterHandler{init(_:String, _:[(String,String)]){}letlock=NSLock()var_value:Double=0func set(_ value:Int64){self.set(Double(value))}func set(_ value:Double){self.lock.withLock{ _value= value}}func increment(by value:Double){self.lock.withLock{self._value+= value}}func decrement(by value:Double){self.lock.withLock{self._value-= value}}}privateclassExampleRecorder:RecorderHandler{init(_:String, _:[(String,String)], _:Bool){}privateletlock=NSLock()varvalues=[(Int64, Double)]()func record(_ value:Int64){self.record(Double(value))}func record(_ value:Double){ // TODO: sliding window lock.withLock{ values.append((Date().nanoSince1970, value))self._count+=1self._sum+= valueself._min=Swift.min(self._min, value)self._max=Swift.max(self._max, value)}}var_sum:Double=0varsum:Double{returnself.lock.withLock{ _sum}}privatevar_count:Int=0varcount:Int{returnself.lock.withLock{ _count}}privatevar_min:Double=0varmin:Double{returnself.lock.withLock{ _min}}privatevar_max:Double=0varmax:Double{returnself.lock.withLock{ _max}}}privateclassExampleTimer:TimerHandler{init(_:String, _:[(String,String)]){}letlock=NSLock()var_value:Int64=0func recordNanoseconds(_ duration:Int64){self.lock.withLock{ _value= duration}}}}
Please seeSECURITY.md for details on the security process.
Do not hesitate to get in touch as well, over onhttps://forums.swift.org/c/server
About
Metrics API for Swift