Giter Site home page Giter Site logo

swift-metrics's Introduction

SwiftMetrics

A Metrics API package for Swift.

Almost all production server software needs to emit metrics information for observability. Because it's unlikely that all parties can agree on one specific metrics backend implementation, this API is designed to establish a standard that can be implemented by various metrics libraries which then post the metrics data to backends like Prometheus, Graphite, publish over statsd, write to disk, etc.

This is the beginning of a community-driven open-source project actively seeking contributions, be it code, documentation, or ideas. Apart from contributing to SwiftMetrics itself, we need metrics compatible libraries which send the metrics over to backend such as the ones mentioned above. What SwiftMetrics provides today is covered in the API docs, but it will continue to evolve with community input.

Getting started

If you have a server-side Swift application, or maybe a cross-platform (e.g. Linux, macOS) application or library, and you would like to emit metrics, targeting this metrics API package is a great idea. Below you'll find all you need to know to get started.

Adding the dependency

To add a dependency on the metrics API package, you need to declare it in your Package.swift:

// swift-metrics 1.x and 2.x are almost API compatible, so most clients should use
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"),

and to your application/library target, add "Metrics" to your dependencies:

.target(
    name: "BestExampleApp",
    dependencies: [
        // ... 
        .product(name: "Metrics", package: "swift-metrics"),
    ]
),

Emitting metrics information

// 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()

Selecting a metrics backend implementation (applications only)

Note: If you are building a library, you don't need to concern yourself with this section. It is the end users of your library (the applications) who will decide which metrics backend to use. Libraries should never change the metrics implementation as that is something owned by the application.

SwiftMetrics only provides the metrics system API. As an application owner, you need to select a metrics backend (such as the ones mentioned above) to make the metrics information useful.

Selecting a backend is done by adding a dependency on the desired backend client implementation and invoking the MetricsSystem.bootstrap function at the beginning of the program:

MetricsSystem.bootstrap(SelectedMetricsImplementation())

This instructs the MetricsSystem to install SelectedMetricsImplementation (actual name will differ) as the metrics backend to use.

As the API has just launched, not many implementations exist yet. If you are interested in implementing one see the "Implementing a metrics backend" section below explaining how to do so. List of existing SwiftMetrics API compatible libraries:

Swift Metrics Extras

You may also be interested in some "extra" modules which are collected in the Swift Metrics Extras repository.

Detailed design

Architecture

We believe that for the Swift on Server ecosystem, it's crucial to have a metrics API that can be adopted by anybody so a multitude of libraries from different parties can all provide metrics information. More concretely this means that we believe all the metrics events from all libraries should end up in the same place, be one of the backends mentioned above or wherever else the application owner may choose.

In the real world, there are so many opinions over how exactly a metrics system should behave, how metrics should be aggregated and calculated, and where/how to persist them. We think it's not feasible to wait for one metrics package to support everything that a specific deployment needs while still being simple enough to use and remain performant. That's why we decided to split the problem into two:

  1. a metrics API
  2. a metrics backend implementation

This package only provides the metrics API itself, and therefore, SwiftMetrics is a "metrics API package." SwiftMetrics can be configured (using MetricsSystem.bootstrap) to choose any compatible metrics backend implementation. This way, packages can adopt the API, and the application can choose any compatible metrics backend implementation without requiring any changes from any of the libraries.

This API was designed with the contributors to the Swift on Server community and approved by the SSWG (Swift Server Work Group) to the "sandbox level" of the SSWG's incubation process.

pitch | discussion | feedback

Metric types

The API supports four metric types:

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. For example, you can use a counter to represent the number of requests served, tasks completed, or errors.

counter.increment(by: 100)

Gauge: A Gauge is a metric that represents a single numerical value that can arbitrarily go up and down. Gauges are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. Gauges are modeled as a Recorder with a sample size of 1 that does not perform any aggregation.

gauge.record(100)

Meter: A Meter is similar to Gauge - a metric that represents a single numerical value that can arbitrarily go up and down. Meters are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. Unlike Gauge, Meter also supports atomic incerements and decerements.

meter.record(100)

Recorder: A recorder collects observations within a time window (usually things like response sizes) and can provide aggregated information about the data sample, for example count, sum, min, max and various quantiles.

recorder.record(100)

Timer: A timer collects observations within a time window (usually things like request duration) and provides aggregated information about the data sample, for example min, max and various quantiles. It is similar to a Recorder but specialized for values that represent durations.

timer.recordMilliseconds(100)

Implementing a metrics backend (e.g. Prometheus client library)

Note: Unless you need to implement a custom metrics backend, everything in this section is likely not relevant, so please feel free to skip.

As seen above, each constructor for Counter, Gauge, Meter, Recorder and Timer provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. Each application can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:

let metricsImplementation = MyFavoriteMetricsImplementation()
MetricsSystem.bootstrap(metricsImplementation)

This instructs the MetricsSystem to install MyFavoriteMetricsImplementation as the metrics backend (MetricsFactory) to use. This should only be done once at the beginning of the program.

Given the above, an implementation of a metric backend needs to conform to protocol MetricsFactory:

public protocol MetricsFactory {
    func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
    func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler
    func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler    
    func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler

    func destroyCounter(_ handler: CounterHandler)
    func destroyMeter(_ handler: MeterHandler)
    func destroyRecorder(_ handler: RecorderHandler)
    func destroyTimer(_ handler: TimerHandler)
}

The MetricsFactory is responsible for instantiating the concrete metrics classes that capture the metrics and perform aggregation and calculation of various quantiles as needed.

Counter

public protocol CounterHandler: AnyObject {
    func increment(by: Int64)
    func reset()
}

Meter

public protocol MeterHandler: AnyObject {
    func set(_ value: Int64)
    func set(_ value: Double)
    func increment(by: Double)
    func decrement(by: Double)
}

Recorder

public protocol RecorderHandler: AnyObject {
    func record(_ value: Int64)
    func record(_ value: Double)
}

Timer

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

Dealing with Overflows

Implementation of metric objects that deal with integers, like Counter and Timer should be careful with overflow. The expected behavior is to cap at .max, and never crash the program due to overflow . For example:

class ExampleCounter: CounterHandler {
    var value: Int64 = 0
    func increment(by amount: Int64) {
        let result = self.value.addingReportingOverflow(amount)
        if result.overflow {
            self.value = Int64.max
        } else {
            self.value = result.partialValue
        }
    }
}

Full example

Here is a full, but contrived, example of an in-memory implementation:

class SimpleMetricsLibrary: MetricsFactory {
    init() {}

    func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
        return ExampleCounter(label, dimensions)
    }

    func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
        return ExampleMeter(label, dimensions)
    }

    func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
        return ExampleRecorder(label, dimensions, aggregate)
    }

    func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
        return ExampleTimer(label, dimensions)
    }

    // implementation is stateless, so nothing to do on destroy calls
    func destroyCounter(_ handler: CounterHandler) {}
    func destroyMeter(_ handler: TimerHandler) {}
    func destroyRecorder(_ handler: RecorderHandler) {}    
    func destroyTimer(_ handler: TimerHandler) {}

    private class ExampleCounter: CounterHandler {
        init(_: String, _: [(String, String)]) {}

        let lock = NSLock()
        var value: Int64 = 0
        func increment(by amount: Int64) {
            self.lock.withLock {
                self.value += amount
            }
        }

        func reset() {
            self.lock.withLock {
                self.value = 0
            }
        }
    }

    private class ExampleMeter: MeterHandler {
        init(_: String, _: [(String, String)]) {}

        let lock = NSLock()
        var _value: Double = 0

        func set(_ value: Int64) {
            self.set(Double(value))
        }

        func set(_ value: Double) {
            self.lock.withLock { _value = value }
        }

        func increment(by value: Double) {
            self.lock.withLock { self._value += value }
        }

        func decrement(by value: Double) {
            self.lock.withLock { self._value -= value }
        }
    }

    private class ExampleRecorder: RecorderHandler {
        init(_: String, _: [(String, String)], _: Bool) {}

        private let lock = NSLock()
        var values = [(Int64, Double)]()
        func record(_ value: Int64) {
            self.record(Double(value))
        }

        func record(_ value: Double) {
            // TODO: sliding window
            lock.withLock {
                values.append((Date().nanoSince1970, value))
                self._count += 1
                self._sum += value
                self._min = Swift.min(self._min, value)
                self._max = Swift.max(self._max, value)
            }
        }

        var _sum: Double = 0
        var sum: Double {
            return self.lock.withLock { _sum }
        }

        private var _count: Int = 0
        var count: Int {
            return self.lock.withLock { _count }
        }

        private var _min: Double = 0
        var min: Double {
            return self.lock.withLock { _min }
        }

        private var _max: Double = 0
        var max: Double {
            return self.lock.withLock { _max }
        }
    }

    private class ExampleTimer: TimerHandler {
        init(_: String, _: [(String, String)]) {}

        let lock = NSLock()
        var _value: Int64 = 0

        func recordNanoseconds(_ duration: Int64) {
            self.lock.withLock { _value = duration }
        }
    }
}

Security

Please see SECURITY.md for details on the security process.

Getting involved

Do not hesitate to get in touch as well, over on https://forums.swift.org/c/server

swift-metrics's People

Contributors

cpriebe avatar fabianfett avatar franzbusch avatar gjcairo avatar hamzahrmalik avatar ktoso avatar lukasa avatar mordil avatar mrlotu avatar natikgadzhi avatar rauhul avatar sherlouk avatar slashmo avatar tomerd avatar weissi avatar yasumoto avatar yim-lee avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swift-metrics's Issues

Allow additional dimensions when recording a value

Current behavior

I create a Counter to track the total number of requests in my application:

private let requestsCounter = Metrics.Counter(label: "http_requests_total", dimensions: [(String, String)]())

Desired Behavior

I'd like to use that in my router to increase the metric on "per-path, per-method, and per-response-code" dimensions:

requestsCounter.dimensions = [
        ("method", request.http.method.string),
        ("path", request.http.url.path),
        ("http_code", "200")
]

Note that this is heavily biased toward using 🔥 Prometheus with its concept of labels which map to our dimensions. In other systems (such as graphite) I'd use different label names.

Considered Approaches

I could create a separate Counter for each path, but then I'd need to create a boldly exploding multi-dimensional matrix of Counters for each dimension.

However, what if we updated the actual "write a value" methods like Counter.increment() and Timer.record() to also take in more dimensions? We'd need to update the way we handle DimensionLabels in SwiftPrometheus but I think that'd allow us to leverage labels from swift-metrics.

github hosted docs have a link to install in dash, but no generated docset

Expected behavior

When I was browsing the docs at https://apple.github.io/swift-metrics/docs/current/CoreMetrics/index.html, I noticed a link to install the docs as a docset into Dash. Sounded good for offline reading, so I clicked it - but the processing failed. As I dug in, it became clear that whatever is generating the documentation is not including the docset in what gets sent into gh-pages branch, which reflects through these docs.

I'm not sure if not including the docset was intentionally, or a miss somewhere - but if it's not intended to be included, the theme files for Jazzy should likely be tweaked to remove the "install into Dash" thing that's displayed by default.

If possible, minimal yet complete reproducer code (or URL to code)

SwiftMetrics version/commit hash

41d2db7

Swift & OS version (output of swift --version && uname -a)

n/a, but for completeness sake:

Apple Swift version 5.2 (swiftlang-1103.0.32.1 clang-1103.0.32.29)
Target: x86_64-apple-darwin19.4.0
Darwin greyberry.local 19.4.0 Darwin Kernel Version 19.4.0: Wed Mar  4 22:28:40 PST 2020; root:xnu-6153.101.6~15/RELEASE_X86_64 x86_64

Release notes for 2.x should do a better job explaining what happened

I think the release notes for 2.0.0 should make it more clear what exactly happened. Because swift-metrics is an API package, issuing a new major could easily lead to an ecosystem split which we should really avoid so it's important everybody understands. I filed the following issues with known dependents:

Additional sugar: Timer.recordInterval(since: DispatchTime, now: DispatchTime = .now())

Noticed during our work on apple/swift-cluster-membership#70 and other projects this pattern:

extension Timer {
    /// Records time interval between the passed in `since` dispatch time and `now`.
    func recordInterval(since: DispatchTime, now: DispatchTime = .now()) {
        self.recordNanoseconds(now.uptimeNanoseconds - since.uptimeNanoseconds)
    }
}

emerges a lot when a message contains "sent at" or something like that, and then reporting metrics becomes timer.recordInterval(since: message.sentAt) leaving the user without any having to dance around math with the time amounts

RFC: Introduce means to "destroy"/"remove" metrics objects

Importing from tomerd/swift-server-metrics-api-proposal#6

Rewritten the rationale in a more formal form, please discuss.


