Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Version 2.0 API #92

Merged
merged 7 commits into from
Sep 18, 2023
Merged

New Version 2.0 API #92

merged 7 commits into from
Sep 18, 2023

Conversation

fabianfett
Copy link
Member

Motivation

Swift Prometheus has been in development since 2018. The previous maintainer (@MrLotU) stepped down about a year ago. Since then @ktoso and @fabianfett have maintained the existing code base.

In order to improve performance in the Prometheus library, we want to replace the existing implementation with a new one for version 2.0. While we do this we also want to get the general API closer to what the prometheus project recommends in their Writing client libraries guide. This means that we will also start to enforce Prometheus rules. For example having two counter with the same name, but one with labels and one without, is illegal in the Prometheus format. Lastly we want to remove the SwiftNIO dependency that is not needed anymore.

I'm well aware that this is a very large PR that changes lots of moving things.

Changes

  • Remove SwiftNIO dependency. Vendor in NIOLock from NIOConcurrencyHelpers
  • Replace PrometheusClient with PrometheusCollectorRegistry
  • Remove Summary implementation
  • Replace Counter, Gauge and Histogram implementations
  • Add Sendability annotations
  • Require Swift 5.7
  • Enforces that label names remain constant for a metric name.

Performance difference

In simple testing scenarios:

  • Exporting values from 1k counters into the prometheus format: ~10x speedup
  • Exporting values from 1k gauges into the prometheus format: ~10x speedup
  • Exporting values from 1k histograms into the prometheus format: ~25x speedup

Differences to Writing client libraries guide

  • There is no explicit Collector protocol, that users could implement to create their own instruments.
    • Counter, Gauge and Histogram should implement Collector
  • Currently the PrometheusCollectorRegistry does not offer a register and unregister method for Collectors
  • There is no API to create a Counter/Gauge/Histogram that is not registered with a PrometheusCollectorRegistry
  • Collectors can't be registered with multiple different PrometheusCollectorRegistrys
  • We don't advertise a default collector

Do we want to offer this API? With the current patch we could add this API later.

Copy link
Contributor

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice! Left some comments inline

@@ -0,0 +1,24 @@
# file options
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all of our recent projects, we actually adopted swift-format since it has a more stable formatting between versions. Would recommend picking it here as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this in a follow up? #94

import Atomics
import CoreMetrics

public final class Counter: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we require that all metric backing handlers are classes? This is a bit sad since this could just be a struct with reference semantics.

Second question, does this need to be public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we require that all metric backing handlers are classes? This is a bit sad since this could just be a struct with reference semantics.

I eventually want to change swift-metrics to not require AnyObject anymore. This will make NoOps much cheaper.

Second question, does this need to be public?

Yes. I think users should be able to use the Prometheus lib without going through swift-metrics.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on both, eventually we can do a revamp of metrics lib. It's one of the earliest and we've learned much since.

Yes, types should be public, prom has more detailed semantics than swift metrics so an "app" can definitely use it direcly

self.labels = labels

var prerendered = [UInt8]()
prerendered.reserveCapacity(64)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you comment why we need to reserve 64 here?


A prometheus client library for Swift.

## Overview
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add installation instructions to the index here as well. Allows us to point people directly at the documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this in a follow up?

Sources/Prometheus/Counter.swift Show resolved Hide resolved
public typealias ValueHistogram = Histogram<Double>

