prepare to release (#1)

* prepare to release

motivation: the sswg voted to adopt the API. this is to prepare to a release

changes:
* rewrite readme
* add API docs
* add utilitiy scripts and docker setup for CI
* adjust linux tests
This commit is contained in:
tomer doron 2019-04-08 18:58:55 -07:00 committed by GitHub
parent 5c1c739969
commit 585a41d684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1010 additions and 881 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/Packages
/*.xcodeproj
.xcode
.SourceKitten

View File

@ -1,2 +1,2 @@
Tomer Doron <tomer@apple.com> <tomerd@apple.com> <tomer.doron@gmail.com>
tomer doron <tomer@apple.com> <tomerd@apple.com> <tomer.doron@gmail.com>
Konrad `ktoso` Malawski <ktoso@apple.com> <konrad.malawski@project13.pl>

View File

@ -12,8 +12,9 @@ needs to be listed here.
### Contributors
- Cory Benfield <lukasa@apple.com>
- Jari (LotU) <j.koopman@jarict.nl>
- Konrad `ktoso` Malawski <ktoso@apple.com>
- Tomer Doron <tomer@apple.com>
- tomer doron <tomer@apple.com>
**Updating this list**

View File

@ -17,10 +17,6 @@ let package = Package(
name: "Metrics",
dependencies: ["CoreMetrics"]
),
.target(
name: "Examples",
dependencies: ["Metrics"]
),
.testTarget(
name: "MetricsTests",
dependencies: ["Metrics"]

130
README.md
View File

@ -1,21 +1,80 @@
# SSWG Metrics api
# swift-metrics
* Proposal: SSWG-xxxx
* Authors: [Tomer Doron](https://github.com/tomerd)
* Status: **Implemented**
* Pitch: [Server: Pitches/Metrics](https://forums.swift.org/t/metrics)
A Metrics API package for Swift.
## Introduction
Almost all production server software needs to emit metrics information for observability. Because it's unlikely that all parties can agree on one specific metrics backend implementation, this API is designed to establish a standard that can be implemented by various metrics libraries which then post the metrics data to backends like [Prometheus](http://prometheus.io/), [Grafana](http://grafana.com/), publish over [statsd](https://github.com/statsd/statsd), write to disk, etc.
Almost all production server software needs to emit metrics information for observability. The SSWG aims to provide a number of packages that can be shared across the whole Swift on Server ecosystem so we need some amount of standardisation. Because it's unlikely that all parties can agree on one full metrics implementation, this proposal is attempting to establish a metrics API that can be implemented by various metrics backends which then post the metrics data to backends like prometheus, graphite, publish over statsd, write to disk, etc.
This is the beginning of a community-driven open-source project actively seeking contributions, be it code, documentation, or ideas. Apart from contributing to swift-metrics itself, we need metrics compatible libraries which send the metrics over to backend such as the ones mentioned above.
## Motivation
What swift-metrics provides today is covered in the [API docs](https://apple.github.io/swift-metrics/). At this moment, we have not tagged a version for swift-metrics, but we will do so soon.
As outlined above we should standardise on an API that if well adopted would allow application owners to mix and match libraries from different vendors with a consistent metrics solution.
## Getting started
## Proposed solution
If you have a server-side Swift application, or maybe a cross-platform (e.g. Linux, macOS) application or library, and you would like to emit metrics, targeting this metrics API package is a great idea. Below you'll find all you need to know to get started.
The proposed solution is to introduce the following types that encapsulate metrics data:
### Adding the dependency
To add a dependency on the metrics API package, you need to declare it in your `Package.swift`:
```swift
// it's early days here so we haven't tagged a version yet, but will soon
.package(url: "https://github.com/apple/swift-metrics.git", .branch("master")),
```
and to your application/library target, add "Metrics" to your dependencies:
```swift
.target(name: "BestExampleApp", dependencies: ["Metrics"]),
```
### Emitting metrics information
```swift
// 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()
```
### Selecting a metrics backend implementation (applications only)
Note: If you are building a library, you don't need to concern yourself with this section. It is the end users of your library (the applications) who will decide which metrics backend to use. Libraries should never change the metrics implementation as that is something owned by the application.
swift-metrics only provides the metrics system API. As an application owner, you need to select a metrics backend (such as the ones mentioned above) to make the metrics information useful.
Selecting a backend is done by adding a dependency on the desired backend client implementation and invoking `MetricsSystem.bootstrap(SelectedMetricsImplementation.init)` at the beginning of the program. This instructs the `MetricsSystem` to install `SelectedMetricsImplementation` (actual name will differ) as the metrics backend to use.
As the API has just launched, not many implementations exist yet. If you are interested in implementing one see the "Implementing a metrics backend" section below explaining how to do so. List of existing swift-metrics API compatible libraries:
- Your library? [Get in touch!](https://forums.swift.org/c/server)
## Detailed design
### Architecture
We believe that for the Swift on Server ecosystem, it's crucial to have a metrics API that can be adopted by anybody so a multitude of libraries from different parties can all provide metrics information. More concretely this means that we believe all the metrics events from all libraries should end up in the same place, be one of the backend mentioned above or wherever else the application owner may choose.
In the real-world there are so many opinions over how exactly a metrics system should behave, how metrics should be aggregated and calculated, and where/how they should be persisted. We think it's not feasible to wait for one metrics package to support everything that a specific deployment needs whilst still being easy enough to use and remain performant. That's why we decided to split the problem into two:
1. a metrics API
2. a metrics backend implementation
This package only provides the metrics API itself and therefore swift-metrics is a "metrics API package". swift-metrics (using `MetricsSystem.bootstrap`) can be configured to choose any compatible metrics backend implementation. This way packages can adopt the API and the application can choose any compatible metrics backend implementation without requiring any changes from any of the libraries.
This API was designed with the contributors to the Swift on Server community and approved by the SSWG (Swift Server Work Group) to the "sandbox level" of the SSWG's incubation process.
[pitch](https://forums.swift.org/t/metrics/19353) |
[discussion](https://forums.swift.org/t/discussion-server-metrics-api/) |
[feedback](https://forums.swift.org/t/feedback-server-metrics-api/)
### Metric types
The API supports four metric types:
`Counter`: A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart. For example, you can use a counter to represent the number of requests served, tasks completed, or errors.
@ -41,36 +100,17 @@ gauge.record(100)
timer.recordMilliseconds(100)
```
How would you use `counter`, `recorder`, `gauge` and `timer` in you application or library? Here is a contrived example for request processing code that emits metrics for: total request count per url, request size and duration and response size:
### Implementing a metrics backend (e.g. Prometheus client library)
Note: Unless you need to implement a custom metrics backend, everything in this section is likely not relevant, so please feel free to skip.
As seen above, each of `Counter`, `Timer`, `Recorder` and `Gauge` constructors provides a metric object. This raises the question of which metrics backend is actually be used when calling these constructors? The answer is that it's configurable _per application_. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
```swift
func processRequest(request: Request) -> Response {
let requestCounter = Counter("request.count", ["url": request.url])
let requestTimer = Timer("request.duration", ["url": request.url])
let requestSizeRecorder = Recorder("request.size", ["url": request.url])
let responseSizeRecorder = Recorder("response.size", ["url": request.url])
requestCounter.increment()
requestSizeRecorder.record(request.size)
let start = Date()
let response = ...
requestTimer.record(Date().timeIntervalSince(start))
responseSizeRecorder.record(response.size)
}
MetricsSystem.bootstrap(MyFavoriteMetricsImplementation.init)
```
## Detailed design
### Implementing a metrics backend (e.g. prometheus client library)
As seen above, the constructors `Counter`, `Timer`, `Recorder` and `Gauge` provides a metric object. This raises the question of what metrics backend I will actually get when calling these constructors? The answer is that it's configurable _per application_. The application sets up the metrics backend it wishes the whole application to use. Libraries should never change the metrics implementation as that is something owned by the application. Configuring the metrics backend is straightforward:
```swift
MetricsSystem.bootstrap(MyFavouriteMetricsImplementation.init)
```
This instructs the `MetricsSystem` to install `MyFavouriteMetricsImplementation` as the metrics backend (`MetricsFactory`) to use. This should only be done once at the beginning of the program.
This instructs the `MetricsSystem` to install `MyFavoriteMetricsImplementation` as the metrics backend (`MetricsFactory`) to use. This should only be done once at the beginning of the program.
Given the above, an implementation of a metric backend needs to conform to `protocol MetricsFactory`:
@ -209,18 +249,4 @@ class SimpleMetricsLibrary: MetricsFactory {
}
```
## State
This is an early proposal so there are still plenty of things to decide and tweak and I'd invite everybody to participate.
### Feedback Wishes
Feedback that would really be great is:
- if anything, what does this proposal *not cover* that you will definitely need
- if anything, what could we remove from this and still be happy?
- API-wise: what do you like, what don't you like?
Feel free to post this as message on the SSWG forum and/or github issues in this repo.
### Open Questions
Do not hesitate to get in touch as well, over on https://forums.swift.org/c/server

View File

@ -49,7 +49,7 @@ internal final class Lock {
/// Release the lock.
///
/// Whenver possible, consider using `withLock` instead of this method and
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
public func unlock() {
let err = pthread_mutex_unlock(self.mutex)
@ -67,7 +67,7 @@ extension Lock {
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
public func withLock<T>(_ body: () throws -> T) rethrows -> T {
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
self.lock()
defer {
self.unlock()
@ -77,7 +77,7 @@ extension Lock {
// specialise Void return (for performance)
@inlinable
public func withLockVoid(_ body: () throws -> Void) rethrows {
internal func withLockVoid(_ body: () throws -> Void) rethrows {
try self.withLock(body)
}
}
@ -122,7 +122,7 @@ internal final class ReadWriteLock {
/// Release the lock.
///
/// Whenver possible, consider using `withLock` instead of this method and
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
public func unlock() {
let err = pthread_rwlock_unlock(self.rwlock)
@ -140,7 +140,7 @@ extension ReadWriteLock {
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
public func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockRead()
defer {
self.unlock()
@ -157,7 +157,7 @@ extension ReadWriteLock {
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
public func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockWrite()
defer {
self.unlock()
@ -167,13 +167,13 @@ extension ReadWriteLock {
// specialise Void return (for performance)
@inlinable
public func withReaderLockVoid(_ body: () throws -> Void) rethrows {
internal func withReaderLockVoid(_ body: () throws -> Void) rethrows {
try self.withReaderLock(body)
}
// specialise Void return (for performance)
@inlinable
public func withWriterLockVoid(_ body: () throws -> Void) rethrows {
internal func withWriterLockVoid(_ body: () throws -> Void) rethrows {
try self.withWriterLock(body)
}
}

View File

@ -12,37 +12,67 @@
//
//===----------------------------------------------------------------------===//
/// This is the Counter protocol a metrics library implements. It must have reference semantics
/// A `CounterHandler` represents a backend implementation of a `Counter`.
///
/// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend.
/// To use the swift-metrics API, please refer to the documentation of `Counter`.
///
/// # Implementation requirements
///
/// To implement your own `CounterHandler` you should respect a few requirements that are necessary so applications work
/// as expected regardless of the selected `CounterHandler` implementation.
///
/// - The `CounterHandler` must be a `class`.
public protocol CounterHandler: AnyObject {
/// Increment the counter.
///
/// - parameters:
/// - value: Amount to increment by.
func increment(_ value: Int64)
/// Reset the counter back to zero.
func reset()
}
// This is the user facing Counter API. Its behavior depends on the `CounterHandler` implementation
/// A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero.
/// For example, you can use a counter to represent the number of requests served, tasks completed, or errors.
/// This is the user facing Counter API. Its behavior depends on the `CounterHandler` implementation
public class Counter {
@usableFromInline
var handler: CounterHandler
public let label: String
public let dimensions: [(String, String)]
// this method is public to provide an escape hatch for situations one must use a custom factory instead of the gloabl one
// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason
/// Create a new Counter.
///
/// This initializer provides an escape hatch for situations one must use a custom factory instead of the gloabl one
/// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason.
///
/// - parameters:
/// - label: The label for the Counter.
/// - dimensions: The dimensions for the Counter.
/// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], handler: CounterHandler) {
self.label = label
self.dimensions = dimensions
self.handler = handler
}
/// Increment the counter.
///
/// - parameters:
/// - value: Amount to increment by.
@inlinable
public func increment<DataType: BinaryInteger>(_ value: DataType) {
self.handler.increment(Int64(value))
}
/// Increment the counter by one.
@inlinable
public func increment() {
self.increment(1)
}
/// Reset the counter back to zero.
@inlinable
public func reset() {
self.handler.reset()
@ -50,19 +80,43 @@ public class Counter {
}
public extension Counter {
/// Create a new Counter.
///
/// - parameters:
/// - label: The label for the Counter.
/// - dimensions: The dimensions for the Counter.
convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeCounter(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler)
}
}
/// This is the Recorder protocol a metrics library implements. It must have reference semantics
/// A `RecorderHandler` represents a backend implementation of a `Recorder`.
///
/// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend.
/// To use the swift-metrics API, please refer to the documentation of `Recorder`.
///
/// # Implementation requirements
///
/// To implement your own `RecorderHandler` you should respect a few requirements that are necessary so applications work
/// as expected regardless of the selected `RecorderHandler` implementation.
///
/// - The `RecorderHandler` must be a `class`.
public protocol RecorderHandler: AnyObject {
/// Record a value.
///
/// - parameters:
/// - value: Value to record.
func record(_ value: Int64)
/// Record a value.
///
/// - parameters:
/// - value: Value to record.
func record(_ value: Double)
}
// This is the user facing Recorder API. Its behavior depends on the `RecorderHandler` implementation
/// A recorder collects observations within a time window (usually things like response sizes) and *can* provide aggregated information about the data sample, for example count, sum, min, max and various quantiles.
/// This is the user facing Recorder API. Its behavior depends on the `RecorderHandler` implementation
public class Recorder {
@usableFromInline
var handler: RecorderHandler
@ -70,8 +124,15 @@ public class Recorder {
public let dimensions: [(String, String)]
public let aggregate: Bool
// this method is public to provide an escape hatch for situations one must use a custom factory instead of the gloabl one
// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason
/// Create a new Recorder.
///
/// This initializer provides an escape hatch for situations one must use a custom factory instead of the gloabl one
/// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason.
///
/// - parameters:
/// - label: The label for the Recorder.
/// - dimensions: The dimensions for the Recorder.
/// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], aggregate: Bool, handler: RecorderHandler) {
self.label = label
self.dimensions = dimensions
@ -79,11 +140,19 @@ public class Recorder {
self.handler = handler
}
/// Record a value.
///
/// - parameters:
/// - value: Value to record.
@inlinable
public func record<DataType: BinaryInteger>(_ value: DataType) {
self.handler.record(Int64(value))
}
/// Record a value.
///
/// - parameters:
/// - value: Value to record.
@inlinable
public func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
self.handler.record(Double(value))
@ -91,69 +160,132 @@ public class Recorder {
}
public extension Recorder {
/// Create a new Recorder.
///
/// - parameters:
/// - label: The label for the Recorder.
/// - dimensions: The dimensions for the Recorder.
convenience init(label: String, dimensions: [(String, String)] = [], aggregate: Bool = true) {
let handler = MetricsSystem.factory.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
self.init(label: label, dimensions: dimensions, aggregate: aggregate, handler: handler)
}
}
// A Gauge is a convenience for non-aggregating Recorder
/// A Gauge is a metric that represents a single numerical value that can arbitrarily go up and down.
/// Gauges are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads.
/// Gauges are modeled as `Recorder` with a sample size of 1 and that does not perform any aggregation.
public class Gauge: Recorder {
/// Create a new Gauge.
///
/// - parameters:
/// - label: The label for the Gauge.
/// - dimensions: The dimensions for the Gauge.
public convenience init(label: String, dimensions: [(String, String)] = []) {
self.init(label: label, dimensions: dimensions, aggregate: false)
}
}
// This is the Timer protocol a metrics library implements. It must have reference semantics
/// A `TimerHandler` represents a backend implementation of a `Timer`.
///
/// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend.
/// To use the swift-metrics API, please refer to the documentation of `Timer`.
///
/// # Implementation requirements
///
/// To implement your own `TimerHandler` you should respect a few requirements that are necessary so applications work
/// as expected regardless of the selected `TimerHandler` implementation.
///
/// - The `TimerHandler` must be a `class`.
public protocol TimerHandler: AnyObject {
/// Record a duration in nanoseconds.
///
/// - parameters:
/// - value: Duration to record.
func recordNanoseconds(_ duration: Int64)
}
// This is the user facing Timer API. Its behavior depends on the `TimerHandler` implementation
/// A timer collects observations within a time window (usually things like request durations) and provides aggregated information about the data sample.
/// For example min, max and various quantiles. It is similar to a `Recorder` but specialized for values that represent durations.
/// This is the user facing Timer API. Its behavior depends on the `TimerHandler` implementation
public class Timer {
@usableFromInline
var handler: TimerHandler
public let label: String
public let dimensions: [(String, String)]
// this method is public to provide an escape hatch for situations one must use a custom factory instead of the gloabl one
// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason
/// Create a new Timer.
///
/// This initializer provides an escape hatch for situations one must use a custom factory instead of the gloabl one
/// we do not expect this API to be used in normal circumstances, so if you find yourself using it make sure its for a good reason.
///
/// - parameters:
/// - label: The label for the Timer.
/// - dimensions: The dimensions for the Timer.
/// - handler: The custom backend.
public init(label: String, dimensions: [(String, String)], handler: TimerHandler) {
self.label = label
self.dimensions = dimensions
self.handler = handler
}
/// Record a duration in nanoseconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordNanoseconds(_ duration: Int64) {
self.handler.recordNanoseconds(duration)
}
/// Record a duration in microseconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordMicroseconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1000)
self.recordNanoseconds(Int64(duration * 1000))
}
/// Record a duration in microseconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordMicroseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1000))
}
/// Record a duration in milliseconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordMilliseconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1_000_000)
self.recordNanoseconds(Int64(duration * 1_000_000))
}
/// Record a duration in milliseconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordMilliseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1_000_000))
}
/// Record a duration in seconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordSeconds<DataType: BinaryInteger>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration) * 1_000_000_000)
self.recordNanoseconds(Int64(duration * 1_000_000_000))
}
/// Record a duration in seconds.
///
/// - parameters:
/// - value: Duration to record.
@inlinable
public func recordSeconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
self.recordNanoseconds(Int64(duration * 1_000_000_000))
@ -161,25 +293,61 @@ public class Timer {
}
public extension Timer {
/// Create a new Timer.
///
/// - parameters:
/// - label: The label for the Timer.
/// - dimensions: The dimensions for the Timer.
convenience init(label: String, dimensions: [(String, String)] = []) {
let handler = MetricsSystem.factory.makeTimer(label: label, dimensions: dimensions)
self.init(label: label, dimensions: dimensions, handler: handler)
}
}
/// The `MetricsFactory` is the bridge between the `MetricsSystem` and the metrics backend implementation.
/// `MetricsFactory` role is to initialize concrete implementations of the various metric types:
/// * `Counter` -> `CounterHandler`
/// * `Recorder` -> `RecorderHandler`
/// * `Timer` -> `TimerHandler`
///
/// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend.
/// To use the swift-metrics API, please refer to the documentation of `MetricsSystem`.
public protocol MetricsFactory {
/// Create a backing `CounterHandler`.
///
/// - parameters:
/// - label: The label for the CounterHandler.
/// - dimensions: The dimensions for the CounterHandler.
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
/// Create a backing `RecorderHandler`.
///
/// - parameters:
/// - label: The label for the RecorderHandler.
/// - dimensions: The dimensions for the RecorderHandler.
/// - aggregate: Is data aggregation expected.
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
/// Create a backing `TimerHandler`.
///
/// - parameters:
/// - label: The label for the TimerHandler.
/// - dimensions: The dimensions for the TimerHandler.
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
}
// This is the metrics system itself, it's mostly used set the type of the `MetricsFactory` implementation
/// The `MetricsSystem` is a global facility where the default metrics backend implementation (`MetricsFactory`) can be
/// configured. `MetricsSystem` is set up just once in a given program to set up the desired metrics backend
/// implementation.
public enum MetricsSystem {
fileprivate static let lock = ReadWriteLock()
fileprivate static var _factory: MetricsFactory = NOOPMetricsHandler.instance
fileprivate static var initialized = false
// Configures which `LogHandler` to use in the application.
/// `bootstrap` is a 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.
///
/// - parameters:
/// - factory: A factory that given an identifier produces instances of metrics handlers such as `CounterHandler`, `RecorderHandler` and `TimerHandler`.
public static func bootstrap(_ factory: MetricsFactory) {
self.lock.withWriterLock {
precondition(!self.initialized, "metrics system can only be initialized once per process. currently used factory: \(self.factory)")
@ -195,12 +363,13 @@ public enum MetricsSystem {
}
}
/// Returns a refernece to the configured factory.
public static var factory: MetricsFactory {
return self.lock.withReaderLock { self._factory }
}
}
/// Ships with the metrics module, used to multiplex to multiple metrics handlers
/// A pseudo-metrics handler that can be used to send messages to multiple other metrics handlers.
public final class MultiplexMetricsHandler: MetricsFactory {
private let factories: [MetricsFactory]
public init(factories: [MetricsFactory]) {
@ -261,6 +430,7 @@ public final class MultiplexMetricsHandler: MetricsFactory {
}
}
/// Ships with the metrics module, used for initial bootstraping.
public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, RecorderHandler, TimerHandler {
public static let instance = NOOPMetricsHandler()

View File

@ -1,106 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// THIS IS NOT PART OF THE PITCH, JUST AN EXAMPLE
//
import Metrics
enum Example1 {
static func main() {
// bootstrap with our example metrics library
let metrics = ExampleMetricsLibrary()
MetricsSystem.bootstrap(metrics)
let server = Server()
let client = Client(server: server)
client.run(iterations: Int.random(in: 10 ... 50))
print("-----> counters")
metrics.counters.forEach { print(" \($0)") }
print("-----> recorders")
metrics.recorders.forEach { print(" \($0)") }
print("-----> timers")
metrics.timers.forEach { print(" \($0)") }
print("-----> gauges")
metrics.gauges.forEach { print(" \($0)") }
}
class Client {
private let activeRequestsGauge = Gauge(label: "Client::ActiveRequests")
private let server: Server
init(server: Server) {
self.server = server
}
func run(iterations: Int) {
let group = DispatchGroup()
let requestsCounter = Counter(label: "Client::TotalRequests")
let requestTimer = Timer(label: "Client::doSomethig")
let resultRecorder = Recorder(label: "Client::doSomethig::result")
for _ in 0 ... iterations {
group.enter()
let start = Date()
requestsCounter.increment()
self.activeRequests += 1
server.doSomethig { result in
requestTimer.record(Date().timeIntervalSince(start))
resultRecorder.record(result)
self.activeRequests -= 1
group.leave()
}
}
group.wait()
}
private let lock = NSLock()
private var _activeRequests = 0
var activeRequests: Int {
get {
return self.lock.withLock { _activeRequests }
} set {
self.lock.withLock { _activeRequests = newValue }
self.activeRequestsGauge.record(newValue)
}
}
}
class Server {
let library = RandomLibrary()
let requestsCounter = Counter(label: "Server::TotalRequests")
func doSomethig(callback: @escaping (Int64) -> Void) {
let timer = Timer(label: "Server::doSomethig")
let start = Date()
requestsCounter.increment()
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) {
self.library.doSomething()
self.library.doSomethingSlow {
timer.record(Date().timeIntervalSince(start))
callback(Int64.random(in: 0 ... 1000))
}
}
}
}
}
private extension NSLock {
func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}

View File

@ -1,291 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// THIS IS NOT PART OF THE PITCH, JUST AN EXAMPLE
//
import Metrics
class ExampleMetricsLibrary: MetricsFactory {
private let config: Config
private let lock = NSLock()
var counters = [ExampleCounter]()
var recorders = [ExampleRecorder]()
var gauges = [ExampleGauge]()
var timers = [ExampleTimer]()
init(config: Config = Config()) {
self.config = config
}
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return self.register(label: label, dimensions: dimensions, registry: &self.counters, maker: ExampleCounter.init)
}
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let options = aggregate ? self.config.recorder.aggregationOptions : nil
return self.makeRecorder(label: label, dimensions: dimensions, options: options)
}
func makeRecorder(label: String, dimensions: [(String, String)], options: [AggregationOption]?) -> RecorderHandler {
guard let options = options else {
return self.register(label: label, dimensions: dimensions, registry: &self.gauges, maker: ExampleGauge.init)
}
let maker = { (label: String, dimensions: [(String, String)]) -> ExampleRecorder in
ExampleRecorder(label: label, dimensions: dimensions, options: options)
}
return self.register(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker)
}
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return self.makeTimer(label: label, dimensions: dimensions, options: self.config.timer.aggregationOptions)
}
func makeTimer(label: String, dimensions: [(String, String)], options: [AggregationOption]) -> TimerHandler {
let maker = { (label: String, dimensions: [(String, String)]) -> ExampleTimer in
ExampleTimer(label: label, dimensions: dimensions, options: options)
}
return self.register(label: label, dimensions: dimensions, registry: &self.timers, maker: maker)
}
func register<Item>(label: String, dimensions: [(String, String)], registry: inout [Item], maker: (String, [(String, String)]) -> Item) -> Item {
let item = maker(label, dimensions)
lock.withLock {
registry.append(item)
}
return item
}
class Config {
let recorder: RecorderConfig
let timer: TimerConfig
init(recorder: RecorderConfig = RecorderConfig(), timer: TimerConfig = TimerConfig()) {
self.recorder = recorder
self.timer = timer
}
}
class RecorderConfig {
let aggregationOptions: [AggregationOption]
init(aggregationOptions: [AggregationOption]) {
self.aggregationOptions = aggregationOptions
}
init() {
self.aggregationOptions = AggregationOption.defaults
}
}
class TimerConfig {
let aggregationOptions: [AggregationOption]
init(aggregationOptions: [AggregationOption]) {
self.aggregationOptions = aggregationOptions
}
init() {
self.aggregationOptions = AggregationOption.defaults
}
}
}
class ExampleCounter: CounterHandler, CustomStringConvertible {
let label: String
let dimensions: [(String, String)]
init(label: String, dimensions: [(String, String)]) {
self.label = label
self.dimensions = dimensions
}
let lock = NSLock()
var value: Int64 = 0
func increment<DataType: BinaryInteger>(_ value: DataType) {
self.lock.withLock {
self.value += Int64(value)
}
}
func reset() {
self.lock.withLock {
self.value = 0
}
}
var description: String {
return "counter [label: \(self.label) dimensions:\(self.dimensions) values:\(self.value)]"
}
}
class ExampleRecorder: RecorderHandler, CustomStringConvertible {
let label: String
let dimensions: [(String, String)]
let options: [AggregationOption]
init(label: String, dimensions: [(String, String)], options: [AggregationOption]) {
self.label = label
self.dimensions = dimensions
self.options = options
}
private let lock = NSLock()
var values = [(Int64, Double)]()
func record(_ value: Int64) {
self.record(Double(value))
}
func record(_ value: Double) {
// TODO: sliding window
self.lock.withLock {
values.append((Date().nanoSince1970, value))
}
self.options.forEach { option in
switch option {
case .count:
self.count += 1
case .sum:
self.sum += value
case .min:
self.min = Swift.min(self.min, value)
case .max:
self.max = Swift.max(self.max, value)
case .quantiles(let items):
self.computeQuantiles(items)
}
}
}
var _sum: Double = 0
var sum: Double {
get {
return self.lock.withLock { _sum }
}
set {
self.lock.withLock { _sum = newValue }
}
}
private var _count: Int = 0
var count: Int {
get {
return self.lock.withLock { _count }
}
set {
self.lock.withLock { _count = newValue }
}
}
private var _min: Double = 0
var min: Double {
get {
return self.lock.withLock { _min }
}
set {
self.lock.withLock { _min = newValue }
}
}
private var _max: Double = 0
var max: Double {
get {
return self.lock.withLock { _max }
}
set {
self.lock.withLock { _max = newValue }
}
}
private var _quantiels = [Float: Double]()
var quantiels: [Float: Double] {
get {
return self.lock.withLock { _quantiels }
}
set {
self.lock.withLock { _quantiels = newValue }
}
}
var description: String {
return "recorder [label: \(self.label) dimensions:\(self.dimensions) count:\(self.count) sum:\(self.sum) min:\(self.min) max:\(self.max) quantiels:\(self.quantiels) values:\(self.values)]"
}
// TODO: offload calcs to queue
private func computeQuantiles(_ items: [Float]) {
self.lock.withLock {
self._quantiels.removeAll()
items.forEach { item in
if let result = Sigma.quantiles.method1(self.values.map { $0.1 }, probability: Double(item)) {
self._quantiels[item] = result
}
}
}
}
}
class ExampleGauge: RecorderHandler, CustomStringConvertible {
let label: String
let dimensions: [(String, String)]
init(label: String, dimensions: [(String, String)]) {
self.label = label
self.dimensions = dimensions
}
let lock = NSLock()
var _value: Double = 0
func record(_ value: Int64) {
self.record(Double(value))
}
func record(_ value: Double) {
self.lock.withLock { _value = value }
}
var description: String {
return "gauge [label: \(self.label) dimensions:\(self.dimensions) value:\(self._value)]"
}
}
class ExampleTimer: ExampleRecorder, TimerHandler {
func recordNanoseconds(_ duration: Int64) {
super.record(duration)
}
override var description: String {
return "timer [label: \(self.label) dimensions:\(self.dimensions) count:\(self.count) sum:\(self.sum) min:\(self.min) max:\(self.max) quantiels:\(self.quantiels) values:\(self.values)]"
}
}
enum AggregationOption {
case count
case sum
case min
case max
case quantiles(_ items: [Float])
public static let defaults: [AggregationOption] = [.count, .sum, .min, .max, .quantiles(defaultQuantiles)]
public static let defaultQuantiles: [Float] = [0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
}
private extension Foundation.Date {
var nanoSince1970: Int64 {
return Int64(self.timeIntervalSince1970 * 1_000_000_000)
}
}
private extension Foundation.NSLock {
func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}

View File

@ -1,223 +0,0 @@
//
// THIS IS NOT PART OF THE PITCH, JUST AN EXAMPLE
//
// copied from https://github.com/evgenyneu/SigmaSwiftStatistics/blob/master/SigmaSwiftStatistics/Quantiles.swift
//
// Created by Alan James Salmoni on 21/12/2016.
// Copyright © 2016 Thought Into Design Ltd. All rights reserved.
//
import Foundation
public enum Sigma {
/**
The class contains nine functions that calculate sample quantiles corresponding to the given probability. The implementation is the same as in R. This is an implementation of the algorithms described in the Hyndman and Fan paper, 1996:
https://www.jstor.org/stable/2684934
https://www.amherst.edu/media/view/129116/original/Sample+Quantiles.pdf
The documentation of the functions is based on R and Wikipedia:
https://en.wikipedia.org/wiki/Quantile
http://stat.ethz.ch/R-manual/R-devel/library/stats/html/quantile.html
*/
public static let quantiles = SigmaQuantiles()
}
public class SigmaQuantiles {
/*
This method calculates quantiles using the inverse of the empirical distribution function.
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method1(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let k = Int((probability * count))
let g = (probability * count) - Double(k)
var new_probability = 1.0
if g == 0.0 { new_probability = 0.0 }
return self.qDef(data, k: k, probability: new_probability)
}
/**
This method uses inverted empirical distribution function with averaging.
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method2(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let k = Int(probability * count)
let g = (probability * count) - Double(k)
var new_probability = 1.0
if g == 0.0 { new_probability = 0.5 }
return self.qDef(data, k: k, probability: new_probability)
}
/**
The 3rd sample quantile method from Hyndman and Fan paper (1996).
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method3(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = -0.5
let k = Int((probability * count) + m)
let g = (probability * count) + m - Double(k)
var new_probability = 1.0
if g <= 0, k % 2 == 0 { new_probability = 0.0 }
return self.qDef(data, k: k, probability: new_probability)
}
/**
It uses linear interpolation of the empirical distribution function.
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method4(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = 0.0
let k = Int((probability * count) + m)
let probability = (probability * count) + m - Double(k)
return self.qDef(data, k: k, probability: probability)
}
/**
This method uses a piecewise linear function where the knots are the values midway through the steps of the empirical distribution function.
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method5(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = 0.5
let k = Int((probability * count) + m)
let probability = (probability * count) + m - Double(k)
return self.qDef(data, k: k, probability: probability)
}
/**
This method is implemented in Microsoft Excel (PERCENTILE.EXC), Minitab and SPSS. It uses linear interpolation of the expectations for the order statistics for the uniform distribution on [0,1].
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method6(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = probability
let k = Int((probability * count) + m)
let probability = (probability * count) + m - Double(k)
return self.qDef(data, k: k, probability: probability)
}
/**
This method is implemented in S, Microsoft Excel (PERCENTILE or PERCENTILE.INC) and Google Docs Sheets (PERCENTILE). It uses linear interpolation of the modes for the order statistics for the uniform distribution on [0, 1].
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method7(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = 1.0 - probability
let k = Int((probability * count) + m)
let probability = (probability * count) + m - Double(k)
return self.qDef(data, k: k, probability: probability)
}
/**
The quantiles returned by the method are approximately median-unbiased regardless of the distribution of x.
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method8(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = (probability + 1.0) / 3.0
let k = Int((probability * count) + m)
let probability = (probability * count) + m - Double(k)
return self.qDef(data, k: k, probability: probability)
}
/**
The quantiles returned by this method are approximately unbiased for the expected order statistics if x is normally distributed.
- parameter data: Array of decimal numbers.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
public func method9(_ data: [Double], probability: Double) -> Double? {
if probability < 0 || probability > 1 { return nil }
let data = data.sorted(by: <)
let count = Double(data.count)
let m = (0.25 * probability) + (3.0 / 8.0)
let k = Int((probability * count) + m)
let probability = (probability * count) + m - Double(k)
return self.qDef(data, k: k, probability: probability)
}
/**
Shared function for all quantile methods.
- parameter data: Array of decimal numbers.
- parameter k: the position of the element in the dataset.
- parameter probability: the probability value between 0 and 1, inclusive.
- returns: sample quantile.
*/
private func qDef(_ data: [Double], k: Int, probability: Double) -> Double? {
if data.isEmpty { return nil }
if k < 1 { return data[0] }
if k >= data.count { return data.last }
return ((1.0 - probability) * data[k - 1]) + (probability * data[k])
}
}

