swift-metrics is a free, open source monitoring & observability project written in Swift and released under Apache-2.0. It has 798 GitHub stars, 83 forks and 8 open issues, and was last pushed 6 days ago. On this registry it ranks #239 of 270 tracked projects in Monitoring & Observability, with 5 head-to-head comparisons available. It gained 2 stars over the last 30 tracked days.

What is swift-metrics?

SwiftMetrics is the Apache-2.0 metrics API package for Swift, a shared abstraction that lets server-side and cross-platform Swift applications emit metrics for observability through whichever backend an application owner selects rather than tying emission code to a single library.

What it is

SwiftMetrics is an open-source Swift package published under the Apache-2.0 licence and maintained in the apple organization on GitHub. It lives in the Swift server ecosystem, tagged with the metrics and swift-server topics, and is distributed through the Swift Package Manager. The package provides only the metrics system API, not a backend: it defines a standard interface that various metrics libraries implement, and those implementations post the resulting data to backends such as Prometheus, Graphite, statsd, or to disk.

The concrete problem it solves is backend lock-in. Almost all production server software needs to emit metrics information, but it is unlikely that all parties can agree on one specific metrics backend implementation. Without a shared API, every application and library hard-wires itself to one vendor library, and swapping that library means touching call sites throughout the codebase. SwiftMetrics separates the two concerns. Libraries and applications target the API, and the application owner picks the backend at startup by invoking MetricsSystem.bootstrap. Libraries should never change the metrics implementation, since that choice belongs to the application. The project is community-driven and actively seeks contributions in code, documentation, and ideas, including additional metrics-compatible libraries that forward metrics to backends.

Key capabilities

  • Declares a dependency in Package.swift pointing at https://github.com/apple/swift-metrics.git, with a version range; swift-metrics 1.x and 2.x are almost API compatible, so most clients use a range rather than one pin.
  • Installs a backend with MetricsSystem.bootstrap(SelectedMetricsImplementation()) at the beginning of the program, after the application adds a dependency on the desired backend client.
  • Emits metrics from application code through the API, as shown by Counter(label: "users.created") and its increment() method.
  • Stays backend-agnostic: implementations publish over statsd, write to disk, or post to Prometheus or Graphite.
  • Supports testing with TestMetrics() and withMetricsFactory(_:), letting a suite assert metric values such as expectCounter("users.created").values == [1].
  • Scopes a metrics factory to a task-local block, so counters created inside the block use the test factory while counters created on demand outside it fall back to the global factory and fail if that global was never bootstrapped.
  • Keeps backend selection owned by applications, with libraries depending only on the API package.

Who uses it and how

  • Server-side Swift applications that need to emit metrics information for observability and want the backend decision deferred to deployment time.
  • Cross-platform applications and libraries running on Linux and macOS, which the README names explicitly as target environments.
  • Library authors who target the metrics API so that end users of the library retain control over which backend is installed.
  • Teams already standardized on Prometheus, which can adopt SwiftPrometheus, one of the listed SwiftMetrics-API-compatible libraries, as their backend implementation.
  • Test suites that swap in TestMetrics through withMetricsFactory to assert exact counter values instead of inspecting a live backend.

Getting started

Add the dependency to Package.swift using the repository URL https://github.com/apple/swift-metrics.git, then, if you are building an application rather than a library, add a backend client package and call MetricsSystem.bootstrap at the start of the program.

How it compares

The facts name no competing Swift metrics API, and no paid products are listed as being replaced. The only adjacent project named is SwiftPrometheus, which appears as a compatible backend implementation rather than a peer API, so swift-metrics stands alone in this registry as the abstraction layer, with the choice of backend deliberately left to the adopter.

When to use it — and when not to

Choose swift-metrics when building Swift software that should emit metrics without committing to one backend, and when the deployment environment will decide where those metrics land. It is an API package with no storage, no bundled backend, and no operational dependencies of its own — the README notes that not many implementations exist yet, so a team expecting a turnkey monitoring stack will still have to select and operate a backend such as Prometheus or Graphite, and the documentation covers the API rather than any hosting requirements. Library authors in particular should not bootstrap a backend, since that decision belongs to the application, and anyone needing an out-of-the-box exporter rather than a standard interface should look at an implementation package instead.