public final class Histogram<Value: Bucketable>: @unchecked Sendable {
private let lock = NIOLock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please use a NIOLockedValueBox here. We are really really encouraging everyone to not just put @unchcked Sendable on their types but rather stick everything into the locked value box since that only has condition Sendable conformance and we have come across a few places where people used a lock and incorrectly declared @unchecked Sendable.

///
/// To use a ``PrometheusCollectorRegistry`` with `swift-metrics` use the ``PrometheusMetricsFactory``.
public final class PrometheusCollectorRegistry: @unchecked Sendable {
// Sendability is enforced through the internal `lock`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here please


import CoreMetrics

/// A wrapper around ``PrometheusClient`` to implement the `swift-metrics` `MetricsFactory` protocol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no PrometheusClient anymore


/// A wrapper around ``PrometheusClient`` to implement the `swift-metrics` `MetricsFactory` protocol
public struct PrometheusMetricsFactory: Sendable {
/// The underlying ``PrometheusClient`` that is used to generate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here ;)

Comment on lines 33 to 37
public var valueHistogramBuckets: [String: [Double]]

public var labelAndDimensionSanitizer: @Sendable (_ label: String, _ dimensions: [(String, String)]) -> (String, [(String, String)])

public init(client: PrometheusCollectorRegistry) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get some docs on those three things?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also do I have to create two things now to bootstrap prometheus? Both the registry and the factory? Couldn't we just default this parameter here to .init()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should create a new one, or if we should provide a .default singleton. I think I'd be more in favor of the .default, wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a big benefit on a default singleton here and we avoided introducing singletons in AHC because we want to discuss this in the SSWG in a broader scope. So I would say we shouldn't start introducing it here. Just .init() default seems fine to me.

if let prerenderedLabels = Self.prerenderLabels(labels) {
prerendered.append(UInt8(ascii: "{"))
prerendered.append(contentsOf: prerenderedLabels)
prerendered.append(contentsOf: #"} "#.utf8)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't use ascii here?

}

public func increment(by amount: Int64) {
precondition(amount >= 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we give those preconditions good messages always please? It's much nicer user experience when crashes contain message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW crashed don't contain precondition messages. Only fatal errors messages are included. I have personally switched over to fatal error exclusively because of that reason


public final class Counter: Sendable {
let intAtomic = ManagedAtomic(Int64(0))
let floatAtomic = ManagedAtomic(Double(0).bitPattern)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but it's weird to call a Double property "float"; yeah "floating point" but it's a double ->

Suggested change
let floatAtomic = ManagedAtomic(Double(0).bitPattern)
let doubleAtomic = ManagedAtomic(Double(0).bitPattern)


let name: String
let labels: [(String, String)]
let prerenderedExport: [UInt8]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the key to the perf wins I suppose, good 👍

// We busy loop here until we can update the atomic successfully.
// Using relaxed ordering here is sufficient, since the as-if rules guarantess that
// the following operations are executed in the order presented here. Every statement
// depends on the execution before.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's clear here but thx for comments around busy loops, always good to document those 💯

import Atomics
import CoreMetrics

public final class Gauge: Sendable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In follow up please add docs to all those types

self.atomic.store(value.bitPattern, ordering: .relaxed)
}

public func increment(by amount: Double = 1.0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit inconsistent, on purpose? The counter had Int64 variants of APIs as well, no need here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope not un porpose


import CoreMetrics

public protocol Bucketable: AdditiveArithmetic, Comparable, Sendable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs for all those types

var bucketRepresentation: String { get }
}

public typealias TimeHistogram = Histogram<Duration>
Copy link
Collaborator

@ktoso ktoso Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm.... not really "Time", or is it from prom specs...? If not, then mirror the contained value maybe?
I'd expect DurationHistogram

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to DurationHistogram


// MARK: Destroying Metrics

public func destroyCounter(_ counter: Counter) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're pretty clear but please document.

E.g. if one can make a new one after a destroy etc

// MARK: Emitting

public func emit(into buffer: inout [UInt8]) {
let metrics = self.box.withLockedValue { $0 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is the NIO type, but tbh for such there should be a get() on it, WDYT?

Copy link
Collaborator

@ktoso ktoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, minor comments inline.

@Andrewangeta
Copy link

Looks nice! :shipit:

@ktoso
Copy link
Collaborator

ktoso commented Sep 16, 2023

Feel free to merge at will btw 👍

Copy link
Contributor

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything looks good but we are missing an implementation of the newly added Meter type.

@fabianfett fabianfett merged commit 46b8b95 into swift-server:main Sep 18, 2023
5 checks passed
@fabianfett fabianfett deleted the ff-v2 branch September 18, 2023 12:04
@fabianfett fabianfett added this to the 2.0 milestone Sep 25, 2023
fabianfett added a commit to fabianfett/SwiftPrometheus that referenced this pull request Sep 26, 2023
In swift-server#92, we added a Histogram that is generic over the observed type. This means that each Histogram was also carrying a generic argument. To work around this we created typealiases for `DurationHistogram` and `ValueHistogram`.

The motivation behind this was to preserve as much detailed information as possible. Duration can be more precise than Double. However in the grand scheme of things we now believe that this is overkill and a simpler to use Histogram type is more valuable. Thus this patch removes the generic argument from Histogram.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants