Metrics

Another area the Swift Server working group is looking to improve is server-side metrics. Currently, there is no standard metrics solution which works across frameworks and libraries.

We are interested to collect ideas and requirements people have, especially where they are informed by experience in other language ecosystems, or experience deploying server-side applications in production today.

To kick the discussion off, I'll reply to this message with some initials thoughts of mine.

3 Likes

wSimilar to what we are defining for logging, we would like to suggest an abstract metrics API which will allow application owners to plug-in different metrics backends, and library developers to emit metrics without getting in the way. Reasoning for such architecture is summarized in here, that post is about logging but the same is true for metrics as well.

An example for such abstract API could look something like:

public protocol Counter: AnyObject {
    func increment<DataType: BinaryInteger>(_ value: DataType)
}

public protocol Recorder: AnyObject {
    func record<DataType: BinaryInteger>(_ value: DataType)
    func record<DataType: BinaryFloatingPoint>(_ value: DataType)
}

public protocol Timer: AnyObject {
    func recordNanoseconds(_ duration: Int64)
}

public protocol MetricsHandler {
    func makeCounter(label: String, dimensions: [(String, String)]) -> Counter
    func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder
    func makeTimer(label: String, dimensions: [(String, String)]) -> Timer
}

public extension MetricsHandler {
    @inlinable
    func makeGauge(label: String, dimensions: [(String, String)] = []) -> Recorder {
        return self.makeRecorder(label: label, dimensions: dimensions, aggregate: false)
    }
}

this means:

  1. the actual metrics library (eg https://forums.swift.org/t/client-side-prometheus-implementation) will implement MetricsHandler

  2. the application owner bootstrap the with metrics library of choice (exact API tbd)

  3. application and libraries developers call something like the below when they want to emit metrics

let counter = Metrics.makeCounter("foo")
...
counter.increment()` 
3 Likes

This looks like a great start. A couple questions:

Why do the protocols conform to AnyObject? Is this just another way of restricting the protocol to a class, or does it mean something more? Side note, maybe it would be nice to have a protocol for this like MetricsObject or something that unites them.

How will you register which MetricsHandler you want to use? Will that be exactly like how logging works? Something like

Metrics.bootstrap(PrometheusMetricsHandler.init)

How will using shared metrics objects work? It seems that with the current design, if you wanted to use a shared counter, recorder, and timer you would need to pass those all separately (i.e., req.counter.increment(), req.recorder.record(...)). Comparing to logging, we have:

  • Metrics = Logging
  • MetricsHandler = LogHandler
  • AnyObject (MetricsObject) = Logger, but more than one

Maybe there should be a separate, unified type like Logger in this package. With Logging, you just pass around the Logger and you can log any type of message (i.e., req.logger.log(...)).
But I'm not really sure if this difference is a bad thing. Maybe it's a common use case to only want to pass around one type of metrics object.

thanks for the feedback @tanner0101

Why do the protocols conform to AnyObject ? Is this just another way of restricting the protocol to a class, or does it mean something more?

by forcing them to be classes, we can reduce their memory footprint, and there is no real advantage of allowing them to be structs as they don't carry state

How will you register which MetricsHandler you want to use? Will that be exactly like how logging works?

exactly! you would "bind" your backend of choice at application bootstrap time, just as you described

Maybe there should be a separate, unified type like Logger in this package. With Logging, you just pass around the Logger and you can log any type of message

the main reason to pass loggers around (for example, expose them at the request/context object) is that they carry contextual state (metadata) which you want to preserve. with metrics, you dont typically carry such state, so its less often the case you want to pass them around. having said that, the dimensions are typically contextual so an argument could be made to pass some metrics object that carries them around and from which you can create named timers, counters, etc. good direction to explore further, not sure of it should be part of the core api or implemented on top by frameworks that want to operate in that manner

1 Like

Ah okay, that makes sense then. Thanks!

Hello,

Great work with the proposal!

I'm thinking about a couple of usability improvements for Timer class, which may be very useful for future Metrics users.
I've just posted GitHub issue with my idea, here is the link: Metrics Timer usability improvement idea · Issue #2 · tomerd/swift-server-metrics-api-proposal · GitHub

Didn't want to copy-paste the whole issue, if it's more convenient for you just let me know and I will move it here from GitHub.

Thanks!

1 Like