project readme (upstream, from github) — read inline

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

Correct metrics usage pattern

Metrics objects should be created once, with pre-defined labels and dimensions known at initialization time, and reused for the lifetime of the component. Creating new metric objects on every request or operation is an antipattern:

  • It can lead to unbounded memory allocation if the labels or dimensions are unbounded (e.g. per-request IDs), causing unbounded cardinality in the metrics backend.
  • It is slow and can become a bottleneck for fast parallel execution, since metric creation typically involves factory synchronization and backend registration.
// ❌ Creating metrics on demand — unbounded cardinality when dimensions vary per-request
func handleRequest(requestID: String) {
    let counter = Counter(label: "requests", dimensions: [("request_id", requestID)])
    counter.increment()
}

// ✅ Create metrics once during setup with fixed dimensions and reuse them
struct RequestHandler {
    let requestCounter = Counter(label: "requests")

    func handleRequest(requestID: String) {
        requestCounter.increment()
    }
}

When using a scoped factory override — such as withMetricsFactory(_:) for testing — the factory is only active for the duration of the closure. Any metrics created outside that scope will not see the overridden factory and will fall back to the global one. If no global factory has been bootstrapped, such metrics will fail to initialize, providing a safeguard against creating metrics outside of the designated setup scope.

struct UserService {
    let counter: Counter

    init() {
        // ✅ Created during init — picks up the task-local factory
        self.counter = Counter(label: "users.created")
    }

    func createUser(name: String) async throws -> User {
        // ❌ Created on demand — task-local factory is no longer in scope,
        //    falls back to global; fails if global is not bootstrapped
        let onDemandCounter = Counter(label: "users.created.on_demand")
        let user = User()
        self.counter.increment()
        return user
    }
}

@Test
func testUserCreation() async throws {
    let testMetrics = TestMetrics()

    // The task-local factory is only active inside this block
    let service = withMetricsFactory(testMetrics) {
        UserService()  // counter is created here — uses testMetrics
    }

    // service.createUser() runs outside the withMetricsFactory scope,
    // so onDemandCounter inside it will NOT use testMetrics
    _ = try await service.createUser(name: "Alice")

    #expect(try testMetrics.expectCounter("users.created").values == [1])
}

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:

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 six 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)
  • FloatingPointCounter: A variation of a Counter that records a floating point value, instead of an integer.
floatingPointCounter.increment(by: 10.5)

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 increments and decrements.

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)

Transforming labels and dimensions

When integrating metrics from multiple libraries or subsystems, you may want to add common dimensions (such as service name or environment) or rename labels consistently. MappingMetricsFactory wraps an existing MetricsFactory and applies a transformation to the label and dimensions of every metric before forwarding it to the upstream factory:

// Add a "service" dimension to all metrics created through this factory
let factory = myMetricsImplementation.withLabelAndDimensionsMapping { label, dimensions in
    (label, dimensions + [("service", "checkout")])
}
let counter = Counter(label: "request_count", dimensions: [("method", "GET")], factory: factory)

Transforms can also be chained:

let factory = myMetricsImplementation
    .withLabelAndDimensionsMapping { label, dimensions in
        ("myapp.\(label)", dimensions)
    }
    .withLabelAndDimensionsMapping { label, dimensions in
        (label, dimensions + [("env", "production")])
    }

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 the security process 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

Related Libraries

swift-system-metrics uses the Metrics API to emit system resource metrics such as CPU, memory, and file descriptors, providing insight into your application's resource consumption.

Frequently asked questions

Is swift-metrics free to use?

swift-metrics is open source under the Apache-2.0 licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does swift-metrics do?

Metrics API for Swift

What is swift-metrics written in?

swift-metrics is primarily written in Swift. Its source is publicly available at https://github.com/apple/swift-metrics, and it has 798 GitHub stars.