Define a way to "destroy" metrics

  • Previous ticket: tomerd/swift-server-metrics-api-proposal#6
  • Status: implemented #3 open to adjustments
  • For end-users: This is an additive change, it enables end-users to do things which they were not able to do before; it does not force them to do so.
  • For metrics library authors: This sets a strong implementation style recommendation, that resources shall be allowed to be destroyed at the users request. We ask that implementations important for the Swift Server ecosystem adopt this semantic, yet also allow that there may be implementations which do not have a need for implementing this call (e.g. stateless ones)

This proposal aims to explain the need for "destroying" metrics via the common Metrics API. The proposal explores a wide variety of possible metrics library implementations, and attempts to strike a balance that is safe, performant and should not block any implementation from achieving it's specific goals.

In order to bring more focus onto the discussion -- as it has been laying around, but perhaps its importance underestimated so far, the proposal will follow swift-evolution inspired format for the writeup.

Proposal

There must be a well-defined way to "destroy", metrics in long lived processes if we know a metric will never again be touched.

This proposal forms a case for why this API is required and best to be included right away, rather than as an after thought, and that it is best to perform the "destroy" operations explicitly rather than implicitly.

Having that said, the primary goal here is that there must be a common agreed way to solve this for all implementations of this API, otherwise the model breaks down for any external library exporting metrics -- so if someone had better ideas, we are open to discussing them, within reasonable timeline.

Terms

Before diving into the case analysis, let's define some terms to be on the same page when we talk about things.

  • metrics API -- for the sake of this discussion, this entire library apple/swift-metrics
  • metrics backend, metrics library -- a library which implements this API,
  • external library / "3rd party library" -- for sake of this discussion I mean this only as "some library which depends on metrics API, and wants to offer metrics. It can not and does not know about the metrics library." This is not about the 1st/3rd party thing in the sense of "an Apple library or not."
  • short lived metric -- the "short" is relative here, but the point is "lives shorter than the entire lifetime of the application

Analysis of cases where delete/destroy does not matter

Before we dive into the actual problematic cases, let us acknowledge that for a lot of metrics, the destroy() may never need to be invoked, and this is absolutely fine and expected.

Metrics which stay around for entire lifetime of app

This is very typical in HTTP servers which want to instrument and expose a "global view", e.g. offering: "histogram of all request durations", "gauge of memory used by app", "gauge of requests per second, globally for app" etc. We can see an example of such here in Vapor Metrics: https://github.com/vapor-community/VaporMonitoring/blob/master/Sources/VaporMonitoring/VaporMetricsPrometheus.swift#L14-L23

These are absolutely expected to never have to be destroyed -- these kinds of metrics are few, and stay around for the entire lifetime of the application. This proposal does not concern this type of metrics at all; with explicit destroy such metrics would never call it, and with implicit via ARC they would ensure to always keep strong references.

👍

Problematic cases analysis

"Caching metrics library" and "short lived" metrics

This is an example of what might quite often be implemented in the real world: a metrics backend which has a form of "registry,"
in fact, current implementations in Swift already fall in this category e.g.:

These libraries keep strong references to metrics objects in order to print or give them back to users when asked by label. There is no API that might allow removing those strong references, thus whichever resources for metrics we would allocate, stay there forever. This may seem fine for typical HTTP metrics which include global throughput, or per-path endpoints, but quickly explodes in size if metrics are high-density histograms (weighting potentially kilobytes), or many unique identities want to report metrics, but are known to disappear after a while.

Inspecting live system for more information "for a while"

Another case to highlight is a case of temporarily increasing "measure everything on this hot path for a few seconds", where we may have instrumented code, but the counters are not being hit in normal operation, however when debugging we may want to enable "measure everything" which would be too expensive to keep always on in production.

We may get a lot of counters and metrics from this, inspect them, and maybe find a bottleneck -- e.g. imagine a streaming application with many stages. And it is grinding to a halt -- you may want to measure "what is the throughput at each of the stages of this stream" to locate the bottleneck. You do not want those metrics to be reported forevermore from here on, since collecting them is too costly at this granularity, but during interactive debugging it can be invaluable to gain insights about the system.

In this example, the stream library or someone could has explicit information about when to stop measuring, and the metrics objects can be destroyed. This is only one of the examples where we might want to have explicit destroy(), calls but plenty other situations call for a similar mechanism. Hopefully this one is interesting enough to exemplify the issue.

Proposed Solution

Solution 1: library.destroy(Metric) or metric.destroy() 👍

This proposal aims to introduce an explicit function on metrics factory to "destroy" or "delete" (name to be decided),
metrics from the system; It is up to implementations to decide when exactly it will be destroyed, however they should aim
to do so as early as possible after such call:

public protocol MetricsFactory {
    // ...

    /// Signals the `MetricsFactory` that the passed in `MetricHandler` will no longer be updated.
    /// Implementing this functionality is _optional_, and depends on the semantics of the concrete `MetricsFactory`.
    ///
    /// In response to this call, the factory _may_ release resources associated with this metric,
    /// e.g. in case the metric contains references to "heavy" resources, such as file handles, connections,
    /// or large in-memory data structures.
    ///
    /// # Intended usage
    ///
    /// Metrics library implementations are _not_ required to act on this signal immediately (or at all).
    /// However, the presence of this API allows middle-ware libraries wanting to emit metrics for resources with
    /// well-defined life-cycles to behave pro-actively, and signal when a given metric is known to not be used anymore,
    /// which can make an positive impact with regards to resource utilization in case of metrics libraries which keep
    /// references to "heavy" resources.
    ///
    /// It is expected that some metrics libraries, may choose to omit implementing this functionality.
    /// One such example may be a library which directly emits recorded values to some underlying shared storage engine,
    /// which means that the `MetricHandler` objects themselves are light-weight by nature, and thus no lifecycle
    /// management and releasing of such metrics handlers is necessary.
    func destroy<M: Metric>(metric: M)

    // alternatively, delete the Metric type and provide overloads:
    // func destroy<C: Counter>(_ counter: C)
    // func destroy<R: Recorder>(_ recorder: R)
    // func destroy<T: Timer>(_ timer: T)
}

Open question 1, naming bikeshed:

  • destroy
  • delete
  • release was ruled out since it is confusing with ARC release terminology
  • better ideas?

It should be "dual" to the make, I'm open to ideas here.

