diff --git a/Package.swift b/Package.swift index 5eeb8dc..76c2ff9 100644 --- a/Package.swift +++ b/Package.swift @@ -20,6 +20,7 @@ let package = Package( products: [ .library(name: "CoreMetrics", targets: ["CoreMetrics"]), .library(name: "Metrics", targets: ["Metrics"]), + .library(name: "SystemMetrics", targets: ["SystemMetrics"]), ], targets: [ .target( @@ -30,9 +31,17 @@ let package = Package( name: "Metrics", dependencies: ["CoreMetrics"] ), + .target( + name: "SystemMetrics", + dependencies: ["CoreMetrics"] + ), .testTarget( name: "MetricsTests", dependencies: ["Metrics"] ), + .testTarget( + name: "SystemMetricsTests", + dependencies: ["SystemMetrics"] + ), ] ) diff --git a/Sources/CoreMetrics/Metrics.swift b/Sources/CoreMetrics/Metrics.swift index de48f14..5750f68 100644 --- a/Sources/CoreMetrics/Metrics.swift +++ b/Sources/CoreMetrics/Metrics.swift @@ -382,6 +382,22 @@ public enum MetricsSystem { fileprivate static var _factory: MetricsFactory = NOOPMetricsHandler.instance fileprivate static var initialized = false + /// Acquire the writer lock for the duration of the given block. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + public static func withWriterLock(_ body: () throws -> T) rethrows -> T { + return try self.lock.withWriterLock(body) + } + + /// Acquire the reader lock for the duration of the given block. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + public static func withReaderLock(_ body: () throws -> T) rethrows -> T { + return try self.lock.withReaderLock(body) + } + /// `bootstrap` is an one-time configuration function which globally selects the desired metrics backend /// implementation. `bootstrap` can be called at maximum once in any given program, calling it more than once will /// lead to undefined behaviour, most likely a crash. diff --git a/Sources/SystemMetrics/SystemMetrics.swift b/Sources/SystemMetrics/SystemMetrics.swift new file mode 100644 index 0000000..b777654 --- /dev/null +++ b/Sources/SystemMetrics/SystemMetrics.swift @@ -0,0 +1,347 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Metrics API open source project +// +// Copyright (c) 2018-2020 Apple Inc. and the Swift Metrics API project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CoreMetrics +import Dispatch + +#if os(Linux) +import Glibc +#endif + +extension MetricsSystem { + fileprivate static var systemMetricsProvider: SystemMetricsProvider? + + /// `bootstrapWithSystemMetrics` is an one-time configuration function which globally selects the desired metrics backend + /// implementation, and enables system level metrics. `bootstrapWithSystemMetrics` can be called at maximum once in any given program, + /// calling it more than once will lead to undefined behaviour, most likely a crash. + /// + /// - parameters: + /// - factory: A factory that given an identifier produces instances of metrics handlers such as `CounterHandler`, `RecorderHandler` and `TimerHandler`. + /// - config: Used to configure `SystemMetrics`. + public static func bootstrapWithSystemMetrics(_ factory: MetricsFactory, config: SystemMetrics.Configuration) { + self.bootstrap(factory) + self.bootstrapSystemMetrics(config) + } + + /// `bootstrapSystemMetrics` is an one-time configuration function which globally enables system level metrics. + /// `bootstrapSystemMetrics` can be called at maximum once in any given program, calling it more than once will lead to + /// undefined behaviour, most likely a crash. + /// + /// - parameters: + /// - config: Used to configure `SystemMetrics`. + public static func bootstrapSystemMetrics(_ config: SystemMetrics.Configuration) { + self.withWriterLock { + precondition(self.systemMetricsProvider == nil, "System metrics already bootstrapped.") + self.systemMetricsProvider = SystemMetricsProvider(config: config) + } + } + + internal class SystemMetricsProvider { + fileprivate let queue = DispatchQueue(label: "com.apple.CoreMetrics.SystemMetricsHandler", qos: .background) + fileprivate let timeInterval: DispatchTimeInterval + fileprivate let dataProvider: SystemMetrics.DataProvider + fileprivate let labels: SystemMetrics.Labels + fileprivate let timer: DispatchSourceTimer + + init(config: SystemMetrics.Configuration) { + self.timeInterval = config.interval + self.dataProvider = config.dataProvider + self.labels = config.labels + self.timer = DispatchSource.makeTimerSource(queue: self.queue) + + self.timer.setEventHandler(handler: DispatchWorkItem(block: { [weak self] in + guard let self = self, let metrics = self.dataProvider() else { return } + Gauge(label: self.labels.label(for: \.virtualMemoryBytes)).record(metrics.virtualMemoryBytes) + Gauge(label: self.labels.label(for: \.residentMemoryBytes)).record(metrics.residentMemoryBytes) + Gauge(label: self.labels.label(for: \.startTimeSeconds)).record(metrics.startTimeSeconds) + Gauge(label: self.labels.label(for: \.cpuSecondsTotal)).record(metrics.cpuSeconds) + Gauge(label: self.labels.label(for: \.maxFileDescriptors)).record(metrics.maxFileDescriptors) + Gauge(label: self.labels.label(for: \.openFileDescriptors)).record(metrics.openFileDescriptors) + })) + + self.timer.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval) + + if #available(OSX 10.12, *) { + self.timer.activate() + } else { + self.timer.resume() + } + } + + deinit { + self.timer.cancel() + } + } +} + +public enum SystemMetrics { + /// Provider used by `SystemMetrics` to get the requested `SystemMetrics.Data`. + /// + /// Defaults are currently only provided for linux. (`SystemMetrics.linuxSystemMetrics`) + public typealias DataProvider = () -> SystemMetrics.Data? + + /// Configuration used to bootstrap `SystemMetrics`. + /// + /// Backend implementations are encouraged to extend `SystemMetrics.Configuration` with a static extension with + /// defaults that suit their specific backend needs. + public struct Configuration { + let interval: DispatchTimeInterval + let dataProvider: SystemMetrics.DataProvider + let labels: SystemMetrics.Labels + + /// Create new instance of `SystemMetricsOptions` + /// + /// - parameters: + /// - pollInterval: The interval at which system metrics should be updated. + /// - dataProvider: The provider to get SystemMetrics data from. If none is provided this defaults to + /// `SystemMetrics.linuxSystemMetrics` on Linux platforms and `SystemMetrics.noopSystemMetrics` + /// on all other platforms. + /// - labels: The labels to use for generated system metrics. + public init(pollInterval interval: DispatchTimeInterval = .seconds(2), dataProvider: SystemMetrics.DataProvider? = nil, labels: Labels) { + self.interval = interval + if let dataProvider = dataProvider { + self.dataProvider = dataProvider + } else { + #if os(Linux) + self.dataProvider = SystemMetrics.linuxSystemMetrics + #else + self.dataProvider = SystemMetrics.noopSystemMetrics + #endif + } + self.labels = labels + } + } + + /// Labels for the reported System Metrics Data. + /// + /// Backend implementations are encouraged to provide a static extension with + /// defaults that suit their specific backend needs. + public struct Labels { + /// Prefix to prefix all other labels with. + let prefix: String + /// Virtual memory size in bytes. + let virtualMemoryBytes: String + /// Resident memory size in bytes. + let residentMemoryBytes: String + /// Total user and system CPU time spent in seconds. + let startTimeSeconds: String + /// Total user and system CPU time spent in seconds. + let cpuSecondsTotal: String + /// Maximum number of open file descriptors. + let maxFileDescriptors: String + /// Number of open file descriptors. + let openFileDescriptors: String + + /// Create a new `Labels` instance. + /// + /// - parameters: + /// - prefix: Prefix to prefix all other labels with. + /// - virtualMemoryBytes: Virtual memory size in bytes + /// - residentMemoryBytes: Resident memory size in bytes. + /// - startTimeSeconds: Total user and system CPU time spent in seconds. + /// - cpuSecondsTotal: Total user and system CPU time spent in seconds. + /// - maxFds: Maximum number of open file descriptors. + /// - openFds: Number of open file descriptors. + public init(prefix: String, virtualMemoryBytes: String, residentMemoryBytes: String, startTimeSeconds: String, cpuSecondsTotal: String, maxFds: String, openFds: String) { + self.prefix = prefix + self.virtualMemoryBytes = virtualMemoryBytes + self.residentMemoryBytes = residentMemoryBytes + self.startTimeSeconds = startTimeSeconds + self.cpuSecondsTotal = cpuSecondsTotal + self.maxFileDescriptors = maxFds + self.openFileDescriptors = openFds + } + + func label(for keyPath: KeyPath) -> String { + return self.prefix + self[keyPath: keyPath] + } + } + + /// System Metric data. + /// + /// The current list of metrics exposed is taken from the Prometheus Client Library Guidelines + /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors + public struct Data { + /// Virtual memory size in bytes. + var virtualMemoryBytes: Int + /// Resident memory size in bytes. + var residentMemoryBytes: Int + /// Start time of the process since unix epoch in seconds. + var startTimeSeconds: Int + /// Total user and system CPU time spent in seconds. + var cpuSeconds: Int + /// Maximum number of open file descriptors. + var maxFileDescriptors: Int + /// Number of open file descriptors. + var openFileDescriptors: Int + } + + #if os(Linux) + internal static func linuxSystemMetrics() -> SystemMetrics.Data? { + class CFile { + let path: String + + private var file: UnsafeMutablePointer? + + init(_ path: String) { + self.path = path + } + + deinit { + assert(self.file == nil) + } + + func open() { + guard let f = fopen(path, "r") else { + return + } + self.file = f + } + + func close() { + if let f = self.file { + self.file = nil + let success = fclose(f) == 0 + assert(success) + } + } + + func readLine() -> String? { + guard let f = self.file else { + return nil + } + #if compiler(>=5.1) + let buff: [CChar] = Array(unsafeUninitializedCapacity: 1024) { ptr, size in + guard fgets(ptr.baseAddress, Int32(ptr.count), f) != nil else { + if feof(f) != 0 { + size = 0 + return + } else { + preconditionFailure("Error reading line") + } + } + size = strlen(ptr.baseAddress!) + } + if buff.isEmpty { return nil } + return String(cString: buff) + #else + var buff = [CChar](repeating: 0, count: 1024) + let hasNewLine = buff.withUnsafeMutableBufferPointer { ptr -> Bool in + guard fgets(ptr.baseAddress, Int32(ptr.count), f) != nil else { + if feof(f) != 0 { + return false + } else { + preconditionFailure("Error reading line") + } + } + return true + } + if !hasNewLine { + return nil + } + return String(cString: buff) + #endif + } + + func readFull() -> String { + var s = "" + func loop() -> String { + if let l = readLine() { + s += l + return loop() + } + return s + } + return loop() + } + } + + let ticks = _SC_CLK_TCK + + let file = CFile("/proc/self/stat") + file.open() + defer { + file.close() + } + + enum StatIndices { + static let virtualMemoryBytes = 20 + static let residentMemoryBytes = 21 + static let startTimeTicks = 19 + static let utimeTicks = 11 + static let stimeTicks = 12 + } + + guard + let statString = file.readFull() + .split(separator: ")") + .last + else { return nil } + let stats = String(statString) + .split(separator: " ") + .map(String.init) + guard + let virtualMemoryBytes = Int(stats[safe: StatIndices.virtualMemoryBytes]), + let rss = Int(stats[safe: StatIndices.residentMemoryBytes]), + let startTimeTicks = Int(stats[safe: StatIndices.startTimeTicks]), + let utimeTicks = Int(stats[safe: StatIndices.utimeTicks]), + let stimeTicks = Int(stats[safe: StatIndices.stimeTicks]) + else { return nil } + let residentMemoryBytes = rss * _SC_PAGESIZE + let startTimeSeconds = startTimeTicks / ticks + let cpuSeconds = (utimeTicks / ticks) + (stimeTicks / ticks) + + var _rlim = rlimit() + + guard withUnsafeMutablePointer(to: &_rlim, { ptr in + getrlimit(__rlimit_resource_t(RLIMIT_NOFILE.rawValue), ptr) == 0 + }) else { return nil } + + let maxFileDescriptors = Int(_rlim.rlim_max) + + guard let dir = opendir("/proc/self/fd") else { return nil } + defer { + closedir(dir) + } + var openFileDescriptors = 0 + while readdir(dir) != nil { openFileDescriptors += 1 } + + return .init( + virtualMemoryBytes: virtualMemoryBytes, + residentMemoryBytes: residentMemoryBytes, + startTimeSeconds: startTimeSeconds, + cpuSeconds: cpuSeconds, + maxFileDescriptors: maxFileDescriptors, + openFileDescriptors: openFileDescriptors + ) + } + + #else + #warning("System Metrics are not implemented on non-Linux platforms yet.") + #endif + + internal static func noopSystemMetrics() -> SystemMetrics.Data? { + return nil + } +} + +private extension Array where Element == String { + subscript(safe index: Int) -> String { + guard index >= 0, index < endIndex else { + return "" + } + + return self[index] + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 50ad1ac..6dfbe0e 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -24,9 +24,11 @@ import XCTest #if os(Linux) || os(FreeBSD) @testable import MetricsTests +@testable import SystemMetricsTests XCTMain([ testCase(MetricsExtensionsTests.allTests), testCase(MetricsTests.allTests), + testCase(SystemMetricsTest.allTests), ]) #endif diff --git a/Tests/SystemMetricsTests/SystemMetricsTests+XCTest.swift b/Tests/SystemMetricsTests/SystemMetricsTests+XCTest.swift new file mode 100644 index 0000000..2c3f6a6 --- /dev/null +++ b/Tests/SystemMetricsTests/SystemMetricsTests+XCTest.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Metrics API open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// SystemMetricsTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension SystemMetricsTest { + static var allTests: [(String, (SystemMetricsTest) -> () throws -> Void)] { + return [ + ("testSystemMetricsGeneration", testSystemMetricsGeneration), + ] + } +} diff --git a/Tests/SystemMetricsTests/SystemMetricsTests.swift b/Tests/SystemMetricsTests/SystemMetricsTests.swift new file mode 100644 index 0000000..13db0d1 --- /dev/null +++ b/Tests/SystemMetricsTests/SystemMetricsTests.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Metrics API open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import SystemMetrics +import XCTest +#if os(Linux) +import Glibc +#endif + +class SystemMetricsTest: XCTestCase { + func testSystemMetricsGeneration() throws { + #if os(Linux) + let _metrics = SystemMetrics.linuxSystemMetrics() + #else + let _metrics = SystemMetrics.noopSystemMetrics() + throw XCTSkip() + #endif + XCTAssertNotNil(_metrics) + let metrics = _metrics! + XCTAssertNotNil(metrics.virtualMemoryBytes) + XCTAssertNotNil(metrics.residentMemoryBytes) + XCTAssertNotNil(metrics.startTimeSeconds) + XCTAssertNotNil(metrics.cpuSeconds) + XCTAssertNotNil(metrics.maxFileDescriptors) + XCTAssertNotNil(metrics.openFileDescriptors) + } +} diff --git a/scripts/sanity.sh b/scripts/sanity.sh index 6e73704..92e83ad 100755 --- a/scripts/sanity.sh +++ b/scripts/sanity.sh @@ -32,7 +32,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/2018-2019/YEARS/' -e 's/2019/YEARS/' + sed -e 's/2018-2019/YEARS/' -e 's/2019/YEARS/' -e 's/2018-2020/YEARS/' } printf "=> Checking linux tests... "