Swift-Metrics with Multiple Backends
Swift Metrics is an open-source package written in Swift that provides observability for software products. The idea is that you have a standard way to deploy these metrics to a backend.
// 1) let's import the metrics API package
import Metrics
// 2) we need to create a concrete metric object, the label works similarly to a `DispatchQueue` label
let counter = Counter(label: "com.example.BestExampleApp.numberOfRequests")
// 3) we're now ready to use it
counter.increment()
You implement backends by conforming to the MetricsFactory protocol
public protocol MetricsFactory {
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
func destroyCounter(_ handler: CounterHandler)
func destroyRecorder(_ handler: RecorderHandler)
func destroyTimer(_ handler: TimerHandler)
}
And then bootstrapping your metrics implementation
let metricsImplementation = MyFavoriteMetricsImplementation()
MetricsSystem.bootstrap(metricsImplementation)
The assumption with this structure is that you will ship your metrics to one location; a server, a file, or somewhere else. This seems limiting to some degree. There may be a need to collect the same metrics and ship them to multiple places at the same time. Separating out the Metric itself from the backend, I may want to ship the same metrics to the following places for specific reasions.
1. Console - debugging 2. File - field data for crashes, etc. 3. HTTPS - Platform monitoring and correlation with other information 4. StatsD - Just for fun
An example:
let amazingMetricsSystem = AmazingMetricsSystem(backends: [
ConsoleBackend(),
FileBackend(),
HTTPSPostBackend()
])
One path for this implementation could be through specifying another protocol "Backend", with a single function called emit that takes a metric as a parameter.
protocol Backend {
func emit(metric: Metric)
}
Emit could be as simple as a print statement or be a post call
public func emit(_ metric: Metric) {
_ = client.post(url: self.url, item: metric, headers: ["Content-Type":"application/json"])
}
or a StatsD call
func emit(_ metric: Metric) -> EventLoopFuture<Void> {
return self.connect().flatMap { channel in
channel.writeAndFlush(metric)
}
}
This implementation of this in the "MetricsKit" would look something like:
public func record(_ value: Double) {
self.lock.withLock {
// this may loose precision but good enough as an example
values.append((Date(), Double(value)))
}
//print("recoding \(value) in \(self.label)")
self.backends.forEach { $0.emit( Metric(name: self.label, value: value, type: .gauge) )}
}
(This needs to be adjusted so that it's asyncronous.)
The Metric would be protocol based and implemented as follows:
public protocol MetricProtocol: Codable {
var id: Int? { get set }
var name: String { get set }
var value: String { get set }
var type: MetricType { get set }
}
public struct Metric: MetricProtocol {
//static var schema: String = "metrics"
//@ID(key: "id")
public var id: Int?
//@Field(key: "name")
public var name: String
//@Field(key: "value")
public var value: String
//@Field(key: "type")
public var type: MetricType
public init(id: Int? = nil, name: String, value: Int64, type: MetricType) {
self.name = name
self.value = String(value)
self.type = type
}
public init(id: Int? = nil, name: String, value: Double, type: MetricType) {
self.name = name
self.value = floor(value) != value ? String(value) : String(Int64(value))
self.type = type
}
}
public enum MetricType: String, Codable, CaseIterable {
case gauge = "g"
case counter = "c"
case timer = "ms"
case histogram = "h"
case meter = "m"
}
The backend does the transformation necessary that hides the implementation details of the backend separate from the metric collection itself.
The idea being that each backend asynchronosyly emits upon receipt of a metric payload.
How is this different than the current architecture?
Swift-Metrics today represents each Metrics implementation as a backend. In theory, you whould implement and bootstrap for each implementation. That is not possible today. You can only bootstrap to one implementation. This requires me to create three fully seperate "architectures" for metrics reporting.