Open question 2, should we add the Metric type to allow writing the single destroy<M: Metric>(metric) function, or rather ask implementations to implement 3 overloads. I am on the side of introducing the Metric type, but am happy to be convinced otherwise if there are good reasons.

Open question 3, shall the destroy() method live:

  • on the MetricsFactory (only),
  • also be added to MetricsSystem (and delegate to the factory), or
  • be moved to the metric types themselves so one could write counter.destroy()

I am strongly in favor of the current design and/or adding it to the system itself as well, as adding the function to the types themselves will lead us to:

  • grow storage size per metric by another pointer to the metric system which can be bothersome for light objects like counters (esp if an object would contain many conters).
  • this reference to the factory inside of the metric likely cause a reference cycle between the factory (if it has a "registry", which many advanced systems will have -- e.g. all current implementations in Swift), which we'd need to break and complicate the design even more.
  • the majority of use cases where destroy calls will be needed a library or framework will be doing these calls, and not end users (!).
    • if end users do want to destroy metrics explicitly, they likely know they want to do so.

Note that this design addition is strictly adding functionality of removing metrics, to implementations which otherwise would never release metrics at all. In that sense, this proposal does not make it "worse" or "more difficult" than existing API and implementations -- they did not release, and would continue not to release (unless they care to), and would be as well-off as they have been already.

Detailed design

A full implementation is provided in #3

Implementations are simple, and are the dual of making a metric.

public func destroy<M: Metric>(metric: M) {
    switch metric {
    case let counter as Counter:
        self.counters.removeValue(forKey: counter.label)
    case let recorder as Recorder:
        self.recorders.removeValue(forKey: recorder.label)
    case let timer as Timer:
        self.timers.removeValue(forKey: timer.label)
    default:
        return // nothing to do, not a metric that we created/stored
    }
}

Alternatively, 3 overloads of this could be provided, and the now-added for this implementation Metric type could be destroyed. Note that the removing of metrics could be implemented using various ways, but most of the time will boil down to removing by label, or some other identifier (which does NOT necessarily have to be the printable name -- e.g. if a library supported some form of nesting etc).

This, again, ONLY matters if a metrics factory as a form of "registry." If your implementation is completely stateless e.g. "print out whenever a metric is updated", this change does not impact your library, so you can implement it as:

    public func destroy<M: Metric>(metric: M) {
        // no-op
    }

It has been considered to provide a default empty implementation however this seems to cause confusion for reviewers as well as for people using the library -- they can not easily check if the implementation is empty or not just by opening the metrics library source. It is nicer and more explicit for understandability of a library to explicitly opt out.

As for user API impact:

  • for metrics which "stay for the lifetime of the app"
    • users nor library authors need to change anything.
  • for metrics which are managed by a framework or library and have fixed "short" lifecycle
    • library developers are likely aware of these lifecycles and know when to invoke destroy
    • if they have lifecycles, but do not destroy the metrics, it is not worse than status quo, and it is simpler to put destroy calls in the right lifecycle callbacks which such framework likely has (even if internally)
  • for metrics which are created and managed by end users:
    • most metrics are "for app lifetime" metrics, and not releasing is exactly same as status quo and same as many other metrics ecosystems
    • some metrics, which users know they want to destroy since interactively debugging or similar, they are now enabled to do so.

Alternatives considered

"Just™ Ride on ARC" 👎

"Just™" riding on ARC for correctness of a program and always releasing whenever the users "let go" of a metric sounds very tempting, however it yields very unexpected side effects, and blocks from easily implementing various useful patterns.

This seems very tempting, however we argue that it is abusing ARC and actively complicates the designs and understandability of lifecycle of metrics. Most notably, it makes the following style not possible to implement correctly:

// "Exhibit A"
// CAN'T implement this nicely with ARC based destroy()
class RarelyCreatedResource {
    static func make() {
        // we want easily measure how many times this is invoked
        // so we add the following line:
        Counter(label: "RarelyCreatedResource.creations").increment()
        // we assumed our metrics library is a "caching one" (see above)
        // ...
        // but if the implementation is ARC based, the counter is destroyed() right away after this line
    }
}

// Would have to be this:
class RarelyCreatedResource {
    static let creationsCounter = Counter(label: "RarelyCreatedResource.creations")

    static func make() {
        creations.increment() // always works, _regardless_ if ARC based or `destroy()`-API based.
    }
}

Another scary implication of such design is that it MAY lure implementations into performing more cleanup actions in deinit, which is best to be avoided and could/would most likely cause issues in multi-threaded environments (esp. since there are no guarantees about which thread the deinitializer is run on). Alternatively implementations would have to implement "periodically clean up all metrics which may have been released" -- finding those metrics again requires synchronization and potentially taking locks around the registry cleanup operation -- which may be contended by metrics user threads wanting to obtain metrics instances.

Another example where ARC complicates things in "weird ways", because we are actually abusing it somewhat to express lifetimes which do not necessarily match the lifecycle of the metric itself (!), is when we would like to express the following:

  • A metrics backend which collects all metrics, and prints them out every 3 seconds (or reports totals to somewhere).

Now imagine that we have a counter that comes in and out of reachability a few times during this period. This means it's storage would also be released as it appears and disappears via ARC lifecycle management. This would cause us to report wrongly to the user that the metric has a count of x while in reality it had some x+y+z value.

This is possible to work around of course, by separating the metric storage from the weakly referenced by the registry metric itself, e.g. like this:

// "Exhibit B"
// "work" but is abusing the ARC mechanism and making it difficult to reason about lifecycle
// NOT proposing we adopt this.

class AtLeastOnceCounterHandler: CounterHandler {
     let container: AtLeastOnceCounterContainer

      init(container: AtLeastOnceCounterContainer) {
         self.container = container
     }

      func increment(by: Int64) {
         self.container.value += by
     }
     func reset() {
         self.container.value = 0
     }

  }
 class AtLeastOnceCounterContainer {
     weak var handler: AtLeastOnceCounterHandler? = nil
     var value: Int64 = 0
 }

class PrintAtLeastOnceMetrics: MetricsFactory {
    var counters: [String: AtLeastOnceCounterContainer] = [:]

    public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
        let container = AtLeastOnceCounterContainer()
        let counter = AtLeastOnceCounterHandler(container: container)
        container.handler = counter
        counters[label] = container
        return counter
    }

This implementation makes it non trivial to understand when a metric has reached end of its life, and also makes it a requirement to start implementing "gc-like" structures in the registry -- it would have to scan the registry for "was possibly something destroyed?", which would need to take write locks around the datastructure perhaps etc.

