Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement default process & runtime metrics #61

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8097604
Some of the linux process metrics
MrLotU Feb 15, 2020
69ba436
Some of these things are non Int32 on Linux
MrLotU Feb 15, 2020
ad29d87
Add logging
MrLotU Feb 15, 2020
24882ac
Add logging
MrLotU Feb 15, 2020
55970f5
Add logging
MrLotU Feb 15, 2020
e276789
Temp disable loop
MrLotU Feb 15, 2020
ce0ab69
Add logging
MrLotU Feb 15, 2020
928d094
Add logging
MrLotU Feb 15, 2020
dde317d
Add logging
MrLotU Feb 15, 2020
9a975d1
Add logging
MrLotU Feb 15, 2020
c6b8fba
cleanup
MrLotU Feb 15, 2020
3a8bb45
Address remarks
MrLotU Feb 21, 2020
027477f
Sanity script
MrLotU Feb 21, 2020
ffda628
Address most comments
MrLotU Mar 26, 2020
47cb74a
Merge branch 'master' into LotU-ProcessMetrics
MrLotU Mar 26, 2020
3f5de0e
Sanity
MrLotU Mar 26, 2020
286ab64
Move to Int
MrLotU Mar 26, 2020
1b3be71
Cleanup
MrLotU Mar 26, 2020
a072b5d
Remove Foundation dependency
MrLotU Apr 5, 2020
a044d14
Merge branch 'master' into LotU-ProcessMetrics
MrLotU Apr 5, 2020
00cc3b0
Address Tomerds comments
MrLotU May 22, 2020
0cf0d17
Merge branch 'master' into LotU-ProcessMetrics
MrLotU May 22, 2020
3f98a8f
Unsafe pointer updates
MrLotU May 22, 2020
78dcb03
Merge branch 'LotU-ProcessMetrics' of https://github.com/MrLotU/swift…
MrLotU May 22, 2020
9ab2612
Add #if check back in
MrLotU May 22, 2020
8ffec02
Review comments
MrLotU Jul 11, 2020
606163b
Review remarks
MrLotU Jul 16, 2020
71e992f
Rework to DispatchSourceTimer & fix tests
MrLotU Jul 23, 2020
85fd683
Remove magic numbers
MrLotU Jul 23, 2020
27bd191
Merge branch 'master' into LotU-ProcessMetrics
MrLotU Jul 23, 2020
5c82101
Sanity script
MrLotU Jul 23, 2020
fa82705
Merge branch 'LotU-ProcessMetrics' of https://github.com/MrLotU/swift…
MrLotU Jul 23, 2020
ead6f1d
Make Swift 5.0 work
MrLotU Jul 28, 2020
7193cef
Sanity
MrLotU Jul 28, 2020
a43cab2
Merge branch 'main' into LotU-ProcessMetrics
MrLotU Sep 18, 2020
3a6f1a1
Merge branch 'main' into LotU-ProcessMetrics
MrLotU Oct 9, 2020
9317fd1
Refactor SystemMetrics to a seperate module
MrLotU Nov 6, 2020
2da692d
Merge branch 'LotU-ProcessMetrics' of https://github.com/MrLotU/swift…
MrLotU Nov 6, 2020
cbb8a02
Merge branch 'main' into LotU-ProcessMetrics
MrLotU Nov 6, 2020
9c95321
Add back bootstrapWithSystemMetrics helper
MrLotU Nov 6, 2020
73baa5a
Merge branch 'LotU-ProcessMetrics' of https://github.com/MrLotU/swift…
MrLotU Nov 6, 2020
adacbf2
Cleanup
MrLotU Nov 6, 2020
8e9b22f
Refactor the way the locks are used
MrLotU Nov 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"]
),
]
)
16 changes: 16 additions & 0 deletions Sources/CoreMetrics/Metrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(_ 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<T>(_ 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.
Expand Down
347 changes: 347 additions & 0 deletions Sources/SystemMetrics/SystemMetrics.swift
Original file line number Diff line number Diff line change
@@ -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<Labels, String>) -> 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<FILE>?

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