View File

@ -1,36 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// THIS IS NOT PART OF THE PITCH, JUST AN EXAMPLE
//
import Metrics
class RandomLibrary {
let methodCallsCounter = Counter(label: "RandomLibrary::TotalMethodCalls")
func doSomething() {
self.methodCallsCounter.increment()
}
func doSomethingSlow(callback: @escaping () -> Void) {
self.methodCallsCounter.increment()
let timer = Timer(label: "RandomLibrary::doSomethingSlow")
let start = Date()
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) {
timer.record(Date().timeIntervalSince(start))
callback()
}
}
}

View File

@ -1,130 +0,0 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// THIS IS NOT PART OF THE PITCH, JUST AN EXAMPLE
//
import Metrics
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)
}
private class ExampleCounter: CounterHandler {
init(_: String, _: [(String, String)]) {}
let lock = NSLock()
var value: Int64 = 0
func increment(_ value: Int64) {
self.lock.withLock {
self.value += value
}
}
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
self.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)
}
}
}
private extension Foundation.Date {
var nanoSince1970: Int64 {
return Int64(self.timeIntervalSince1970 * 1_000_000_000)
}
}
private extension Foundation.NSLock {
func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}

View File

@ -1,9 +1,28 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
@_exported import CoreMetrics
@_exported import class CoreMetrics.Timer
@_exported import Foundation
// Convenience for measuring duration of a closure
public extension Timer {
/// Convenience for measuring duration of a closure.
///
/// - parameters:
/// - label: The label for the Timer.
/// - dimensions: The dimensions for the Timer.
/// - body: Closure to run & record.
@inlinable
static func measure<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
let timer = Timer(label: label, dimensions: dimensions)
@ -15,13 +34,20 @@ public extension Timer {
}
}
// Convenience for using Foundation and Dispatch
public extension Timer {
/// Convenience for recording a duration based on TimeInterval.
///
/// - parameters:
/// - duration: The duration to record.
@inlinable
func record(_ duration: TimeInterval) {
self.recordSeconds(duration)
}
/// Convenience for recording a duration based on DispatchTimeInterval.
///
/// - parameters:
/// - duration: The duration to record.
@inlinable
func record(_ duration: DispatchTimeInterval) {
switch duration {

View File

@ -12,8 +12,21 @@
//
//===----------------------------------------------------------------------===//
//
// THIS IS NOT PART OF THE PITCH, JUST AN EXAMPLE
// LinuxMain.swift
//
import XCTest
print("##### example 1 #####")
Example1.main()
///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
#if os(Linux) || os(FreeBSD)
@testable import MetricsTests
XCTMain([
testCase(MetricsExtensionsTests.allTests),
testCase(MetricsTests.allTests),
])
#endif

View File

@ -0,0 +1,45 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// CoreMetricsTests+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 MetricsTests {
static var allTests : [(String, (MetricsTests) -> () throws -> Void)] {
return [
("testCounters", testCounters),
("testCounterBlock", testCounterBlock),
("testRecorders", testRecorders),
("testRecordersInt", testRecordersInt),
("testRecordersFloat", testRecordersFloat),
("testRecorderBlock", testRecorderBlock),
("testTimers", testTimers),
("testTimerBlock", testTimerBlock),
("testTimerVariants", testTimerVariants),
("testGauge", testGauge),
("testGaugeBlock", testGaugeBlock),
("testMUX", testMUX),
("testCustomFactory", testCustomFactory),
]
}
}

View File

@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// MetricsTests+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 MetricsExtensionsTests {
static var allTests : [(String, (MetricsExtensionsTests) -> () throws -> Void)] {
return [
("testTimerBlock", testTimerBlock),
("testTimerWithTimeInterval", testTimerWithTimeInterval),
("testTimerWithDispatchTime", testTimerWithDispatchTime),
]
}
}

View File

@ -56,25 +56,43 @@ class MetricsExtensionsTests: XCTestCase {
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500))
timer.record(nano)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(testTimer.values[0].1)), nano, "expected value to match")
XCTAssertEqual(Int(testTimer.values[0].1), nano.nano(), "expected value to match")
// micro
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500))
timer.record(micro)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(testTimer.values[1].1)), micro, "expected value to match")
XCTAssertEqual(Int(testTimer.values[1].1), micro.nano(), "expected value to match")
// milli
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500))
timer.record(milli)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(testTimer.values[2].1)), milli, "expected value to match")
XCTAssertEqual(Int(testTimer.values[2].1), milli.nano(), "expected value to match")
// seconds
let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500))
timer.record(sec)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(.nanoseconds(Int(testTimer.values[3].1)), sec, "expected value to match")
XCTAssertEqual(Int(testTimer.values[3].1), sec.nano(), "expected value to match")
// never
timer.record(DispatchTimeInterval.never)
XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match")
XCTAssertEqual(testTimer.values[4].1, 0, "expected value to match")
}
}
// https://bugs.swift.org/browse/SR-6310
extension DispatchTimeInterval {
func nano() -> Int {
switch self {
case .nanoseconds(let value):
return value
case .microseconds(let value):
return value * 1000
case .milliseconds(let value):
return value * 1_000_000
case .seconds(let value):
return value * 1_000_000_000
case .never:
return 0
}
}
}

56
docker/Dockerfile Normal file
View File

@ -0,0 +1,56 @@
ARG ubuntu_version=18.04
FROM ubuntu:$ubuntu_version
# needed to do again after FROM due to docker limitation
ARG ubuntu_version
ARG DEBIAN_FRONTEND=noninteractive
# do not start services during installation as this will fail and log a warning / error.
RUN echo "#!/bin/sh\nexit 0" > /usr/sbin/policy-rc.d
# basic dependencies
RUN apt-get update && apt-get install -y wget git build-essential software-properties-common pkg-config locales
RUN apt-get update && apt-get install -y libicu-dev libblocksruntime0 curl libcurl4-openssl-dev libz-dev
# local
RUN locale-gen en_US.UTF-8
RUN locale-gen en_US en_US.UTF-8
RUN dpkg-reconfigure locales
RUN echo 'export LANG=en_US.UTF-8' >> $HOME/.profile
RUN echo 'export LANGUAGE=en_US:en' >> $HOME/.profile
RUN echo 'export LC_ALL=en_US.UTF-8' >> $HOME/.profile
# known_hosts
RUN mkdir -p $HOME/.ssh
RUN touch $HOME/.ssh/known_hosts
RUN ssh-keyscan github.com 2> /dev/null >> $HOME/.ssh/known_hosts
# clang
RUN apt-get update && apt-get install -y clang-3.9
RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-3.9 100
RUN update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-3.9 100
# ruby and jazzy for docs generation
#ARG skip_ruby_from_ppa
#RUN [ -n "$skip_ruby_from_ppa" ] || apt-add-repository -y ppa:brightbox/ruby-ng
#RUN [ -n "$skip_ruby_from_ppa" ] || { apt-get update && apt-get install -y ruby2.4 ruby2.4-dev; }
#RUN [ -z "$skip_ruby_from_ppa" ] || { apt-get update && apt-get install -y ruby ruby-dev; }
#RUN apt-get update && apt-get install -y libsqlite3-dev
RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev
RUN gem install jazzy --no-ri --no-rdoc
# swift
ARG swift_version=4.2.3
ARG swift_flavour=RELEASE
ARG swift_builds_suffix=release
RUN mkdir $HOME/.swift
RUN wget -q "https://swift.org/builds/swift-${swift_version}-${swift_builds_suffix}/ubuntu$(echo $ubuntu_version | sed 's/\.//g')/swift-${swift_version}-${swift_flavour}/swift-${swift_version}-${swift_flavour}-ubuntu${ubuntu_version}.tar.gz" -O $HOME/swift.tar.gz
RUN tar xzf $HOME/swift.tar.gz --directory $HOME/.swift --strip-components=1
RUN echo 'export PATH="$HOME/.swift/usr/bin:$PATH"' >> $HOME/.profile
RUN echo 'export LINUX_SOURCEKIT_LIB_PATH="$HOME/.swift/usr/lib"' >> $HOME/.profile
# script to allow mapping framepointers on linux
RUN mkdir -p $HOME/.scripts
RUN wget -q https://raw.githubusercontent.com/apple/swift/master/utils/symbolicate-linux-fatal -O $HOME/.scripts/symbolicate-linux-fatal
RUN chmod 755 $HOME/.scripts/symbolicate-linux-fatal
RUN echo 'export PATH="$HOME/.scripts:$PATH"' >> $HOME/.profile