All in all this implementation would make it:

  • hard to reason about
  • very complex, multiple cyclic references that need to be broken
  • needs to invent GC-like processes to clean up the registry; since doing so from deinitializers is highly discouraged.

Summary:

  • While it is very tempting to "ride on ARC" for the releasing of metrics objects, it is the semantically wrong thing to do.
  • "Ride on ARC" may yield surprising problems, like Exhibit A, which people could expect to work fine, until later on, on production, they discover they made a mistake; I would not be surprised if someone was then tempted to implement a strong registry themselves -- likely on strong references, and cause the entire ARC dance to be useless.
  • Since lifecycle in this alternate solution is strongly fixed by ARC, users would have to "keep passing around" the same exact instance of a counter, potentially causing much boilerplate.
  • It may seem that riding on ARC is less error prone, in the sense that "people won't have a destroy() call they can forget about", however in reality the majority of metrics are either: a) "forever" or b) "managed for end-users" by middle ware / frameworks / libraries which explicitly control lifecycle of some resources.
  • We can not infer "will never be used again" safely from just ARC alone, as the Exhibit A shows.

Prior Art (for destroy/release API)

Go

Rust

Java

Erlang

Summary:

  • Almost all libraries / APIs opt for explicit release/destroy APIs
    • technically runtimes like the JVM (or Rust, even quite the same with RC/ARC) could do "ARC style", but it is considered an anti pattern, so they do not.
    • it is worth noting that metrics lifecycle simply is not the same as object lifecycle itself for many of the API patterns we may want to offer, thus an explicit API more natural.

consider changing `Timer`, `Counter`, `Gauge` and `Recorder` to a value type.

Currently Timer, Counter, Gauge and Recorder are immutable and don't serve as a storage for corresponding timer/counter etc. data. They serve only as a bridge to an actual <metricType>Handler classes.
Actual backends are only holding a registry of underlying -Handler and public API types are only stored in a user code. This likely means that in the usual use case Timer and counterparts won't be passed around a lot, and changing them to a value type will reduce allocations.

Consider a type preserving API

This is a very raw first draft for a new API proposal.

Motivation

Swift Metrics API currently follows the same design principles as Swift Logging. The Server Metrics API forums thread links out to a PR.

Currently users create new metrics by calling initializers on different Metric types:

Counter(label: String, dimensions: [(String, String)])

When users call this, internally we will reach out to the registered MetricFactory through the global MetricSystem.factory to create a new CounterHandler, so that the new Counter can be backed by the just created CounterHandler.

Generally I think this behavior is great and I do not want to change it.

However whenever I use Swift Metrics, I pass around the MetricsFactory manually instead of going through the global MetricSystem.factory. The reason for this is to allow better testability, when using the TestMetrics from the MetricsTestKit module. If I don't use the global MetricSystem.factory, I can easily create a new TestMetrics instance for each test run, which even allows me to run tests in parallel.

Currently the MetricsFactory protocol creates existential MetricHandlers. For each supported Metric type, the factory returns an existential MetricHandler (CounterHandler, TimerHandler, etc.). The reason for not returning explicitly typed handlers here, is that the MetricsFactory needed to be stored as an existential itself in the MetricsSystem.global. Before Swift 5.7 existentials were not able to have associated types.

However thanks to SE-309 Unlock existentials for all protocols we are now able to make MetricsFactory type safe.

Proposed Solution

Since I don't want to break API, I propose a new Protocol that adopters can implement:

public protocol TypePreservingMetricsFactory: MetricsFactory {
    associatedtype Counter: CounterProtocol
    associatedtype FloatingPointCounter: FloatingPointCounterProtocol
    associatedtype Timer: TimerProtocol
    associatedtype Recorder: RecorderProtocol

    func makeSomeCounter(label: String, dimensions: [(String, String)]) -> Self.Counter

    func makeSomeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> Self.FloatingPointCounter

    func makeSomeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Self.Recorder

    func makeSomeTimer(label: String, dimensions: [(String, String)]) -> Self.Timer

    func destroyCounter(_ handler: Self.Counter)

    func destroyFloatingPointCounter(_ handler: Self.FloatingPointCounter)

    func destroyRecorder(_ handler: Self.Recorder)

    func destroyTimer(_ handler: Self.Timer)
}

I added the Some word into the make function calls to ensure those methods are not overloaded by return type. Other naming suggestions are highly welcome. The new CounterProtocol is very close to the existing CounterHandler protocol. However it adds requirements for the label and the dimensions.
The reason for this is simple: I would like to remove the current Counter, Timer and Recorder wrapper classes, that add one level of indirection, that isn't needed.

public protocol CounterProtocol: CounterHandler {
    var label: String { get }
    var dimensions: [(String, String)] { get }
}

public protocol FloatingPointCounterProtocol: FloatingPointCounterHandler {
    var label: String { get }
    var dimensions: [(String, String)] { get }
}

If we want to preserve the option to create an untyped Metric in the future with an init, we could use callAsFunction on top of a protocol.

If a user implements TypePreservingMetricsFactory all methods needed for MetricsFactory are implemented by a default implementation:

// This extension ensures TypePreservingMetricsFactory also implements MetricsFactory
extension TypePreservingMetricsFactory {
    public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
        self.makeSomeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
    }

    public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
        self.makeSomeTimer(label: label, dimensions: dimensions)
    }

    public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
        self.makeCounter(label: label, dimensions: dimensions)
    }

    public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler {
        self.makeSomeFloatingPointCounter(label: label, dimensions: dimensions)
    }

    public func destroyTimer(_ handler: TimerHandler) {
        if let handler = handler as? Self.Timer {
            self.destroySomeTimer(handler)
        }
    }

    public func destroyCounter(_ handler: CounterHandler) {
        if let handler = handler as? Self.Counter {
            self.destroySomeCounter(handler)
        }
    }

    public func destroyRecorder(_ handler: RecorderHandler) {
        if let handler = handler as? Self.Recorder {
            self.destroySomeRecorder(handler)
        }
    }

    public func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) {
        if let handler = handler as? Self.FloatingPointCounter {
            self.destroySomeFloatingPointCounter(handler)
        }
    }
}

Future ideas

  • The make and destroy methods on the TypePreservingMetricsFactory should be async to allow the use of an actor to create, maintain and destroy the created metrics. However this is a breaking change.
  • We should make the Gauge type stand byitself and not be implemented on top of recorder.
  • We should potentially offer simple Counter, Timer and Recorder implementations, that implementors of backends can use right away. So backend developers only have to worry about metric lifecycle management and exporting.

