Skip to content

Commit ca566ed

Browse files
authored
Implement default process & runtime metrics (#3)
* Implement system metrics * Update contributor list * Point to 2.1.1
1 parent 5a53a14 commit ca566ed

File tree

8 files changed

+426
-10
lines changed

8 files changed

+426
-10
lines changed

CONTRIBUTORS.txt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ needs to be listed here.
1111

1212
### Contributors
1313

14-
- Cory Benfield <[email protected]>
15-
- Jari (LotU) <[email protected]>
16-
- Konrad `ktoso` Malawski <[email protected]>
17-
- tomer doron <[email protected]>
14+
- Konrad `ktoso` Malawski <[email protected]>
15+
1816

1917
**Updating this list**
2018

Package.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
import PackageDescription
1717

1818
let package = Package(
19-
name: "swift-metrics",
19+
name: "swift-metrics-extras",
2020
products: [
2121
.library(name: "SystemMetrics", targets: ["SystemMetrics"]),
2222
],
23+
dependencies: [
24+
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.1.1"),
25+
],
2326
targets: [
2427
.target(
2528
name: "SystemMetrics",
26-
dependencies: []
29+
dependencies: ["CoreMetrics"]
2730
),
2831
.testTarget(
2932
name: "SystemMetricsTests",
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Metrics API open source project
4+
//
5+
// Copyright (c) 2018-2020 Apple Inc. and the Swift Metrics API project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import CoreMetrics
15+
import Dispatch
16+
17+
#if os(Linux)
18+
import Glibc
19+
#endif
20+
21+
extension MetricsSystem {
22+
fileprivate static var systemMetricsProvider: SystemMetricsProvider?
23+
24+
/// `bootstrapWithSystemMetrics` is an one-time configuration function which globally selects the desired metrics backend
25+
/// implementation, and enables system level metrics. `bootstrapWithSystemMetrics` can be called at maximum once in any given program,
26+
/// calling it more than once will lead to undefined behaviour, most likely a crash.
27+
///
28+
/// - parameters:
29+
/// - factory: A factory that given an identifier produces instances of metrics handlers such as `CounterHandler`, `RecorderHandler` and `TimerHandler`.
30+
/// - config: Used to configure `SystemMetrics`.
31+
public static func bootstrapWithSystemMetrics(_ factory: MetricsFactory, config: SystemMetrics.Configuration) {
32+
self.bootstrap(factory)
33+
self.bootstrapSystemMetrics(config)
34+
}
35+
36+
/// `bootstrapSystemMetrics` is an one-time configuration function which globally enables system level metrics.
37+
/// `bootstrapSystemMetrics` can be called at maximum once in any given program, calling it more than once will lead to
38+
/// undefined behaviour, most likely a crash.
39+
///
40+
/// - parameters:
41+
/// - config: Used to configure `SystemMetrics`.
42+
public static func bootstrapSystemMetrics(_ config: SystemMetrics.Configuration) {
43+
self.withWriterLock {
44+
precondition(self.systemMetricsProvider == nil, "System metrics already bootstrapped.")
45+
self.systemMetricsProvider = SystemMetricsProvider(config: config)
46+
}
47+
}
48+
49+
internal class SystemMetricsProvider {
50+
fileprivate let queue = DispatchQueue(label: "com.apple.CoreMetrics.SystemMetricsHandler", qos: .background)
51+
fileprivate let timeInterval: DispatchTimeInterval
52+
fileprivate let dataProvider: SystemMetrics.DataProvider
53+
fileprivate let labels: SystemMetrics.Labels
54+
fileprivate let timer: DispatchSourceTimer
55+
56+
init(config: SystemMetrics.Configuration) {
57+
self.timeInterval = config.interval
58+
self.dataProvider = config.dataProvider
59+
self.labels = config.labels
60+
self.timer = DispatchSource.makeTimerSource(queue: self.queue)
61+
62+
self.timer.setEventHandler(handler: DispatchWorkItem(block: { [weak self] in
63+
guard let self = self, let metrics = self.dataProvider() else { return }
64+
Gauge(label: self.labels.label(for: \.virtualMemoryBytes)).record(metrics.virtualMemoryBytes)
65+
Gauge(label: self.labels.label(for: \.residentMemoryBytes)).record(metrics.residentMemoryBytes)
66+
Gauge(label: self.labels.label(for: \.startTimeSeconds)).record(metrics.startTimeSeconds)
67+
Gauge(label: self.labels.label(for: \.cpuSecondsTotal)).record(metrics.cpuSeconds)
68+
Gauge(label: self.labels.label(for: \.maxFileDescriptors)).record(metrics.maxFileDescriptors)
69+
Gauge(label: self.labels.label(for: \.openFileDescriptors)).record(metrics.openFileDescriptors)
70+
}))
71+
72+
self.timer.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
73+
74+
if #available(OSX 10.12, *) {
75+
self.timer.activate()
76+
} else {
77+
self.timer.resume()
78+
}
79+
}
80+
81+
deinit {
82+
self.timer.cancel()
83+
}
84+
}
85+
}
86+
87+
public enum SystemMetrics {
88+
/// Provider used by `SystemMetrics` to get the requested `SystemMetrics.Data`.
89+
///
90+
/// Defaults are currently only provided for linux. (`SystemMetrics.linuxSystemMetrics`)
91+
public typealias DataProvider = () -> SystemMetrics.Data?
92+
93+
/// Configuration used to bootstrap `SystemMetrics`.
94+
///
95+
/// Backend implementations are encouraged to extend `SystemMetrics.Configuration` with a static extension with
96+
/// defaults that suit their specific backend needs.
97+
public struct Configuration {
98+
let interval: DispatchTimeInterval
99+
let dataProvider: SystemMetrics.DataProvider
100+
let labels: SystemMetrics.Labels
101+
102+
/// Create new instance of `SystemMetricsOptions`
103+
///
104+
/// - parameters:
105+
/// - pollInterval: The interval at which system metrics should be updated.
106+
/// - dataProvider: The provider to get SystemMetrics data from. If none is provided this defaults to
107+
/// `SystemMetrics.linuxSystemMetrics` on Linux platforms and `SystemMetrics.noopSystemMetrics`
108+
/// on all other platforms.
109+
/// - labels: The labels to use for generated system metrics.
110+
public init(pollInterval interval: DispatchTimeInterval = .seconds(2), dataProvider: SystemMetrics.DataProvider? = nil, labels: Labels) {
111+
self.interval = interval
112+
if let dataProvider = dataProvider {
113+
self.dataProvider = dataProvider
114+
} else {
115+
#if os(Linux)
116+
self.dataProvider = SystemMetrics.linuxSystemMetrics
117+
#else
118+
self.dataProvider = SystemMetrics.noopSystemMetrics
119+
#endif
120+
}
121+
self.labels = labels
122+
}
123+
}
124+
125+
/// Labels for the reported System Metrics Data.
126+
///
127+
/// Backend implementations are encouraged to provide a static extension with
128+
/// defaults that suit their specific backend needs.
129+
public struct Labels {
130+
/// Prefix to prefix all other labels with.
131+
let prefix: String
132+
/// Virtual memory size in bytes.
133+
let virtualMemoryBytes: String
134+
/// Resident memory size in bytes.
135+
let residentMemoryBytes: String
136+
/// Total user and system CPU time spent in seconds.
137+
let startTimeSeconds: String
138+
/// Total user and system CPU time spent in seconds.
139+
let cpuSecondsTotal: String
140+
/// Maximum number of open file descriptors.
141+
let maxFileDescriptors: String
142+
/// Number of open file descriptors.
143+
let openFileDescriptors: String
144+
145+
/// Create a new `Labels` instance.
146+
///
147+
/// - parameters:
148+
/// - prefix: Prefix to prefix all other labels with.
149+
/// - virtualMemoryBytes: Virtual memory size in bytes
150+
/// - residentMemoryBytes: Resident memory size in bytes.
151+
/// - startTimeSeconds: Total user and system CPU time spent in seconds.
152+
/// - cpuSecondsTotal: Total user and system CPU time spent in seconds.
153+
/// - maxFds: Maximum number of open file descriptors.
154+
/// - openFds: Number of open file descriptors.
155+
public init(prefix: String, virtualMemoryBytes: String, residentMemoryBytes: String, startTimeSeconds: String, cpuSecondsTotal: String, maxFds: String, openFds: String) {
156+
self.prefix = prefix
157+
self.virtualMemoryBytes = virtualMemoryBytes
158+
self.residentMemoryBytes = residentMemoryBytes
159+
self.startTimeSeconds = startTimeSeconds
160+
self.cpuSecondsTotal = cpuSecondsTotal
161+
self.maxFileDescriptors = maxFds
162+
self.openFileDescriptors = openFds
163+
}
164+
165+
func label(for keyPath: KeyPath<Labels, String>) -> String {
166+
return self.prefix + self[keyPath: keyPath]
167+
}
168+
}
169+
170+
/// System Metric data.
171+
///
172+
/// The current list of metrics exposed is taken from the Prometheus Client Library Guidelines
173+
/// https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors
174+
public struct Data {
175+
/// Virtual memory size in bytes.
176+
var virtualMemoryBytes: Int
177+
/// Resident memory size in bytes.
178+
var residentMemoryBytes: Int
179+
/// Start time of the process since unix epoch in seconds.
180+
var startTimeSeconds: Int
181+
/// Total user and system CPU time spent in seconds.
182+
var cpuSeconds: Int
183+
/// Maximum number of open file descriptors.
184+
var maxFileDescriptors: Int
185+
/// Number of open file descriptors.
186+
var openFileDescriptors: Int
187+
}
188+
189+
#if os(Linux)
190+
internal static func linuxSystemMetrics() -> SystemMetrics.Data? {
191+
class CFile {
192+
let path: String
193+
194+
private var file: UnsafeMutablePointer<FILE>?
195+
196+
init(_ path: String) {
197+
self.path = path
198+
}
199+
200+
deinit {
201+
assert(self.file == nil)
202+
}
203+
204+
func open() {
205+
guard let f = fopen(path, "r") else {
206+
return
207+
}
208+
self.file = f
209+
}
210+
211+
func close() {
212+
if let f = self.file {
213+
self.file = nil
214+
let success = fclose(f) == 0
215+
assert(success)
216+
}
217+
}
218+
219+
func readLine() -> String? {
220+
guard let f = self.file else {
221+
return nil
222+
}
223+
#if compiler(>=5.1)
224+
let buff: [CChar] = Array(unsafeUninitializedCapacity: 1024) { ptr, size in
225+
guard fgets(ptr.baseAddress, Int32(ptr.count), f) != nil else {
226+
if feof(f) != 0 {
227+
size = 0
228+
return
229+
} else {
230+
preconditionFailure("Error reading line")
231+
}
232+
}
233+
size = strlen(ptr.baseAddress!)
234+
}
235+
if buff.isEmpty { return nil }
236+
return String(cString: buff)
237+
#else
238+
var buff = [CChar](repeating: 0, count: 1024)
239+
let hasNewLine = buff.withUnsafeMutableBufferPointer { ptr -> Bool in
240+
guard fgets(ptr.baseAddress, Int32(ptr.count), f) != nil else {
241+
if feof(f) != 0 {
242+
return false
243+
} else {
244+
preconditionFailure("Error reading line")
245+
}
246+
}
247+
return true
248+
}
249+
if !hasNewLine {
250+
return nil
251+
}
252+
return String(cString: buff)
253+
#endif
254+
}
255+
256+
func readFull() -> String {
257+
var s = ""
258+
func loop() -> String {
259+
if let l = readLine() {
260+
s += l
261+
return loop()
262+
}
263+
return s
264+
}
265+
return loop()
266+
}
267+
}
268+
269+
let ticks = _SC_CLK_TCK
270+
271+
let file = CFile("/proc/self/stat")
272+
file.open()
273+
defer {
274+
file.close()
275+
}
276+
277+
enum StatIndices {
278+
static let virtualMemoryBytes = 20
279+
static let residentMemoryBytes = 21
280+
static let startTimeTicks = 19
281+
static let utimeTicks = 11
282+
static let stimeTicks = 12
283+
}
284+
285+
guard
286+
let statString = file.readFull()
287+
.split(separator: ")")
288+
.last
289+
else { return nil }
290+
let stats = String(statString)
291+
.split(separator: " ")
292+
.map(String.init)
293+
guard
294+
let virtualMemoryBytes = Int(stats[safe: StatIndices.virtualMemoryBytes]),
295+
let rss = Int(stats[safe: StatIndices.residentMemoryBytes]),
296+
let startTimeTicks = Int(stats[safe: StatIndices.startTimeTicks]),
297+
let utimeTicks = Int(stats[safe: StatIndices.utimeTicks]),
298+
let stimeTicks = Int(stats[safe: StatIndices.stimeTicks])
299+
else { return nil }
300+
let residentMemoryBytes = rss * _SC_PAGESIZE
301+
let startTimeSeconds = startTimeTicks / ticks
302+
let cpuSeconds = (utimeTicks / ticks) + (stimeTicks / ticks)
303+
304+
var _rlim = rlimit()
305+
306+
guard withUnsafeMutablePointer(to: &_rlim, { ptr in
307+
getrlimit(__rlimit_resource_t(RLIMIT_NOFILE.rawValue), ptr) == 0
308+
}) else { return nil }
309+
310+
let maxFileDescriptors = Int(_rlim.rlim_max)
311+
312+
guard let dir = opendir("/proc/self/fd") else { return nil }
313+
defer {
314+
closedir(dir)
315+
}
316+
var openFileDescriptors = 0
317+
while readdir(dir) != nil { openFileDescriptors += 1 }
318+
319+
return .init(
320+
virtualMemoryBytes: virtualMemoryBytes,
321+
residentMemoryBytes: residentMemoryBytes,
322+
startTimeSeconds: startTimeSeconds,
323+
cpuSeconds: cpuSeconds,
324+
maxFileDescriptors: maxFileDescriptors,
325+
openFileDescriptors: openFileDescriptors
326+
)
327+
}
328+
329+
#else
330+
#warning("System Metrics are not implemented on non-Linux platforms yet.")
331+
#endif
332+
333+
internal static func noopSystemMetrics() -> SystemMetrics.Data? {
334+
return nil
335+
}
336+
}
337+
338+
private extension Array where Element == String {
339+
subscript(safe index: Int) -> String {
340+
guard index >= 0, index < endIndex else {
341+
return ""
342+
}
343+
344+
return self[index]
345+
}
346+
}

Tests/LinuxMain.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift Metrics API open source project
44
//
5-
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
5+
// Copyright (c) 2018-2020 Apple Inc. and the Swift Metrics API project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -23,8 +23,9 @@ import XCTest
2323
///
2424

2525
#if os(Linux) || os(FreeBSD)
26-
@testable import MetricsTests
26+
@testable import SystemMetricsTests
2727

2828
XCTMain([
29+
testCase(SystemMetricsTest.allTests),
2930
])
3031
#endif

0 commit comments

Comments
 (0)