View File

@ -0,0 +1,13 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-4.2
build:
args:
ubuntu_version: "18.04"
swift_version: "4.2"
test:
image: swift-metrics:18.04-4.2

View File

@ -0,0 +1,13 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-5.0
build:
args:
ubuntu_version: "18.04"
swift_version: "5.0"
test:
image: swift-metrics:18.04-5.0

View File

@ -0,0 +1,34 @@
# this file is not designed to be run directly
# instead, use the docker-compose.<os>.<swift> files
# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.1804.50.yaml run test
version: "3"
services:
runtime-setup:
image: swift-metrics:default
build:
context: .
dockerfile: Dockerfile
common: &common
image: swift-metrics:default
depends_on: [runtime-setup]
volumes:
- ~/.ssh:/root/.ssh
- ..:/code
working_dir: /code
sanity:
<<: *common
command: /bin/bash -cl "./scripts/sanity.sh"
docs:
<<: *common
environment:
- CI
command: /bin/bash -cl "./scripts/generate_docs.sh"
test:
<<: *common
command: /bin/bash -cl "swift test"

View File

@ -3,7 +3,7 @@
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the Swift Metrics API project authors
## 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

116
scripts/generate_docs.sh Executable file
View File

@ -0,0 +1,116 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## 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
##
##===----------------------------------------------------------------------===##
set -e
my_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
root_path="$my_path/.."
version=$(git describe --abbrev=0 --tags || echo "0.0.0")
modules=(CoreMetrics Metrics)
if [[ "$(uname -s)" == "Linux" ]]; then
# build code if required
if [[ ! -d "$root_path/.build/x86_64-unknown-linux" ]]; then
swift build
fi
# setup source-kitten if required
source_kitten_source_path="$root_path/.SourceKitten"
if [[ ! -d "$source_kitten_source_path" ]]; then
git clone https://github.com/jpsim/SourceKitten.git "$source_kitten_source_path"
fi
source_kitten_path="$source_kitten_source_path/.build/x86_64-unknown-linux/debug"
if [[ ! -d "$source_kitten_path" ]]; then
rm -rf "$source_kitten_source_path/.swift-version"
cd "$source_kitten_source_path" && swift build && cd "$root_path"
fi
# generate
mkdir -p "$root_path/.build/sourcekitten"
for module in "${modules[@]}"; do
if [[ ! -f "$root_path/.build/sourcekitten/$module.json" ]]; then
"$source_kitten_path/sourcekitten" doc --spm-module $module > "$root_path/.build/sourcekitten/$module.json"
fi
done
fi
[[ -d docs/$version ]] || mkdir -p docs/$version
[[ -d swift-metrics.xcodeproj ]] || swift package generate-xcodeproj
# run jazzy
if ! command -v jazzy > /dev/null; then
gem install jazzy --no-ri --no-rdoc
fi
tmp=`mktemp -d`
mkdir -p "$tmp/docs/$version"
module_switcher="$tmp/docs/$version/README.md"
jazzy_args=(--clean
--author 'swift-metrics team'
--readme "$module_switcher"
--author_url https://github.com/apple/swift-metrics
--github_url https://github.com/apple/swift-metrics
--theme fullwidth
--xcodebuild-arguments -scheme,swift-metrics-Package)
cat > "$module_switcher" <<"EOF"
# swift-metrics docs
swift-metrics is a Swift metrics API package.
To get started with swift-metrics, [`import Metrics`](../CoreMetrics/index.html). The most important types are:
* [`Counter`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Counter.html)
* [`Timer`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Timer.html)
* [`Recorder`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Recorder.html)
* [`Gauge`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Gauge.html)
swift-metrics contains multiple modules:
EOF
for module in "${modules[@]}"; do
echo " - [$module](../$module/index.html)" >> "$module_switcher"
done
for module in "${modules[@]}"; do
echo "processing $module"
args=("${jazzy_args[@]}" --output "$tmp/docs/$version/$module" --docset-path "$tmp/docset/$version/$module" --module "$module")
if [[ -f "$root_path/.build/sourcekitten/$module.json" ]]; then
args+=(--sourcekitten-sourcefile "$root_path/.build/sourcekitten/$module.json")
fi
jazzy "${args[@]}"
done
# push to github pages
if [[ $CI == true ]]; then
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
GIT_AUTHOR=$(git --no-pager show -s --format='%an <%ae>' HEAD)
git fetch origin +gh-pages:gh-pages
git checkout gh-pages
rm -rf "docs"
cp -r "$tmp/docs" .
cp -r "docs/$version" docs/current
git add --all docs
echo '<html><head><meta http-equiv="refresh" content="0; url=docs/current/CoreMetrics/index.html" /></head></html>' > index.html
git add index.html
touch .nojekyll
git add .nojekyll
changes=$(git diff-index --name-only HEAD)
if [[ -n "$changes" ]]; then
echo -e "changes detected\n$changes"
git commit --author="$GIT_AUTHOR" -m "publish $version docs"
git push origin gh-pages
else
echo "no changes detected"
fi
git checkout -f $BRANCH_NAME
fi

