TheCB4

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.

Tagged with: