SwiftMetrics
Swift 的 Metrics API 包。
几乎所有生产级别的服务器软件都需要提供指标信息以满足可观测性。由于不太可能所有参与者都会就一种特定的指标后端实现达成一致,因此该 API 被设计为建立一个标准,该标准可以由各种指标库实现,然后发送指标数据到后端,例如 Prometheus、Graphite、通过 statsd 发布、写入磁盘等。
这是一个由社区驱动的开源项目,积极寻求贡献,无论是代码、文档还是想法。除了向 SwiftMetrics 本身做贡献之外,我们还需要度量兼容的库,这些库可以将度量数据发送到后端,如上述提到的那些。SwiftMetrics 今天提供的功能已在 API 文档 中介绍,但它会随着社区的反馈不断发展。
入门
如果您有一个服务器端 Swift 应用程序,或者一个跨平台(例如 Linux、macOS)的应用程序或库,并且您希望生成指标,那么在 metrics API 包上定位是一个很好的主意。下面您将了解到如何开始。
添加依赖
要添加对指标 API 包的依赖,您需要在您的 Package.swift
中声明它
// As of May 5, 2019, SwiftMetrics' major stable release is 1.0.0
// To depend on this release, you can use
.package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"),
并将 "Metrics" 添加到您的应用程序/库目标中。
.target(name: "BestExampleApp", dependencies: ["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
(实际名称将不同)安装为要使用的指标后端。
现有与SwiftMetrics API兼容的库列表
- SwiftPrometheus,支持Prometheus
- 您的库?取得联系!
详细设计
架构
我们认为,对于Swift on Server生态系统,拥有一个可以被任何人采用的指标API至关重要,因此来自不同方的大量库都可以提供指标信息。更具体地说,这意味着我们认为来自所有库的所有指标事件都应该最终进入同一个地方,无论是上述提到的后端之一,还是应用程序所有者可能选择的任何其他地方。
在真实世界中,人们对如何精确地表现度量系统、如何聚合和计算度量以及在哪里/如何持久化度量有着许多不同的看法。我们认为在保持足够简单易用和性能的情况下,等待一个度量包支持特定部署所需的所有功能是不可行的。这就是我们决定将问题分为两个部分的原因
- 一个度量API
- 一个度量后端实现
本软件包仅提供度量API自身,因此,SwiftMetrics是一个“度量API软件包”。SwiftMetrics可以通过MetricsSystem.bootstrap
进行配置来选择任何兼容的度量后端实现。这样,软件包可以采用API,应用可以选择任何兼容的度量后端实现,而不需要修改任何库。
该API是在Swift on Server社区贡献者協助下设计的,并已获得SWG(Swift服务器工作组)的批准,达到SWG孵化流程的“沙盒”级别。
度量类型
API支持四种度量类型
Counter
:Counter是一个累积度量,表示唯一一个单调递增的计数器,其值只能增加或在重启时重置为零。例如,你可以使用Counter表示被服务请求数量、完成的任务数或错误。
counter.increment(by: 100)
Recorder
:Recorder在一个时间窗口内收集观察值(通常是响应大小等),并可以提供有关数据样本的汇总信息,例如计数、求和、最小值、最大值和各种分位数。
recorder.record(100)
Gauge
:Gauge是一个代表单个数值的度量,该数值可以任意增减。Gauge通常用于表示温度或当前内存使用等测量值,也可以表示像活动线程数这样的可增减“计数”。Gauge以不具有聚合功能的单个样本大小的Recorder
来建模。
gauge.record(100)
Timer
:Timer在一个时间窗口内收集观察值(通常是请求持续时间等),并提供了关于数据样本的汇总信息,例如最小值、最大值和各种分位数。它与Recorder
类似,但专门针对表示持续时间等值的值。
timer.recordMilliseconds(100)
实现度量后端(例如Prometheus客户端库)
注意:除非你需要实现自定义度量后端,本节的所有内容可能都不相关,因此请自由跳过。
如上图所示,每个Counter
、Timer
、Recorder
和Gauge
构造函数都提供了度量对象。这种不确定性是设计来隐藏通过这些构造函数调用的所选度量后端的。每个应用都可以选择和配置其所需的后端。应用需要设置它想使用的度量后端。配置度量后端是简单明了的
let metricsImplementation = MyFavoriteMetricsImplementation()
MetricsSystem.bootstrap(metricsImplementation)
这指示 MetricsSystem
将 MyFavoriteMetricsImplementation
安装为度量后端(MetricsFactory
)以供使用。这应该只在程序的开始处执行一次。
鉴于上述情况,度量后端的实现需要符合 协议 MetricsFactory
。
public protocol MetricsFactory {
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
func destroyCounter(_ handler: CounterHandler)
func destroyRecorder(_ handler: RecorderHandler)
func destroyTimer(_ handler: TimerHandler)
}
MetricsFactory
负责实例化捕获度量并按需执行各种分位数聚合和计算的具体度量类。
计数器
public protocol CounterHandler: AnyObject {
func increment(by: Int64)
func reset()
}
计时器
public protocol TimerHandler: AnyObject {
func recordNanoseconds(_ duration: Int64)
}
记录器
public protocol RecorderHandler: AnyObject {
func record(_ value: Int64)
func record(_ value: Double)
}
处理溢出
处理整数类型的度量对象实现,例如 Counter
和 Timer
应小心处理溢出。预期的行为是上限为 .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 makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let maker: (String, [(String, String)]) -> RecorderHandler = aggregate ? ExampleRecorder.init : ExampleGauge.init
return maker(label, dimensions)
}
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 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 ExampleRecorder: RecorderHandler {
init(_: String, _: [(String, String)]) {}
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 ExampleGauge: RecorderHandler {
init(_: String, _: [(String, String)]) {}
let lock = NSLock()
var _value: Double = 0
func record(_ value: Int64) {
self.record(Double(value))
}
func record(_ value: Double) {
self.lock.withLock { _value = value }
}
}
private class ExampleTimer: ExampleRecorder, TimerHandler {
func recordNanoseconds(_ duration: Int64) {
super.record(duration)
}
}
}
同时,也欢迎您通过https://forums.swift.org/c/server与我们联系。