Clarify more visibly in docs intended use SPI/API difference

Expected behavior

Users should not be confused about how to use the API.

Actual behavior

In docs and browsing the code the "init that should not be used" is the first one people encounter -- as it is defined on the classes, e.g.:

    public init(label: String, dimensions: [(String, String)], handler: TimerHandler) {

while the one that should be used is only defined later on in an extension:

    convenience init(label: String, dimensions: [(String, String)] = []) {

We should make this easier to spot and understand.


There's a few "levels" how much we want to make the spi/api split explicit, let's start with more docs and formatting and we'll see...

Allow for struct MetricHandlers

Currently, MetricHandlers are declared as follows (taking TimerHandler as an example here)

public protocol TimerHandler: AnyObject {
    /// Record a duration in nanoseconds.
    ///
    /// - parameters:
    ///     - value: Duration to record.
    func recordNanoseconds(_ duration: Int64)
}

where they conform to AnyObject specifying they have to be classes.
The reasoning behind this can be found on the forums:

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
(https://forums.swift.org/t/metrics/19353/4)

I however, did run into a case where (IMO) it'd be nicer & cleaner to have them be structs.

I'd like to propose the following solution as a workaround:

public protocol BaseTimerHandler {
    /// Record a duration in nanoseconds.
    ///
    /// - parameters:
    ///     - value: Duration to record.
    func recordNanoseconds(_ duration: Int64)
}

public protocol TimerHandler: BaseTimerHandler, AnyObject { }

Where users should be encouraged to use TimerHandler instead of BaseTimerHandler, but the rest of the flow can use BaseTimerHandler and handle both classes and structs.

Please let me know what you think. If the overall consensus is that this'd be a good addition/change, I'll open up a PR 😄

[Question] What is the utility of aggregate?

I look at the code and tests. However, I couldn't figure out the objective of having `aggregate property.
The only test I found that mention that property was the description one.

func testDescriptions() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let timer = Timer(label: "hello.timer")
XCTAssertEqual("\(timer)", "Timer(hello.timer, dimensions: [])")
let counter = Counter(label: "hello.counter")
XCTAssertEqual("\(counter)", "Counter(hello.counter, dimensions: [])")
let gauge = Gauge(label: "hello.gauge")
XCTAssertEqual("\(gauge)", "Gauge(hello.gauge, dimensions: [], aggregate: false)")
let recorder = Recorder(label: "hello.recorder")
XCTAssertEqual("\(recorder)", "Recorder(hello.recorder, dimensions: [], aggregate: true)")
}

Gauge.record() is under-defined

So I've been using the API a bit, and found an confusing thing:

It is confusing with regards how one should implement gauge.record(). An implementation I just reviewed, the prometheus one, implements it by delegating to it's underlying add which I think is incorrect, however we don't really provide any guidance about it, so I can see how the author ended up in this:

// SwiftPrometheus
    func record(_ value: Int64) {
        self.record(value.doubleValue)
    }
    
    func record(_ value: Double) {
        gauge.inc(value, labels)
    }

Remember also that we force a gauge to be an NOT aggregated recorder:

    public convenience init(label: String, dimensions: [(String, String)] = []) {
        self.init(label: label, dimensions: dimensions, aggregate: false)
    }

Which again strengthens my idea that it should be a "set" operation.

https://github.com/MrLotU/SwiftPrometheus/blob/master/Sources/PrometheusMetrics/PrometheusMetrics.swift#L37-L42

I think we need to:

  • specify which semantics gauge.record -- I think it should be a "set" on the Gauge, and on Histogram it depends I guess but also sounds like a "set"
  • I wonder if we are able to offer an add an add() really (case for this below)...

For reference, the only guidance we provide are:

/// A recorder collects observations within a time window (usually things like response sizes) and can provide aggregated information about the data sample,
/// for example, count, sum, min, max and various quantiles.

and

/// Record a value.

public func record<DataType: BinaryInteger>(_ value: DataType) {

Which does not explain if record is a set or add, or if its interpretation should change depending if the recorder / gauge is aggregate or not.


Note also that open telemetry is converging on gauges offering both and I'd tend to agree with that actually now that I had to use our API a bit:

https://github.com/open-telemetry/opentelemetry-java/blob/master/api/src/main/java/io/opentelemetry/metrics/GaugeLong.java

  • add
  • set -- like our record

Allow including baggage in reporting metrics?

It should be possible to co-relate metrics to a baggage context.

It effectively means adding some baggage items as labels to a metric.

This likely can be done as extensions in some way?

Should Gauge gain `add` in addition to `record`?

Expected behavior

I'd like to use gauges to easily implement a metric of how many things I have alive in my system -- examples could be threads or connections.

// ==== ----------------------------------------------------------------------------------------------------------------
// goal: easily implement gauges which measure "amount of alive things"

class Heavy {

    init() {
        // this is hard to express when we only have "record":
        let gauge = Gauge(label: "heavy-1")
        gauge.add(1) 
    }

    deinit {
        // this is hard to express when we only have "record":
        gauge.add(-1) 
    }

    // or start() / stop(), same story

}

Actual behavior

Can't implement this like that since we do not offer add, we only offer record, so I have to implement my own thing which keep the total number and then use that to record into the actual metrics gauge:

Implementation idea 1, which fails since Gauge is not open:

// ==== ----------------------------------------------------------------------------------------------------------------
// wishful thinking workaround impl, though heavy...

///
/// error: cannot inherit from non-open class 'Gauge' outside of its defining module
/// internal class AddGauge: Gauge {
///                ^
@usableFromInline
internal class AddGauge: Gauge {

    @usableFromInline
    internal let _storage: Atomic<Int>
    internal let underlying: Gauge

    public convenience init(label: String, dimensions: [(String, String)] = []) {
        self.underlying = Gauge(label: label, dimensions: dimensions)
    }

    override func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
        self.underlying.record(value)
    }

    @inlinable
    func add<DataType: BinaryFloatingPoint>(_ value: DataType) {
        let value = self._storage.add(value)
        self.underlying.record(value)
    }
}

So what I end up is something like:

// ==== ----------------------------------------------------------------------------------------------------------------
// current workaround

@usableFromInline
internal class AddGauge {

    @usableFromInline
    internal let _storage: Atomic<Int>
    @usableFromInline
    internal let underlying: Gauge

    public init(label: String, dimensions: [(String, String)] = []) {
        self._storage = .init(value: 0)
        self.underlying = Gauge(label: label, dimensions: dimensions)
    }

    @inlinable
    func record(_ value: Int) {
        self.underlying.record(value)
    }

    @inlinable
    func add<DataType: BinaryFloatingPoint>(_ value: DataType) {
        let v = Int(value)
        self.underlying.record(self._storage.add_load(v))
    }
}

Main question:

  • Should we add add() to Gauge as it enables this popular use case?
    • if yes, this needs a major bump, better now than later I guess though
    • if no, what's the pattern people should adopt for this use case?
  • alternatively making the types open would allow for implementing such things nicer...
    • that's likely a BAD idea though, so let's not
  • we could tell people to implement a special weird handler, and add an extension to Counter.add which has to pattern match on the handler if it supports an add operation or not... it seems a bit weird though and would come at a cost.

Other points:

Explicitly set counter values

swift-metrics doesn't currently provide a Gauge.record api for Counters and this probably makes as Counters are supposed to be strictly incrementing. However this behavior doesn't compose if the metric source already accumulates the value.

let total = GetTotalNetworkPackets()
Counter(label: "network_packets_total")
  .increment(total) // <-- increment is wrong here (double counting)

Instead you have to use a Gauge to directly set this value. However consumers now see the value as a gauge and not a counter which is the real behavior.

I'm not sure if this is actually something we want to support, but it probably warrants some thought.


Prometheus in go provides an escape hatch for this using NewConstMetric

Export default process & runtime metrics

From the Prometheus client library spec, Client libraries SHOULD offer what they can of the Standard exports, documented below.

Metric name Help string Unit
process_cpu_seconds_total Total user and system CPU time spent in seconds. seconds
process_open_fds Number of open file descriptors. file descriptors
process_max_fds Maximum number of open file descriptors. file descriptors
process_virtual_memory_bytes Virtual memory size in bytes. bytes
process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes. bytes
process_resident_memory_bytes Resident memory size in bytes. bytes
process_heap_bytes Process heap size in bytes. bytes
process_start_time_seconds Start time of the process since unix epoch in seconds. seconds

I think these metrics would be useful to have, also outside of Prometheus.

My proposal is to implement these metrics into swift-metrics directly, with the option of switching them off.

Possible API would be:

MetricsSystem.bootstrap(_ factory: MetricsFactory, provideSystemMetrics: Bool = true)

As a note I'd like to add I'm not 100% sure which of the above metrics are feasible and possible to get a hold of in Swift, so some experimentation/research might be needed there.

Timer does not maintain units

Expected behavior

When I record a duration taking duration as a certain format, I expect the metric to be exposed in that same unit.

Actual behavior

As described in swift-server/swift-prometheus#10, I'm looking at unexpected behavior where I'm inputting a duration as Seconds, but then the unit is exported as nanoseconds.

I understand why we're storing the value as an Int, but the unit translation is unexpected when it's being displayed.

This is something that maybe we could handle in the implementations but that doesn't feel correct.

Steps to reproduce

  1. Record a duration on a Timer using seconds as the unit. summary.recordSeconds(1.0)
  2. collect the metrics.

If possible, minimal yet complete reproducer code (or URL to code)

https://github.com/MrLotU/SwiftPrometheus/pull/11/files#diff-5b4f613ceb7c7173fa4cddb0c60c9e49R133

SwiftMetrics version/commit hash

master

[Question] Best Practice for Timer w/o Foundation.Date

While working with SwiftMetrics to implement a Timer I was left wondering what's the best way to capture the start/end without reaching for Foundation.Date?

My first instinct was to use NIO.TimeAmount or NIO.NIODeadline as they have the semantics I'm after of just grabbing references to .now() and finding the difference (and having the proper types for Timer.recordNanoseconds(_:) - but the documentation and usage of NIODeadline as a point in time is dirty.

So I looked into Dispatch.DispatchTimeInterval, but those APIs work with UInt64 rather than the desired Int64 to pass to Timer.recordNanoseconds(_:). This would require handling overflow and conversion.

In all other cases with Timer, things are generally handled pretty well for me (due to generics), but in the base situation of recordNanoseconds I'm left to have to handle this myself.

According to this thread #5 I feel like the best option is to check for possible overflow and then default to max.

For context, my use case is to grab .now() before executing an I/O operation, and then finding the new .now() when my callback is first executed.

API for setting list of dimensions on `record`, rather than on Metric creation

Current metrics api only supports setting dimensions on metric creation. I think to fully leverage dimensions and express in code that certain set of metrics report into a same metric label, we need to allow setting dimensions on record as well as in constructor. E.g. imagine we're trying to instrument an http server. With custom dimensions we could do something like this:

let requestDurationTimer = Timer(label: "request_duration", dimensions: [("instance": "myInstance")])
func handle(request: Request) {
  var statusCodeClass: String = "2XX"
  let startTime = DispatchTime.now()
  defer { 
    requestDurationTimer.recordNanoseconds(since: startTime, 
                                           dimensions: [("statusCode": statusCodeClass, 
                                                         "uri": request.uri)])
  }
  do {
    try handleRequest(request)
  } catch let error as ServerError {
    // recovery routine
    statusCodeClass = "5XX"
  } catch let error as UserError {
    statusCodeClass = "4XX"
  }
}

We won't have to create a separate requestDurationTimer for each "uri" as well as a separate timer for each uri/error class combination. This also applies to more complex state machines instrumentation when we may want to express certain event belongs to a certain state/command.

Allow for multiple backends to emit metrics

Expected behavior

let amazingMetricsSystem = AmazingMetricsSystem(backends: [
  ConsoleBackend(),
  OSLog(Backend),
  FileBackend(),
  StatsdBackend(),
  PrometheusBackend(),
  HTTPSPostBackend()
])

Maybe this is too complex, but would be helpful if I want to see console, File, and maybe a remote host of some sort.

This could be done through a protocol conformance

protocol Backend {
  func emit(metric: Metric)
}

working example

import ClientKit
import MyMetricsKit

class HttpBackend: Backend {
  private let client: Client

  public var url: String
  public var configuration: Client.Configuration
  
  public init(url: String, configuration: Client.Configuration) {
    self.url = url
    self.configuration = configuration
    self.client = Client(eventLoopGroupProvider: .createNew, configuration: configuration)
  }
  
  public func emit(_ metric: Metric) {
    _ = client.post(url: self.url, item: metric, headers: ["Content-Type":"application/json"])
  }
}

  public func recordNanoseconds(_ duration: Int64) {
    self.lock.withLock {
      values.append((Date(), duration))
    }
    //print("recoding \(duration) \(self.label)")
    self.backends.forEach { $0.emit( Metric(name: self.label, value: duration, type: .timer) )}
  }

Potentially could add metadata to each metric as well for OS and other things ( that can be reset for privacy).

Re-initializing swift-metrics in tests

Expected behavior

Right now, there is no way to re-initialize the global swift-metrics configuration. MetricsSystems.bootstrap fails if called for a second time. I understand that this is deliberate.

However, this can be a problem when writing tests for an application that uses swift-metrics. Consider three types of tests: (i) Tests that don't care about the metrics backend and run fine with the no-op metrics handler; (ii) tests that make sure that certain metrics are submitted to the metrics backend for which it would be helpful to register a custom test backend; and (iii) (integration) tests which test that the application sets up the application backend properly or that rely on the correct backend.

swift-metrics itself relies on the MetricsSystem.bootstrapInternal function for its own tests to re-initialize the metrics system but as the name suggest that is declared as internal.

The current workaround is to use bootstrapInternal via @testable import CoreMetrics. Is this the intended way of testing code with swift-metrics dependencies? Is there/Could there be a better alternative?

Note that in case (iii), there currently doesn't seem to be a way at all to have multiple tests that call the application code that ends up calling MetricsSystem.bootstrap, e.g. when the backend configuration depends on application arguments. Should these type of tests be treated as integration tests that should run in their own process?

Actual behavior

Tests that require to re-initialize the metrics system can only do so by relying on the internal MetricsSystem.bootstrapInternal function. Multiple tests that call application code relying on MetricsSystem.bootstrap must run in separate processes.

API for creating metrics with specific buckets or quantiles

Some more users often need to pre-configure their metrics with pre-determined bucket sizes (and numbers) or quantiles they want to report. This matters most for metrics which do some form of aggregation client side, like prometheus (e.g. https://github.com/MrLotU/SwiftPrometheus/blob/master/Sources/Prometheus/Prometheus.swift#L210 ).

We should make it possible to use the SwiftMetrics API without dropping to raw APIs and still be able to configure bucket sizes etc. These are a common and not specific-backend bound configuration parameter.

It could look something like:

Timer(
    label: "hello", 
    dimensions: [("a": "aaa")], 
    buckets: [.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10]
)
Timer(
    label: "hello",
    dimensions: [("a": "aaa")], 
    buckets: .linear([.100, .100, 100)]
)
Timer(
    label: "hello",
    dimensions: [("a": "aaa")], 
    buckets: .exponential([1, 2, 64)]
)

Timer(
    label: "hello",
    dimensions: [("a": "aaa")], 
    quantiles: [0.5, 0.05, 0.9, 0.01]
)

Use case discussed with @avolokhov, we'll iterate on the exact needs and shapes of this soon

[Feature Request] Update dimensions in Gauge/Meter/etc. a public var

Expected behavior

When collecting metrics for Prometheus, I found using previously declared Gauge/etc. was not convenient.
The following code snippet shows how I reuse the Gauge/etc. in Go:

type Collector struct {
	sync.Mutex
	usedMemory        *prometheus.GaugeVec
}

func (c *Collector) Collect(ch chan<- prometheus.Metric) {
	// Only one Collect call in progress at a time.
	c.Lock()
	defer c.Unlock()

	c.usedMemory.Reset()

	...
	for i := 0; i < int(numDevices); i++ {
		c.usedMemory.WithLabelValues(index, uuid, name, sn, slot).Set(float64(mem.Used))
	}
	...
}

I can reuse usedMemory for multiple devices and update the labels (similar to dimensions). But dimensions is a public let in Gauge/etc., I can't mutate its values.

Actual behavior

dimensions is a public let in Gauge/etc., I can't mutate its values after init.

Steps to reproduce

N/A

If possible, minimal yet complete reproducer code (or URL to code)

N/A

SwiftMetrics version/commit hash

2.4.1

Swift & OS version (output of swift --version && uname -a)

~ swift --version
swift-driver version: 1.62.15 Apple Swift version 5.7.1 (swiftlang-5.7.1.135.3 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0
~ uname -a
Darwin bogon 22.6.0 Darwin Kernel Version 22.6.0: Sun Dec 17 22:14:44 PST 2023; root:xnu-8796.141.3.703.2~2/RELEASE_ARM64_T8103 arm64

Consider defining type alias for dimension `(String, String)`

Expected behavior

It should be clear what each String in a dimension tuple represents.

Actual behavior

Saw this code and had to google search to remind myself that dimensions are just key-value pairs:

let dimensions: [(String, String)]

Please consider defining type alias:

typealias Dimension = (key: String, value: String)

let dimensions: [Dimension]

Steps to reproduce

N/A

If possible, minimal yet complete reproducer code (or URL to code)

N/A

SwiftMetrics version/commit hash

1.1.0

Swift & OS version (output of swift --version && uname -a)

N/A

Logging metrics factory

When adding metrics to an application or framework it would be useful to have some indication of the metrics emitted during execution to serve as a basic sanity check ("when I do this does metric X get emitted?").

Providing a metrics factory which prints to stdout/stderr whenever a value is recorded would be helpful for this case. Is this something we think we should provide in swift-metrics?

overflow

originally brought up by @weissi

We should think about integer overflows, kinda likely to happen with metrics and you wouldn’t want to crash for metrics.

We could check for them explicitly or use &+ and friends instead of + but that’s only defined behaviour for unsigned ints I think (that’s how it is in C , overflow defined for unsigned and undefined for signed ints).

This is just to remind us to think about it and write tests.

Nio has examples for overflow tests: https://github.com/apple/swift-nio/blob/06649bb8c704a042fc07f2013ae429e2d646e7bb/Tests/NIOTests/ByteBufferTest.swift#L938

Sanity script should check for missing `return` statements

Since Swift 5.2 (IIRC) you're allowed to omit the return statement in single statement functions and getters as such:

func foo() -> String {
    "bar"
}

Since we also support versions pre 5.2, the sanity script should check for this and enforce the use of return statements everywhere.

Expected behavior

The sanity script should check and correct missing return statements.

Actual behavior

Sanity script does not correct this

If possible, minimal yet complete reproducer code (or URL to code)

See #61 (Does not use return in a couple of places, did pass sanity script)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.