SwiftMetrics

用于 Swift 的 Metrics API 包。

几乎所有生产服务器软件都需要发出指标信息以进行可观察性。由于所有各方不太可能就一个特定的指标后端实现达成一致,因此此 API 旨在建立一个标准,该标准可以由各种指标库实现,然后将指标数据发布到后端,例如 PrometheusGraphite,通过 statsd 发布,写入磁盘等。

这是一个由社区驱动的开源项目的开始,积极寻求贡献,无论是代码、文档还是想法。除了贡献 SwiftMetrics 本身之外,我们还需要与指标兼容的库,这些库将指标发送到后端,例如上面提到的那些。 SwiftMetrics 今天提供的功能已在 API 文档中涵盖,但它将随着社区的投入而不断发展。

入门

如果您有一个服务器端 Swift 应用程序,或者可能是一个跨平台(例如 Linux、macOS)应用程序或库,并且您想发出指标,那么以这个指标 API 包为目标是一个好主意。下面您将找到入门所需的一切。

添加依赖项

要添加对 metrics API 包的依赖,您需要在 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"),

并将“Metrics”添加到您的应用程序/库目标的依赖项中

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

发出指标信息

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

选择指标后端实现(仅限应用程序)

注意:如果您正在构建库,则无需关心本节。 您的库的最终用户(应用程序)将决定使用哪个指标后端。 库永远不应更改指标实现,因为这是应用程序拥有的。

SwiftMetrics 仅提供指标系统 API。 作为应用程序所有者,您需要选择一个指标后端(例如上面提到的那些)以使指标信息有用。

选择后端是通过添加对所需后端客户端实现的依赖并在程序开始时调用 MetricsSystem.bootstrap 函数来完成的

MetricsSystem.bootstrap(SelectedMetricsImplementation())

这指示 MetricsSystem 安装 SelectedMetricsImplementation(实际名称会有所不同)作为要使用的指标后端。

由于 API 刚刚发布,因此还没有很多实现。如果您有兴趣实现一个,请参阅下面的“实现指标后端”部分,其中说明了如何实现。 现有与 SwiftMetrics API 兼容的库列表

Swift Metrics 扩展

您可能还会对 Swift Metrics 扩展存储库中收集的一些“额外”模块感兴趣。

详细设计

架构

我们认为,对于 Swift on Server 生态系统来说,拥有一个可以被任何人采用的指标 API 至关重要,因此来自不同方的众多库都可以提供指标信息。更具体地说,这意味着我们认为所有库的所有指标事件都应该最终出现在同一个地方,无论是上面提到的后端之一,还是应用程序所有者可能选择的任何其他地方。

在现实世界中,关于指标系统应该如何运行、如何聚合和计算指标以及在哪里/如何持久化指标,存在很多观点。我们认为等待一个指标包来支持特定部署所需的一切,同时仍然足够简单易用且保持高性能是不可行的。这就是为什么我们决定将问题分成两部分

  1. 指标 API
  2. 指标后端实现

此包仅提供指标 API 本身,因此 SwiftMetrics 是一个“指标 API 包”。 SwiftMetrics 可以配置(使用 MetricsSystem.bootstrap)以选择任何兼容的指标后端实现。这样,包可以采用 API,并且应用程序可以选择任何兼容的指标后端实现,而无需任何库进行任何更改。

此 API 由 Swift on Server 社区的贡献者设计,并经 SSWG(Swift Server Work Group)批准达到 SSWG 孵化过程的“沙箱级别”。

提案 | 讨论 | 反馈

指标类型

该 API 支持六种指标类型

Counter:计数器是一种累积指标,表示一个单调递增的计数器,其值只能增加或在重新启动时重置为零。 例如,您可以使用计数器来表示已服务的请求数、已完成的任务数或错误数。

counter.increment(by: 100)
floatingPointCounter.increment(by: 10.5)

Gauge:Gauge 是一种指标,表示可以任意上下浮动的单个数值。 Gauge 通常用于测量值,例如温度或当前内存使用情况,但也用于可以上下浮动的“计数”,例如活动线程数。 Gauge 被建模为一个样本大小为 1 的 Recorder,不执行任何聚合。

gauge.record(100)

Meter: Meter 类似于 Gauge - 一种指标,表示可以任意上下浮动的单个数值。 Meters 通常用于测量值,例如温度或当前内存使用情况,但也用于可以上下浮动的“计数”,例如活动线程数。 与 Gauge 不同,Meter 还支持原子递增和递减。

meter.record(100)

Recorder:记录器在时间窗口内收集观察结果(通常是响应大小之类的内容),并且可以提供有关数据样本的聚合信息,例如计数、总和、最小值、最大值和各种分位数。

recorder.record(100)

Timer:计时器在时间窗口内收集观察结果(通常是请求持续时间之类的内容),并提供有关数据样本的聚合信息,例如最小值、最大值和各种分位数。它类似于 Recorder,但专门用于表示持续时间的值。

timer.recordMilliseconds(100)

实现指标后端(例如 Prometheus 客户端库)

注意:除非您需要实现自定义指标后端,否则本节中的所有内容可能无关紧要,请随意跳过。

如上所示,CounterGaugeMeterRecorderTimer 的每个构造函数都提供一个指标对象。 这种不确定性有意掩盖了所选指标后端调用这些构造函数。 每个应用程序都可以选择和配置其所需的后端。 应用程序设置它希望使用的指标后端。 配置指标后端很简单

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

这指示 MetricsSystem 安装 MyFavoriteMetricsImplementation 作为要使用的指标后端 (MetricsFactory)。 这应该只在程序开始时完成一次。

鉴于上述情况,指标后端的实现需要符合 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)
}

MetricsFactory 负责实例化具体的指标类,这些类捕获指标并执行各种分位数的聚合和计算,根据需要。

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

处理溢出

处理整数的指标对象(如 CounterTimer)的实现应注意溢出。 预期的行为是限制在 .max,并且永远不会由于溢出而导致程序崩溃。 例如

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
        }
    }
}

完整示例

这是一个完整的,但人为的,内存实现的示例

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.md

参与其中

也请随时联系,请访问 https://forums.swift.org/c/server