231
scripts/generate_linux_tests.rb Executable file
View File

@ -0,0 +1,231 @@
#!/usr/bin/env ruby
#
# process_test_files.rb
#
# Copyright 2016 Tony Stone
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Created by Tony Stone on 5/4/16.
#
require 'getoptlong'
require 'fileutils'
require 'pathname'
include FileUtils
#
# This ruby script will auto generate LinuxMain.swift and the +XCTest.swift extension files for Swift Package Manager on Linux platforms.
#
# See https://github.com/apple/swift-corelibs-xctest/blob/master/Documentation/Linux.md
#
def header(fileName)
string = <<-eos
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// <FileName>
//
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.
///
eos
string
.sub('<FileName>', File.basename(fileName))
.sub('<Date>', Time.now.to_s)
end
def createExtensionFile(fileName, classes)
extensionFile = fileName.sub! '.swift', '+XCTest.swift'
print 'Creating file: ' + extensionFile + "\n"
File.open(extensionFile, 'w') do |file|
file.write header(extensionFile)
file.write "\n"
for classArray in classes
file.write 'extension ' + classArray[0] + " {\n\n"
file.write ' static var allTests : [(String, (' + classArray[0] + ") -> () throws -> Void)] {\n"
file.write " return [\n"
for funcName in classArray[1]
file.write ' ("' + funcName + '", ' + funcName + "),\n"
end
file.write " ]\n"
file.write " }\n"
file.write "}\n\n"
end
end
end
def createLinuxMain(testsDirectory, allTestSubDirectories, files)
fileName = testsDirectory + '/LinuxMain.swift'
print 'Creating file: ' + fileName + "\n"
File.open(fileName, 'w') do |file|
file.write header(fileName)
file.write "\n"
file.write "#if os(Linux) || os(FreeBSD)\n"
for testSubDirectory in allTestSubDirectories.sort { |x, y| x <=> y }
file.write ' @testable import ' + testSubDirectory + "\n"
end
file.write "\n"
file.write " XCTMain([\n"
testCases = []
for classes in files
for classArray in classes
testCases << classArray[0]
end
end
for testCase in testCases.sort { |x, y| x <=> y }
file.write ' testCase(' + testCase + ".allTests),\n"
end
file.write " ])\n"
file.write "#endif\n"
end
end
def parseSourceFile(fileName)
puts 'Parsing file: ' + fileName + "\n"
classes = []
currentClass = nil
inIfLinux = false
inElse = false
ignore = false
#
# Read the file line by line
# and parse to find the class
# names and func names
#
File.readlines(fileName).each do |line|
if inIfLinux
if /\#else/.match(line)
inElse = true
ignore = true
else
if /\#end/.match(line)
inElse = false
inIfLinux = false
ignore = false
end
end
else
if /\#if[ \t]+os\(Linux\)/.match(line)
inIfLinux = true
ignore = false
end
end
next if ignore
# Match class or func
match = line[/class[ \t]+[a-zA-Z0-9_]*(?=[ \t]*:[ \t]*XCTestCase)|func[ \t]+test[a-zA-Z0-9_]*(?=[ \t]*\(\))/, 0]
if match
if match[/class/, 0] == 'class'
className = match.sub(/^class[ \t]+/, '')
#
# Create a new class / func structure
# and add it to the classes array.
#
currentClass = [className, []]
classes << currentClass
else # Must be a func
funcName = match.sub(/^func[ \t]+/, '')
#
# Add each func name the the class / func
# structure created above.
#
currentClass[1] << funcName
end
end
end
classes
end
#
# Main routine
#
#
testsDirectory = 'Tests'
options = GetoptLong.new(['--tests-dir', GetoptLong::OPTIONAL_ARGUMENT])
options.quiet = true
begin
options.each do |option, value|
case option
when '--tests-dir'
testsDirectory = value
end
end
rescue GetoptLong::InvalidOption
end
allTestSubDirectories = []
allFiles = []
Dir[testsDirectory + '/*'].each do |subDirectory|
next unless File.directory?(subDirectory)
directoryHasClasses = false
Dir[subDirectory + '/*Test{s,}.swift'].each do |fileName|
next unless File.file? fileName
fileClasses = parseSourceFile(fileName)
#
# If there are classes in the
# test source file, create an extension
# file for it.
#
next unless fileClasses.count > 0
createExtensionFile(fileName, fileClasses)
directoryHasClasses = true
allFiles << fileClasses
end
if directoryHasClasses
allTestSubDirectories << Pathname.new(subDirectory).split.last.to_s
end
end
#
# Last step is the create a LinuxMain.swift file that
# references all the classes and funcs in the source files.
#
if allFiles.count > 0
createLinuxMain(testsDirectory, allTestSubDirectories, allFiles)
end
# eof

