[Feedback] Swift Prometheus Implementation

Proposal Review: SSWG-0008 (Prometheus Client)

After the discussion thread , we are proposing this as a final revision of this proposal and enter the proposal review phase which will run until the 1st August 2019.

The feedback model will be very similar to the one known from Swift Evolution. The community is asked to provide feedback in the way outlined below and after the review period finishes, the SSWG will -- based on the community feedback -- decide whether to promote the proposal to the Sandbox maturity level or not.

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the evolution of the server-side Swift ecosystem.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough?
  • Does this proposal fit well with the feel and direction of Swift on Server?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thank you for contributing to the Swift Server Work Group!

What happens if the proposal gets accepted?

If this proposal gets accepted, the official repository will be created and the code (minus examples, the proposal text, etc) will be submitted. The repository will then become usable as a SwiftPM package and a version (likely 0.1.0 ) will be tagged. The development (in form of pull requests) will continue as a regular open-source project.


SwiftPrometheus - Prometheus Metrics in Swift

Package Description

Prometheus client side implementation.

Package Name SwiftPrometheus
Module Name Prometheus & PrometheusMetrics
Proposed Maturity Level Sandbox
License Apache 2.0
Dependencies swift-nio 1.0.0..<3.0.0 (1.x.x & 2.x.x) - swift-metrics > 1.0.0

Introduction

For a background on metrics see the metrics proposal discussion and feedback thread.

Prometheus is one of the most widely used libraries for metrics in the serverside world. SwiftPrometheus is a client side implementation in Swift, with the ability to use it both connected to & separately from swift-metrics.

Motivation

With Prometheus being one of the most widely used metric reporting tools, it's a buildstone that can not be left out in a serverside ecosystem. This package is created for everyone to use & build upon for their metric reporting.

Detailed design

SwiftPrometheus works around one base class PrometheusClient and some metric types around it. The prometheus metric types are:
(from the prometheus docs)

  • 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.
  • Gauge - A gauge is a metric that represents a single numerical value that can arbitrarily go up and down.
  • Histogram - A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets. It also provides a sum of all observed values.
  • Summary - Similar to a histogram , a summary samples observations (usually things like request durations and response sizes). While it also provides a total count of observations and a sum of all observed values, it calculates configurable quantiles over a sliding time window.

SwiftPrometheus provides fully featured implementations for all of them, including a thin wrapper around them for integration with swift-metrics.

API Layout

Below section will lay out the public API of this package. For the internal APIs I would suggest you to read through the code on GitHub :smile:. This section is split up into two parts, using this library standalone, or using it integrated with the swift-metrics package.

Without swift-metrics

To get started, initialise an instance of PrometheusClient

let myProm = PrometheusClient()

Once done, you can use the create* APIs to create any of the above described metric types.

// MetricLabels is a helper type used to add labeled metrics.
struct MyCodable: MetricLabels {
   var thing: String = "*"
}

// - Counter
let counter = myProm.createCounter(forType: Int.self, named: "my_counter", helpText: "Just a counter", initialValue: 42, withLabelType: MyCodable.self)

counter.inc() // Increment by one
counter.inc(12) // Increment by a value
counter.inc(12, MyCodable(thing: "test")) // Increment a labeled counter

// - Gauge
let gauge = myProm.createGauge(forType: Int.self, named: "my_gauge", helpText: "Just a gauge", initialValue: 42, withLabelType: MyCodable.self)
gauge.inc() // Same APIs as Counter
gauge.dec() // Same APIs as `inc()` but reversed.
gauge.set(42) // Set the gauge to a specific value

// - Histogram
// Histograms use special labels, different than the Counter & Gauge
struct HistogramLabel: HistogramLabels {
   var le: String = ""
   let route: String

   init() {
       self.route = "*"
   }

   init(_ route: String) {
       self.route = route
   }
}

let histogram = myProm.createHistogram(forType: Double.self, named: "my_histogram", helpText: "Just a histogram", labels: HistogramLabel.self)

histogram.observe(123) // Observes a value

// - Summary
// Like Histograms, Summaries use different label types.
struct SummaryLabel: SummaryLabels {
   var quantile: String = ""
   let route: String

   init() {
       self.route = "*"
   }

   init(_ route: String) {
       self.route = route
   }
}

let summary = myProm.createSummary(forType: Double.self, named: "my_summary", helpText: "Just a summary", labels: SummaryLabel.self)

summary.observe(123) // Observes a value

Then, after you have some metric types, you can use .collect() on your PromtheusClient to get your Prometheus formatted string with all the data.
For example, in a Vapor app:

router.get("/metrics") { req -> String in 
    return myProm.collect()
}

With swift-metrics

For use with swift-metrics, most of the steps described above work the same. To bootstrap the MetricsSystem you create a client and feed it to MetricsSystem:

let myProm = PrometheusClient()
MetricsSystem.bootstrap(myProm)

After that, you can use the metric types used by swift-metrics for your metrics. The mapping is as follows:

swift-metrics SwiftPrometheus
Counter Counter
Gauge Gauge
Recorder (agg) Histogram
Timer Summary

To get a hold of your PrometheusClient either to:
a) use custom prometheus behaviour; or
b) get your metrics output
there is a utility function on MetricsSystem

let myProm = try MetricsSystem.prometheus()

This will either return the PrometheusClient used with .bootstrap() or throw an error if MetricsSystem was not bootstrapped with PrometheusClient
Note: There currently is no support for retrieving PrometheusClient when being used with MultiplexMetricsHandler

Maturity Justification

The implementation has the full feature set required for production use and meets the minimum requirements set forth by the SSWG (except for the fact that I'm a 1 man army creating this library)

Alternatives considered

Other than using a different metrics backend than Prometheus, there are not many alternatives to consider. One thing I'd like to point out though:

This library has support for the destroying of metrics in the way set forth by the swift-metrics package. However, as described in the Prometheus documentation, once a metric is created with a specific type, so for example a Counter named my_counter and that counter is destroyed, it's not allowed to, at a later time, re-create a metric named my_counter with a DIFFERENT type. (Creating another counter is fine). To keep track of this, PrometheusClient will hold a dictionary of metric names & types. ([String: MetricType]). This means that even if you destroy your metrics, your memory footprint will (gradually) increase. All of this is process bound and will reset on a process restart.

Thanks & ending notes

On the ending note of this proposal, I would like to thank a few people specifically:

  • @johannesweiss - Technical help & advise

  • @ktoso - Technical help & advise

  • Anyone who gave input during the initial pitch.

Next to these specific mentions, I'd like to thank you for taking the time to read my proposal and I would love for you to leave a comment below with your thoughts & comments :smile:

7 Likes

First, a quick note: your "Status: Implemented" link is dead and 404s. Please update that link: probably it's best to just point it at the root of the repo to avoid the risk of it breaking in future.

Next, I appreciate that it's a bit late in the game, but I'd like to propose a few API changes in order to future-proof the code.

The first change I'd like to suggest is that PrometheusClient.collect should probably be changed to accept an EventLoopPromise<String> instead of returning a String. My reasoning here is that for large servers metrics calculation can potentially become quite expensive, especially as we have to build a representation of the entire metrics state of the system to return to the Prometheus server.

In the current design this is done by taking a lock and doing the work synchronously on the current thread. That's fine for now, and I'm happy to leave that design in place for v1, but in the future we would probably like to move towards APIs that can better handle the workload of large servers, and such a design will necessarily be delegating the work of both processing metrics changes and building the representation to a background thread. Cross-thread communication in a NIO model requires a Promise, so we should place one into the API now to allow us to adopt the new design in the future without incurring any API breakages.

While we're doing this we should also consider whether, in addition to a EventLoopPromise<String> version of the collect function, we should also have an EventLoopPromise<ByteBuffer> version (which would probably be the base implementation). The reasoning here is that high performance servers may want to avoid the cost of creating a String only to copy its bytes into a ByteBuffer for sending to the wire, and it'd be good to allow that. Naturally having access to a String version is convenient and I don't think we should throw it away, but having both is likely helpful as well.

6 Likes

Thanks for chiming in Cory :slight_smile: Great catch actually, +1, on both the promise as well as an ByteBuffer impl. Technically we could also "stream out" those metrics, but currently we do not have the library to pull that off, so indeed a Promise should at least not lock it to the calling thread :+1:

Another point which we should keep stressing is the wording around the intended usage of such libraries by other libraries -- as they should definitely use the Metrics API/SPI and not implementations. I'll PR some wording proposal around this so it is clear to users and library authors how to use these things so that the metrics ecosystem can take off big time :)

I'll give the repo another look some time soon, thanks for putting up the feedback thread @MrLotU!

4 Likes

Thanks @MrLotU! I'm also +1 on this but second Cory's suggestions. Do they sound good to you too?

1 Like

Thanks for your comments @lukasa, @ktoso and @johannesweiss!

I agree with Cory's suggestion too. I'll see when I can find the time to implement it (though a PR is also welcome :smile:) The only reason I did not do this up until this point is because I wanted to keep the dependencies on other libraries as low as possible. Right now the only dependency is NIOConcurrencyHelpers for Locks.

As for the usage bit @ktoso mentioned; what my vision on this is that any library should indeed use swift-metrics and never be any the wiser as to what backend is being used. The end user is the only one who knows the backend, but in the case of Prometheus, the direct APIs are more powerfull in some places, so I would push end users to use SwiftPrometheus directly and not rely on the swift-metrics APIs

Thanks again for your feedback! I hope I addressed everything like this :smiley:

Small update: I found some time and opened a PR: https://github.com/MrLotU/SwiftPrometheus/pull/12 comments are welcome! :smiley:

@MrLotU okay to close this topic, or do you want to keep open?

I vote to close :+1:
Let's keep ongoing development and improvements in tickets from hereon?

Sorry. Forgot to reply. I think it’s OK to close this. Agree with @ktoso to put things in GH issues/PRs from now on

2 Likes