121
scripts/sanity.sh Executable file
View File

@ -0,0 +1,121 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## 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
##
##===----------------------------------------------------------------------===##
set -eu
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
printf "=> Checking linux tests... "
FIRST_OUT="$(git status --porcelain)"
ruby "$here/../scripts/generate_linux_tests.rb" > /dev/null
SECOND_OUT="$(git status --porcelain)"
if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then
printf "\033[0;31mmissing changes!\033[0m\n"
git --no-pager diff
exit 1
else
printf "\033[0;32mokay.\033[0m\n"
fi
printf "=> Checking license headers... "
tmp=$(mktemp /tmp/.swift-metrics-sanity_XXXXXX)
for language in swift-or-c bash dtrace; do
declare -a matching_files
declare -a exceptions
expections=( )
matching_files=( -name '*' )
case "$language" in
swift-or-c)
exceptions=( -name c_nio_http_parser.c -o -name c_nio_http_parser.h -o -name cpp_magic.h -o -name Package.swift -o -name CNIOSHA1.h -o -name c_nio_sha1.c -o -name ifaddrs-android.c -o -name ifaddrs-android.h)
matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' )
cat > "$tmp" <<"EOF"
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
EOF
;;
bash)
matching_files=( -name '*.sh' )
cat > "$tmp" <<"EOF"
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## 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
##
##===----------------------------------------------------------------------===##
EOF
;;
dtrace)
matching_files=( -name '*.d' )
cat > "$tmp" <<"EOF"
#!/usr/sbin/dtrace -q -s
/*===----------------------------------------------------------------------===*
*
* 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
*
*===----------------------------------------------------------------------===*/
EOF
;;
*)
echo >&2 "ERROR: unknown language '$language'"
;;
esac
expected_lines=$(cat "$tmp" | wc -l)
expected_sha=$(cat "$tmp" | shasum)
(
cd "$here/.."
find . \
\( \! -path './.build/*' -a \
\( "${matching_files[@]}" \) -a \
\( \! \( "${exceptions[@]}" \) \) \) | while read line; do
if [[ "$(cat "$line" | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then
printf "\033[0;31mmissing headers in file '$line'!\033[0m\n"
diff -u <(cat "$line" | head -n $expected_lines) "$tmp"
exit 1
fi
done
printf "\033[0;32mokay.\033[0m\n"
)
done
rm "$tmp"