Compare commits

..

82 Commits
2.0.0 ... main

Author SHA1 Message Date
Honza Dvorsky 4c83e1cdf4
Remove warnings against explicit dependency injection of the metrics factory (#174)
### Motivation:

In the previous PR, we noticed that these warnings seem to discourage
explicit dependency injection, but it's not clear why we should be
discouraging it.

### Modifications:

Removed the warning.

### Result:

N/A
2025-04-29 15:27:10 +02:00
Honza Dvorsky 98d36172c1
Allow providing a custom MetricsFactory to Counter and friends (#172)
### Motivation:

To allow for things like parallel testing, it'd be useful if we can
explicitly provide a MetricsFactory to the existing Counter/... types.

### Modifications:

Added a `factory: MetricsFactory` parameter to all the initializers of
Counter/... types, and kept the existing methods that continue to
default to `MetricsSystem.factory`.

### Result:

Adopters can use a custom MetricsFactory explicitly passed in at metric
creation time. Existing adopters are not affected, unless you opt in,
you continue to use the global factory.
2025-04-29 15:07:30 +02:00
Rick Newton-Rogers 27ecca7ac1
Enable Swift 6.1 jobs in CI (#171)
Motivation:

Swift 6.1 has been released, we should add it to our CI coverage.

Modifications:

Add additional Swift 6.1 jobs where appropriate in main.yml,
pull_request.yml

Result:

Improved test coverage.
2025-04-14 12:15:45 +02:00
Rick Newton-Rogers 071d1cac3c
Enable macOS CI on pull requests (#170)
Motivation:

* Improve test coverage

Modifications:

Enable macOS CI to be run on pull request commits and make the use of
the nightly runner pool for main.yml jobs explicit.

Result:

Improved test coverage.
2025-04-01 12:20:11 +02:00
Rick Newton-Rogers 0556b16079
Enable macOS CI on merge to main and daily timer (#169)
Motivation:

* Improve test coverage
* Check test pass/fail status
* Monitor CI throughput

Modifications:

Enable macOS CI to be run on all merges to main and on a daily timer.

Result:

Improved test coverage run out-of-band at the moment so we can get a
feeling for if any changes need to be made in the repo or in the CI
pipelines to ensure timely and stable checks.
2025-03-31 13:29:44 +01:00
Franz Busch 44491db7cc
Fix 5.10 compile on Ubuntu 24.04 (#168)
Specifically Swift 5.10 on Intel on Ubuntu Noble (24.04) has a crazy bug
which leads to compilation failures in a #if compiler(>=6.0) block:
swiftlang/swift#79285 .

This workaround fixes the compilation by changing the whitespace.

Fixes https://github.com/apple/swift-metrics/issues/166
2025-03-18 16:21:52 +01:00
Franz Busch 3c0f419970
Add `Timer.measure` methods (#140)
# Motivation

This PR supersedes https://github.com/apple/swift-metrics/pull/135. The
goal is to make it easier to measure asynchronous code when using
`Metrics`.

# Modification

This PR does:
- Deprecate the current static method for measuring synchronous code
- Add a new instance method to measure synchronous code
- Add a new instance method to measure asynchronous code
2025-03-10 10:46:27 +01:00
Rick Newton-Rogers cbd39ceaca
Only apply standard swift settings on valid targets (#163)
Only apply standard swift settings on valid targets. The current check
ignores plugins but that is not comprehensive enough.
2025-03-07 19:34:49 +01:00
Rick Newton-Rogers 029e902273
Rename nightly_6_1 params to nightly_next (#162)
Rename nightly_6_1 params to nightly_next; see
https://github.com/apple/swift-nio/pull/3122
2025-03-03 14:46:47 +00:00
Cory Benfield 58f390a873
Tell the truth about the supported metric types (#161)
Our README claims we support 4 metric types, and then lists five. But it
fails to list FloatingPointCounter, which we also support, so the real
number is six.

While I was here I also fixed the DocC topic, which omitted
FloatingPointCounter from the list of metrics but _also_ omitted Gauge
for some weird reason.
2025-01-31 14:31:14 +00:00
Rick Newton-Rogers 53de3bfa9a
CI use 6.1 nightlies (#160)
CI use 6.1 nightlies now that Swift development is happening in the 6.1
branch
2025-01-30 09:59:23 +01:00
Marc Prud'hommeaux 5e63558d12
Android support (#159)
This PR makes the single `import Android` addition that is needed to get
this package building for Android. `skip android test` passes against
the emulator after I made this one change.
2025-01-09 22:49:28 +01:00
Franz Busch 726392cf4e
Update release.yml (#157)
Update the release.yml file with the latest label changes
2024-12-18 14:19:24 +01:00
Rick Newton-Rogers 4a491b5ad5
Enable MemberImportVisibility check on all targets (#156)
Enable MemberImportVisibility check on all targets. Use a standard
string header and footer to bracket the new block for ease of updating
in the future with scripts.
2024-12-16 10:59:35 +00:00
Honza Dvorsky 58e2968a7f
Enable strict concurrency (#155) 2024-11-29 23:28:08 +01:00
Franz Busch 4ec5a219dc
Aligning semantic version label check name (#154) 2024-11-28 12:01:57 +00:00
George Barnett c1976209b5
remove contributors script (#152)
remove contributors script
2024-11-15 09:51:00 +00:00
Rick Newton-Rogers cffefdc627 remove contributors script 2024-11-14 14:11:23 +00:00
Rick Newton-Rogers d720898dbf
Migrate to GitHub Actions (#151) 2024-11-06 22:32:20 +09:00
LamTrinh.Dev 569db3a632
Correct the link of sswg-security at SECURITY.md (#150) 2024-09-17 21:43:37 +09:00
Gustavo Cairo e0165b53d4
Fix Sendability warnings (#137)
* Fix Sendability warnings

* Apply suggestions from code review

* Fix another warning and @unknown default issue

---------

Co-authored-by: Konrad `ktoso` Malawski <konrad.malawski@project13.pl>
2024-06-21 17:18:13 +01:00
Franz Busch d067b0e0f7
Add missing `Sendable` annotations (#148)
# Motivation
We were missing a few `Sendable` annotations on our public types.

# Modification
This PR adds `Sendable` to the missing types.
2024-06-21 05:40:32 +09:00
Peter Adams 79e5fb4fe4
Raise minimum swift version to 5.8 (#147)
Motivation:

It's consistant with support policy (see https://github.com/apple/swift-metrics/pull/141).
It matches what's being tested.
It will make future changes adopting new features easier.

Modifications:

Bump swift tools version to 5.8
Remove linux tests and test discovery flag which are not required.

Result:

Will not longer build using swift before 5.8 - older compilers will find older
versions of this library when resolving versions.
2024-06-12 10:53:26 +01:00
Konrad `ktoso` Malawski ce594e71e9
fix availability of record(:Duration) API (#146) 2024-05-15 18:08:10 +09:00
hamzahrmalik 0124d9ad5c
Change Timer.record(duration: Duration) signature to avoid source breakage (#144) 2024-05-15 14:36:25 +09:00
Si Beaumont eb18581491
Support compiling against Musl (#142)
Co-authored-by: Konrad `ktoso` Malawski <konrad.malawski@project13.pl>
2024-05-14 13:16:51 +09:00
Si Beaumont 33d3f71e00
Update API breakage CI script (#143)
* Update API breakage CI script

* restate sendable conformance to avoid warning/as-error

---------

Co-authored-by: Konrad `ktoso` Malawski <konrad_malawski@apple.com>
2024-05-14 13:09:54 +09:00
Franz Busch 7e53749103
Align supported Swift versions with rest of the ecosystem (#141)
# Motivation

Most of the server ecosystem follows the supported Swift versions from `swift-nio`. This means supporting the latest 3 Swift releases.

# Modification

This PR drops support for all Swift versions before 5.8.

# Result

This makes maintaining this repository easier and allows us to clean up some no longer needed stuff.
2024-05-09 11:19:06 +09:00
Yim Lee 9c0646af4d
Add CI for Swift 5.10 (#138) 2024-01-23 08:24:32 -08:00
Natik Gadzhi 13ea1fe7fc
Introducing Timer.record(_ duration:), closes #114 (#133)
Motivation:

`Duration` is available starting Swift 5.7, and we
should probably support it in our convinience API.

Modifications:
- Timer.record(_ duration: Duration) implementation
- A unit test case
- Generated Linux tests
2023-06-28 14:56:55 -07:00
tomer doron 971ba26378
add increment and decrement public API to Meter (#132)
motivation: expose increment and decrement public APIs

changes:
* add increment and decrement public API to Meter
* improve the implementation of AccumulatingMeter and TestMeter
* add tests for new APIs
* refactor/modernize other tests
2023-06-21 14:15:23 -07:00
tomer doron bf7ea93e17
update code of conduct (#131)
motivation: align CoC across the swift project

changes: change CoC to link to the swift.org version
2023-06-06 10:00:48 -07:00
hamzahrmalik 3402510406
Make 'values' on TestCounter, TestRecorder and TestMeter public (#129)
Co-authored-by: Hamzah Malik <hamzah_malik@apple.com>
2023-05-26 14:49:59 +02:00
hamzahrmalik 8bcdb6e82b
MetricsTestKit: expose all metrics publicly. This allows users to run checks on counters when they might not know the exact label (#127)
Co-authored-by: Hamzah Malik <hamzah_malik@apple.com>
2023-05-26 14:02:46 +02:00
hamzahrmalik 9d5ff3d48f
Use the MetricsTestKit in the MetricsTests rather than using a copy of the TestMetrics utilities (#128)
Co-authored-by: Hamzah Malik <hamzah_malik@apple.com>
2023-05-26 13:25:08 +02:00
tomer doron 32eef8ae84
add "meter" - a new type of metric and metric handler (#123)
motivation: seperate gauge from recorder in a backwards compatible way

changes:
* add new meter and meter handler pair
* add increment and decrement to meter handler
* add default implementation based on recorder for backwards compatibility
* add and adjust tests
2023-05-24 13:55:56 -07:00
Fabian Fett 862b99bc11
Add Package.resolved to .gitignore (#125)
Co-authored-by: Konrad `ktoso` Malawski <konrad.malawski@project13.pl>
2023-05-05 11:19:02 +09:00
Fabian Fett 78b6238009
Use UUID instead of NSUUID in MetricsTests (#126) 2023-05-05 11:01:45 +09:00
Yim Lee a79936ffc1
Update CI (#124)
- Swift 5.8 docker images are available
- Add docker-compose file for Swift 5.9
2023-04-04 20:30:21 -07:00
Gustavo Cairo e8bced74bc
Propagate displayUnit when using MultiplexMetricsHandler (#122) 2023-02-01 11:42:39 -08:00
Yim Lee cbfde655cf
Add CI for Swift 5.8 and update nightly to Ubuntu 22.04 (#121) 2023-01-24 13:34:45 -08:00
Yim Lee 9b39d811a8
Add .spi.yml for Swift Package Index DocC support (#119) 2022-12-02 12:29:28 -08:00
Konrad `ktoso` Malawski bcea8c19fe
+docc prepare for docc publishing (#118)
* +docc prepare for docc publishing

* fix test discovery on 5.5 and 5.6

* Delete Package@swift-5.6.swift

* Update Package.swift

* Apply suggestions from code review

Co-authored-by: Yim Lee <yim_lee@apple.com>

Co-authored-by: Yim Lee <yim_lee@apple.com>
2022-08-24 11:20:06 +09:00
Fabian Fett bd1b935c8e
Make `MetricsFactory` Sendable (#116) 2022-07-21 10:53:47 +09:00
Fabian Fett 53be78637e
Remove testable import from TestMetrics. (#113) 2022-07-20 17:25:45 +09:00
tomer doron d885a4f5e9
adopt sendable (#109)
motivation: adjust to swift 5.6

changes:
* define sendable shims for protocols and structs that may be used in async context
* refactor Gauge to include rather than inherit Recorder
* adjust tests
* add a test to make sure no warning are emitted
2022-07-01 16:38:50 -07:00
tomer doron fd0ee6956b
better abstration for MetricsSystem state (#112)
motivation: prepare for sendable checks (they dont work well with static state)

changes:
* abstract the MetricsSystem state into a "boxed" class that handles the locking
* adjust call sites
2022-06-29 11:08:12 -07:00
Fabian Fett 1c1408bf8f
Expose `MetricsTestKit` as a product (#111) 2022-05-25 07:15:41 +09:00
Konrad `ktoso` Malawski eadb828f87
Import TestMetrics from swift-metrics-extras (#106)
* Import TestMetrics from swift-metrics-extras

* support the year 2021

* rename module
2022-03-18 12:02:04 +09:00
tomer doron e00284be24
ci update (#108)
motivation: 5.6 is out

changes:
* use release version of 5.6
* add docker setup for 5.7 (using nightly for now)
2022-03-16 21:04:42 -07:00
Cory Benfield ea66bbc2b5
Add 5.6 nightly CI (#105)
The 5.6 nightly images are avaialable, let's use them.
2022-01-18 11:52:54 -08:00
Konrad `ktoso` Malawski d09e751437 Update how we recommend including the dependency
Since otherwise current users get hit with warnings.
2021-12-17 10:36:44 +09:00
tomer doron 6bc8aa8a06
add docker-compose setup for 5.5 (#104)
motivation: tests on 5.5

changes: add docker compose file for testing on 5.5
2021-12-16 16:38:36 -08:00
tomer doron 495aca6d51
update doc generation script (#103)
motivation: source-kitten has changed how its invoked

changes:
* invoke source-kitten with the correct flags
* make location of source-kitten source checkout more robust
* udpate docker to install jazzy on on focal
2021-12-16 15:17:27 -08:00
Konrad `ktoso` Malawski 992b87907d [SSWG] document release process 2021-08-23 13:10:27 +09:00
Rauhul Varma 3edd2f57af update comment 2021-08-16 11:49:56 +09:00
Rauhul Varma 2c58b010a2 remove assertions 2021-08-16 11:49:56 +09:00
Rauhul Varma 42372a8598 Introduce FloatingPointCounter
motivation: It is not currently possible to record floating point values via the swift-metrics API even if the metrics backend supports it.

modifications: Adds a `FloatingPointCounter` type to allow users to accumulate non-integral metrics backed by a `FloatingPointCounterHandler`. Introduces a default implementation for creating and destroying `FloatingPointCounterHandler`s for metric backends that do not natively support floating point counters. On such backends, `FloatingPointCounter` is backed by a `AccumulatingRoundingFloatingPointCounter` which accumulates floating point values internally and record increments to a wrapped `CounterHandler` after crossing integer boundaries.

result: Users can create `FloatingPointCounter`s to record floating point values and get enhanced behavior for backends that support floating point values.
2021-08-16 11:49:56 +09:00
tomer doron 99a068b962 Update README.md 2021-06-21 11:24:47 +09:00
tomer doron 8ea359b532 Update README.md 2021-06-21 11:24:47 +09:00
tomer doron 2a7fd99ea8 Update README.md 2021-06-21 11:24:47 +09:00
tomer doron 1bb953eb76 Create SECURITY.md 2021-06-21 11:24:47 +09:00
tomer doron e2c4a510b6
update 5.4 to release docker image (#94)
motivation: 5.4 is out!

changes: use release docker images instead of nightly
2021-05-06 18:24:14 -07:00
James Sherlock c6cdc69235
[readme] Add OpenTelemetry to compatible libraries (#93)
Add OpenTelemetry to compatible libraries
2021-04-06 10:35:50 -07:00
tom doron de1a7d570b add docker setup for 5.4 and nightly
motivation: CI for 5.4 and latest

changes: add docker setup files for 5.4 and main nightly images
2021-02-27 10:58:36 +09:00
Yim Lee 923775a5f6
Use welcoming language (#90) 2021-01-22 00:42:50 -08:00
Konrad `ktoso` Malawski 68e6cb2938 Apply suggestions from code review 2020-11-17 10:07:04 +09:00
Konrad `ktoso` Malawski c793c35d07 Apply suggestions from code review
Co-authored-by: Yim Lee <yim_lee@apple.com>
2020-11-17 10:07:04 +09:00
Konrad `ktoso` Malawski 7274cf41ef +readme Cross link to extras 2020-11-17 10:07:04 +09:00
Jari (LotU) e382458581 Remove reader lock 2020-11-12 18:10:16 +09:00
Jari (LotU) 2b6e31e1b3 Expose lock functionality 2020-11-12 18:10:16 +09:00
Konrad `ktoso` Malawski f5ed78cb26 =core remove not used import (Dispatch) 2020-10-13 10:39:24 +09:00
Moritz Lang 44e8bfc7f5
Add Timer.recordInterval method (#83)
* Add Timer.recordInterval method

Co-authored-by: Konrad `ktoso` Malawski <ktoso@apple.com>
2020-10-12 12:18:21 -07:00
Konrad `ktoso` Malawski 5702ee1174
metrics should include their labels when printed to ease debugging (#82)
motivation: ease debugging

changes: confirm metrics types to CustomStringConvertible
2020-10-08 15:08:43 -07:00
tomer doron cf757fe4eb
remove symbolicate-linux-fatal from Docker (#81)
motivation: we are not actually using symbolicate-linux-fatal in any meaningful way in CI and it's pinned to the master branch which has been removed

changes: remove symbolicate-linux-fatal fetching from Docker
2020-10-07 16:46:50 -07:00
tomer doron 69cc955761 Swift 5.3 was released, use if for CI
motivation: Swift 5.3 was released, use if for CI

changes: update docker setup to use the release version of 5.3
2020-09-20 12:09:04 +09:00
Konrad `ktoso` Malawski b6c9d615de =readme Fix link to point at main branch 2020-09-08 14:56:01 +09:00
Konrad `ktoso` Malawski f46b1894aa Update CONTRIBUTING.md 2020-07-18 12:22:54 +09:00
Konrad `ktoso` Malawski 313c84fdec add sanity.sh info to contributing.md 2020-07-18 12:22:54 +09:00
tomer doron 56ae451f00
improve 5.3 docker setup (#71)
motivation: pin to 5.3 nighties 

changes: use swiftlang/swift:nightly-5.3-master-bionic instead of swiftlang/swift:nightly-master-bionic
2020-05-05 12:00:16 -07:00
tomer doron 41d2db7675
update ci setup (#67)
motivation: 5.2 adoption, prepare for 5.3

changes:
* add 5.2 docker-compose setup
* add 5.3 docker-compose setup (placeholder)
* remove 4.2 docker-compose setup
* format
2020-04-03 19:50:13 -07:00
Johannes Weiss a8db098592
Update README.md (#65) 2020-03-04 08:37:02 -08:00
41 changed files with 3200 additions and 1651 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

14
.github/release.yml vendored Normal file
View File

@ -0,0 +1,14 @@
changelog:
categories:
- title: SemVer Major
labels:
- ⚠️ semver/major
- title: SemVer Minor
labels:
- 🆕 semver/minor
- title: SemVer Patch
labels:
- 🔨 semver/patch
- title: Other Changes
labels:
- semver/none

26
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Main
on:
push:
branches: [main]
schedule:
- cron: "0 8,20 * * *"
jobs:
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
macos-tests:
name: macOS tests
uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
with:
runner_pool: nightly
build_scheme: swift-metrics-Package

34
.github/workflows/pull_request.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: PR
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
soundness:
name: Soundness
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
with:
license_header_check_project_name: "Swift Metrics API"
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
cxx-interop:
name: Cxx interop
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
macos-tests:
name: macOS tests
uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
with:
runner_pool: general
build_scheme: swift-metrics-Package

View File

@ -0,0 +1,18 @@
name: PR label
on:
pull_request:
types: [labeled, unlabeled, opened, reopened, synchronize]
jobs:
semver-label-check:
name: Semantic version label check
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check for Semantic Version label
uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
.SourceKitten
*.orig
.swiftpm
Package.resolved

35
.licenseignore Normal file
View File

@ -0,0 +1,35 @@
.gitignore
**/.gitignore
.licenseignore
.gitattributes
.git-blame-ignore-revs
.mailfilter
.mailmap
.spi.yml
.swift-format
.editorconfig
.github/*
*.md
*.txt
*.yml
*.yaml
*.json
Package.swift
**/Package.swift
Package@-*.swift
**/Package@-*.swift
Package.resolved
**/Package.resolved
Makefile
*.modulemap
**/*.modulemap
**/*.docc/*
*.xcprivacy
**/*.xcprivacy
*.symlink
**/*.symlink
Dockerfile
**/Dockerfile
Snippets/*
dev/git.commit.template
.unacceptablelanguageignore

4
.spi.yml Normal file
View File

@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [CoreMetrics, Metrics, MetricsTestKit]

68
.swift-format Normal file
View File

@ -0,0 +1,68 @@
{
"version" : 1,
"indentation" : {
"spaces" : 4
},
"tabWidth" : 4,
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"spacesAroundRangeFormationOperators" : false,
"indentConditionalCompilationBlocks" : false,
"indentSwitchCaseLabels" : false,
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : true,
"lineBreakBeforeEachGenericRequirement" : true,
"lineLength" : 120,
"maximumBlankLines" : 1,
"respectsExistingLineBreaks" : true,
"prioritizeKeepingFunctionOutputTogether" : true,
"noAssignmentInExpressions" : {
"allowedFunctions" : [
"XCTAssertNoThrow",
"XCTAssertThrowsError"
]
},
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLiteralForEmptyCollectionInit" : false,
"AlwaysUseLowerCamelCase" : false,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoAssignmentInExpressions" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OmitExplicitReturns" : true,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : true,
"ReplaceForEachWithForLoop" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"UseEarlyExits" : false,
"UseExplicitNilCheckInConditions" : false,
"UseLetInEveryBoundCaseVariable" : false,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : false,
"UseSynthesizedInitializer" : false,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
}
}

View File

@ -1,13 +0,0 @@
# file options
--exclude .build
# format options
--self insert
--patternlet inline
--stripunusedargs unnamed-only
--comments ignore
--ifdef no-indent
# rules

View File

@ -1,55 +1,3 @@
# Code of Conduct
To be a truly great community, SwiftMetrics needs to welcome developers from all walks of life,
with different backgrounds, and with a wide range of experience. A diverse and friendly
community will have more great ideas, more unique perspectives, and produce more great
code. We will work diligently to make the SwiftMetrics community welcoming to everyone.
To give clarity of what is expected of our members, SwiftMetrics has adopted the code of conduct
defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source
communities, and we think it articulates our values well. The full text is copied below:
### Contributor Code of Conduct v1.3
As contributors and maintainers of this project, and in the interest of fostering an open and
welcoming community, we pledge to respect all people who contribute through reporting
issues, posting feature requests, updating documentation, submitting pull requests or patches,
and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression, sexual
orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or
nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing others private information, such as physical or electronic addresses, without explicit permission
- Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments,
commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of
Conduct, or to ban temporarily or permanently any contributor for other behaviors that they
deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
consistently applying these principles to every aspect of managing this project. Project
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
from the project team.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting a project maintainer at [swift-server-conduct@group.apple.com](mailto:swift-server-conduct@group.apple.com). All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to the
circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter
of an incident.
*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).*
### Reporting
A working group of community members is committed to promptly addressing any [reported issues](mailto:swift-server-conduct@group.apple.com).
Working group members are volunteers appointed by the project lead, with a
preference for individuals with varied backgrounds and perspectives. Membership is expected
to change regularly, and may grow or shrink.
The code of conduct for this project can be found at https://swift.org/code-of-conduct.

View File

@ -60,10 +60,9 @@ We require that your commit messages match our template. The easiest way to do t
git config commit.template dev/git.commit.template
### Make sure Tests work on Linux
### Run CI checks locally
SwiftMetrics uses XCTest to run tests on both macOS and Linux. While the macOS version of XCTest is able to use the Objective-C runtime to discover tests at execution time, the Linux version is not.
For this reason, whenever you add new tests **you have to run a script** that generates the hooks needed to run those tests on Linux, or our CI will complain that the tests are not all present on Linux. To do this, merely execute `ruby ./scripts/generate_linux_tests.rb` at the root of the package and check the changes it made.
You can run the Github Actions workflows locally using [act](https://github.com/nektos/act). For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally).
## How to contribute your work

View File

@ -1,4 +1,4 @@
// swift-tools-version:4.2
// swift-tools-version:5.9
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
@ -20,19 +20,45 @@ let package = Package(
products: [
.library(name: "CoreMetrics", targets: ["CoreMetrics"]),
.library(name: "Metrics", targets: ["Metrics"]),
.library(name: "MetricsTestKit", targets: ["MetricsTestKit"]),
],
targets: [
.target(
name: "CoreMetrics",
dependencies: []
name: "CoreMetrics"
),
.target(
name: "Metrics",
dependencies: ["CoreMetrics"]
),
.target(
name: "MetricsTestKit",
dependencies: ["Metrics"]
),
.testTarget(
name: "MetricsTests",
dependencies: ["Metrics"]
dependencies: ["Metrics", "MetricsTestKit"]
),
]
)
for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(.enableExperimentalFeature("StrictConcurrency=complete"))
target.swiftSettings = settings
}
// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
for target in package.targets {
switch target.type {
case .regular, .test, .executable:
var settings = target.swiftSettings ?? []
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
target.swiftSettings = settings
case .macro, .plugin, .system, .binary:
() // not applicable
@unknown default:
() // we don't know what to do here, do nothing
}
}
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //

125
README.md
View File

@ -15,15 +15,20 @@ If you have a server-side Swift application, or maybe a cross-platform (e.g. Lin
To add a dependency on the metrics API package, you need to declare it in your `Package.swift`:
```swift
// As of May 5, 2019, SwiftMetrics' major stable release is 1.0.0
// To depend on this release, you can use
.package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"),
// swift-metrics 1.x and 2.x are almost API compatible, so most clients should use
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"),
```
and to your application/library target, add "Metrics" to your dependencies:
```swift
.target(name: "BestExampleApp", dependencies: ["Metrics"]),
.target(
name: "BestExampleApp",
dependencies: [
// ...
.product(name: "Metrics", package: "swift-metrics"),
]
),
```
### Emitting metrics information
@ -57,8 +62,13 @@ As the API has just launched, not many implementations exist yet. If you are int
- [SwiftPrometheus](https://github.com/MrLotU/SwiftPrometheus), support for [Prometheus](https://prometheus.io)
- [StatsD Client](https://github.com/apple/swift-statsd-client), support for StatsD
- [OpenTelemetry Swift](https://github.com/open-telemetry/opentelemetry-swift), support for [OpenTelemetry](https://opentelemetry.io/) which also implements other metrics and tracing backends
- Your library? [Get in touch!](https://forums.swift.org/c/server)
### Swift Metrics Extras
You may also be interested in some "extra" modules which are collected in the [Swift Metrics Extras](https://github.com/apple/swift-metrics-extras) repository.
## Detailed design
### Architecture
@ -80,7 +90,7 @@ This API was designed with the contributors to the Swift on Server community and
### Metric types
The API supports four metric types:
The API supports six 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.
@ -88,18 +98,30 @@ The API supports four metric types:
counter.increment(by: 100)
```
- `FloatingPointCounter`: A variation of a `Counter` that records a floating point value, instead of an integer.
```swift
floatingPointCounter.increment(by: 10.5)
```
`Gauge`: 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 a `Recorder` with a sample size of 1 that does not perform any aggregation.
```swift
gauge.record(100)
```
`Meter`: A Meter is similar to `Gauge` - a metric that represents a single numerical value that can arbitrarily go up and down. Meters 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. Unlike `Gauge`, `Meter` also supports atomic incerements and decerements.
```swift
meter.record(100)
```
`Recorder`: 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.
```swift
recorder.record(100)
```
`Gauge`: 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 a `Recorder` with a sample size of 1 that does not perform any aggregation.
```swift
gauge.record(100)
```
`Timer`: A timer collects observations within a time window (usually things like request duration) 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.
```swift
@ -110,7 +132,7 @@ timer.recordMilliseconds(100)
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 constructor for `Counter`, `Timer`, `Recorder` and `Gauge` provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. _Each application_ can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
As seen above, each constructor for `Counter`, `Gauge`, `Meter`, `Recorder` and `Timer` provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. _Each application_ can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
```swift
let metricsImplementation = MyFavoriteMetricsImplementation()
@ -124,10 +146,12 @@ Given the above, an implementation of a metric backend needs to conform to `prot
```swift
public protocol MetricsFactory {
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
func destroyCounter(_ handler: CounterHandler)
func destroyMeter(_ handler: MeterHandler)
func destroyRecorder(_ handler: RecorderHandler)
func destroyTimer(_ handler: TimerHandler)
}
@ -144,11 +168,14 @@ public protocol CounterHandler: AnyObject {
}
```
**Timer**
**Meter**
```swift
public protocol TimerHandler: AnyObject {
func recordNanoseconds(_ duration: Int64)
public protocol MeterHandler: AnyObject {
func set(_ value: Int64)
func set(_ value: Double)
func increment(by: Double)
func decrement(by: Double)
}
```
@ -161,9 +188,17 @@ public protocol RecorderHandler: AnyObject {
}
```
**Timer**
```swift
public protocol TimerHandler: AnyObject {
func recordNanoseconds(_ duration: Int64)
}
```
#### Dealing with Overflows
Implementaton of metric objects that deal with integers, like `Counter` and `Timer` should be careful with overflow. The expected behavior is to cap at `.max`, and never crash the program due to overflow . For example:
Implementation of metric objects that deal with integers, like `Counter` and `Timer` should be careful with overflow. The expected behavior is to cap at `.max`, and never crash the program due to overflow . For example:
```swift
class ExampleCounter: CounterHandler {
@ -191,9 +226,12 @@ class SimpleMetricsLibrary: MetricsFactory {
return ExampleCounter(label, dimensions)
}
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
return ExampleMeter(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)
return ExampleRecorder(label, dimensions, aggregate)
}
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
@ -202,7 +240,8 @@ class SimpleMetricsLibrary: MetricsFactory {
// implementation is stateless, so nothing to do on destroy calls
func destroyCounter(_ handler: CounterHandler) {}
func destroyRecorder(_ handler: RecorderHandler) {}
func destroyMeter(_ handler: TimerHandler) {}
func destroyRecorder(_ handler: RecorderHandler) {}
func destroyTimer(_ handler: TimerHandler) {}
private class ExampleCounter: CounterHandler {
@ -223,9 +262,32 @@ class SimpleMetricsLibrary: MetricsFactory {
}
}
private class ExampleRecorder: RecorderHandler {
private class ExampleMeter: MeterHandler {
init(_: String, _: [(String, String)]) {}
let lock = NSLock()
var _value: Double = 0
func set(_ value: Int64) {
self.set(Double(value))
}
func set(_ value: Double) {
self.lock.withLock { _value = value }
}
func increment(by value: Double) {
self.lock.withLock { self._value += value }
}
func decrement(by value: Double) {
self.lock.withLock { self._value -= value }
}
}
private class ExampleRecorder: RecorderHandler {
init(_: String, _: [(String, String)], _: Bool) {}
private let lock = NSLock()
var values = [(Int64, Double)]()
func record(_ value: Int64) {
@ -264,26 +326,23 @@ class SimpleMetricsLibrary: MetricsFactory {
}
}
private class ExampleGauge: RecorderHandler {
private class ExampleTimer: TimerHandler {
init(_: String, _: [(String, String)]) {}
let lock = NSLock()
var _value: Double = 0
func record(_ value: Int64) {
self.record(Double(value))
}
var _value: Int64 = 0
func record(_ value: Double) {
self.lock.withLock { _value = value }
}
}
private class ExampleTimer: ExampleRecorder, TimerHandler {
func recordNanoseconds(_ duration: Int64) {
super.record(duration)
self.lock.withLock { _value = duration }
}
}
}
```
## Security
Please see [SECURITY.md](SECURITY.md) for details on the security process.
## Getting involved
Do not hesitate to get in touch as well, over on https://forums.swift.org/c/server

32
RELEASING.md Normal file
View File

@ -0,0 +1,32 @@
# Release process
Swift metrics follows a relatively simple release process, outlined below.
### Issue and PR management basics
The overall goal of Issue and Pull Request management is to allow referring to them in the future, and being able to
consistently and easily find out in which release a certain issue was fixed.
- most work should have an associated GitHub issue, and a Pull Request resolving it should mark it so by e.g. using github's "resolves #1234" mechanism or otherwise make it clear which issue it resolves.
- when a PR is merged, the associated issue is closed and the issue is assigned to the milestone the change is going to be released in.
- if a pull request was made directly, and has no associated issue, the pull request is associated with the milestone instead.
- do not assign both an issue _and_ pull request about the same ticket to the same milestone as it may be confusing why a similar sounding issue was "solved twice".
### Release process
Once it is decided that a release should be cut, follow these steps to make sure the release is nice and clean.
In our example let's consider we're cutting a release for the version `1.2.3`.
- check all outstanding PRs, if any can be merged right away for this release, consider doing so,
- make sure all recently closed PRs or issues have been assigned to the milestone (assign them to the milestone `1.2.3` if not already done),
- create the "next" release milestone, for example `1.2.4` (or `1.3.0` if necessary) and move remaining issues to is,
- this way these tickets are carried over to the "next" release and are a bit easier to find and prioritize.
- close the current milestone (`1.2.3`),
- pull and tag the current commit with `1.2.3`
- prefer signing your tag (`git tag -s`) so it can be confirmed who performed the release and the tag is trustworthy,
- push the tag,
- update and upload the documentation,
- e.g. use jazzy to generate and push the documentation branch (TODO: more details here).
- finally, go to the GitHub releases page and [draft a new release](https://github.com/apple/swift-metrics/releases/new).

43
SECURITY.md Normal file
View File

@ -0,0 +1,43 @@
# Security
This document specifies the security process for the SwiftMetrics project.
## Disclosures
### Private Disclosure Process
The SwiftMetrics maintainers ask that known and suspected vulnerabilities be
privately and responsibly disclosed by emailing
[sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org)
with the all the required detail.
**Do not file a public issue.**
#### When to report a vulnerability
* You think you have discovered a potential security vulnerability in SwiftMetrics.
* You are unsure how a vulnerability affects SwiftMetrics.
#### What happens next?
* A member of the team will acknowledge receipt of the report within 3
working days (United States). This may include a request for additional
information about reproducing the vulnerability.
* We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the
vulnerability within 10 days of the report as per their [security
guidelines][sswg-security].
* Once we have identified a fix we may ask you to validate it. We aim to do this
within 30 days. In some cases this may not be possible, for example when the
vulnerability exists at the protocol level and the industry must coordinate on
the disclosure process.
* If a CVE number is required, one will be requested from [MITRE][mitre]
providing you with full credit for the discovery.
* We will decide on a planned release date and let you know when it is.
* Prior to release, we will inform major dependents that a security-related
patch is impending.
* Once the fix has been released we will publish a security advisory on GitHub
and in the Server → Security Updates category on the [Swift forums][swift-forums-sec].
[sswg]: https://github.com/swift-server/sswg
[sswg-security]: https://www.swift.org/sswg/security/
[swift-forums-sec]: https://forums.swift.org/c/server/security-updates/
[mitre]: https://cveform.mitre.org/

View File

@ -0,0 +1,99 @@
# ``CoreMetrics``
A Metrics API package for Swift.
## Overview
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](https://prometheus.io/), [Graphite](https://graphiteapp.org), publish over [statsd](https://github.com/statsd/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 SwiftMetrics itself, we need metrics compatible libraries which send the metrics over to backend such as the ones mentioned above. What SwiftMetrics provides today is covered in the [API docs](https://apple.github.io/swift-metrics/), but it will continue to evolve with community input.
## Getting started
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.
### Adding the dependency
To add a dependency on the metrics API package, you need to declare it in your `Package.swift`:
```swift
// swift-metrics 1.x and 2.x are almost API compatible, so most clients should use
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"),
```
and to your application/library target, add "Metrics" to your dependencies:
```swift
.target(
name: "BestExampleApp",
dependencies: [
// ...
.product(name: "Metrics", package: "swift-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.
SwiftMetrics 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 the `MetricsSystem.bootstrap` function at the beginning of the program:
```swift
MetricsSystem.bootstrap(SelectedMetricsImplementation())
```
This instructs the `MetricsSystem` to install `SelectedMetricsImplementation` (actual name will differ) as the metrics backend to use.
> Tip: Refer to the project's [README](https://github.com/apple/swift-metrics) for an up-to-date list of backend implementations.
### Swift Metrics Extras
You may also be interested in some "extra" modules which are collected in the [Swift Metrics Extras](https://github.com/apple/swift-metrics-extras) repository.
## 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 backends 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 to persist them. We think it's not feasible to wait for one metrics package to support everything that a specific deployment needs while still being simple 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, SwiftMetrics is a "metrics API package." SwiftMetrics can be configured (using `MetricsSystem.bootstrap`) 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/)
## Topics
### Metric types
- ``Counter``
- ``FloatingPointCounter``
- ``Meter``
- ``Recorder``
- ``Gauge``
- ``Timer``

View File

@ -26,29 +26,58 @@
//
//===----------------------------------------------------------------------===//
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(WASILibc)
// No locking on WASILibc
#elseif canImport(Darwin)
import Darwin
#else
#elseif os(Windows)
import WinSDK
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Android)
import Android
#elseif canImport(Musl)
import Musl
#else
#error("Unsupported runtime")
#endif
/// A threading lock based on `libpthread` instead of `libdispatch`.
///
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
/// of lock is safe to use with `libpthread`-based threading models, such as the
/// one used by NIO.
/// one used by NIO. On Windows, the lock is based on the substantially similar
/// `SRWLOCK` type.
internal final class Lock {
fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> = UnsafeMutablePointer.allocate(capacity: 1)
#if os(Windows)
fileprivate let mutex: UnsafeMutablePointer<SRWLOCK> =
UnsafeMutablePointer.allocate(capacity: 1)
#else
fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> =
UnsafeMutablePointer.allocate(capacity: 1)
#endif
/// Create a new lock.
public init() {
let err = pthread_mutex_init(self.mutex, nil)
precondition(err == 0, "pthread_mutex_init failed with error \(err)")
#if os(Windows)
InitializeSRWLock(self.mutex)
#else
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
let err = pthread_mutex_init(self.mutex, &attr)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
}
deinit {
#if os(Windows)
// SRWLOCK does not need to be free'd
#else
let err = pthread_mutex_destroy(self.mutex)
precondition(err == 0, "pthread_mutex_destroy failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
self.mutex.deallocate()
}
@ -57,8 +86,12 @@ internal final class Lock {
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
public func lock() {
#if os(Windows)
AcquireSRWLockExclusive(self.mutex)
#else
let err = pthread_mutex_lock(self.mutex)
precondition(err == 0, "pthread_mutex_lock failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
}
/// Release the lock.
@ -66,8 +99,12 @@ internal final class Lock {
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
public func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(self.mutex)
#else
let err = pthread_mutex_unlock(self.mutex)
precondition(err == 0, "pthread_mutex_unlock failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
}
}
@ -81,7 +118,7 @@ extension Lock {
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
func withLock<T>(_ body: () throws -> T) rethrows -> T {
self.lock()
defer {
self.unlock()
@ -91,70 +128,119 @@ extension Lock {
// specialise Void return (for performance)
@inlinable
internal func withLockVoid(_ body: () throws -> Void) rethrows {
func withLockVoid(_ body: () throws -> Void) rethrows {
try self.withLock(body)
}
}
/// A threading lock based on `libpthread` instead of `libdispatch`.
extension Lock: @unchecked Sendable {}
/// A reader/writer threading lock based on `libpthread` instead of `libdispatch`.
///
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
/// This object provides a lock on top of a single `pthread_rwlock_t`. This kind
/// of lock is safe to use with `libpthread`-based threading models, such as the
/// one used by NIO.
/// one used by NIO. On Windows, the lock is based on the substantially similar
/// `SRWLOCK` type.
internal final class ReadWriteLock {
fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> = UnsafeMutablePointer.allocate(capacity: 1)
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
fileprivate let rwlock: UnsafeMutablePointer<SRWLOCK> =
UnsafeMutablePointer.allocate(capacity: 1)
fileprivate var shared: Bool = true
#else
fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> =
UnsafeMutablePointer.allocate(capacity: 1)
#endif
/// Create a new lock.
public init() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
InitializeSRWLock(self.rwlock)
#else
let err = pthread_rwlock_init(self.rwlock, nil)
precondition(err == 0, "pthread_rwlock_init failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
}
deinit {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
// SRWLOCK does not need to be free'd
#else
let err = pthread_rwlock_destroy(self.rwlock)
precondition(err == 0, "pthread_rwlock_destroy failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
self.rwlock.deallocate()
}
/// Acquire a reader lock.
///
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
/// Whenever possible, consider using `withReaderLock` instead of this
/// method and `unlock`, to simplify lock handling.
public func lockRead() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
AcquireSRWLockShared(self.rwlock)
self.shared = true
#else
let err = pthread_rwlock_rdlock(self.rwlock)
precondition(err == 0, "pthread_rwlock_rdlock failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
}
/// Acquire a writer lock.
///
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
/// Whenever possible, consider using `withWriterLock` instead of this
/// method and `unlock`, to simplify lock handling.
public func lockWrite() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
AcquireSRWLockExclusive(self.rwlock)
self.shared = false
#else
let err = pthread_rwlock_wrlock(self.rwlock)
precondition(err == 0, "pthread_rwlock_wrlock failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
}
/// Release the lock.
///
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
/// Whenever possible, consider using `withReaderLock` and `withWriterLock`
/// instead of this method and `lockRead` and `lockWrite`, to simplify lock
/// handling.
public func unlock() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
if self.shared {
ReleaseSRWLockShared(self.rwlock)
} else {
ReleaseSRWLockExclusive(self.rwlock)
}
#else
let err = pthread_rwlock_unlock(self.rwlock)
precondition(err == 0, "pthread_rwlock_unlock failed with error \(err)")
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
}
}
extension ReadWriteLock {
/// Acquire the reader lock for the duration of the given block.
///
/// This convenience method should be preferred to `lock` and `unlock` in
/// most situations, as it ensures that the lock will be released regardless
/// of how `body` exits.
/// This convenience method should be preferred to `lockRead` and `unlock`
/// in most situations, as it ensures that the lock will be released
/// regardless of how `body` exits.
///
/// - Parameter body: The block to execute while holding the lock.
/// - Parameter body: The block to execute while holding the reader lock.
/// - Returns: The value returned by the block.
@inlinable
internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockRead()
defer {
self.unlock()
@ -164,14 +250,14 @@ extension ReadWriteLock {
/// Acquire the writer lock for the duration of the given block.
///
/// This convenience method should be preferred to `lock` and `unlock` in
/// most situations, as it ensures that the lock will be released regardless
/// of how `body` exits.
/// This convenience method should be preferred to `lockWrite` and `unlock`
/// in most situations, as it ensures that the lock will be released
/// regardless of how `body` exits.
///
/// - Parameter body: The block to execute while holding the lock.
/// - Parameter body: The block to execute while holding the writer lock.
/// - Returns: The value returned by the block.
@inlinable
internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockWrite()
defer {
self.unlock()
@ -181,13 +267,15 @@ extension ReadWriteLock {
// specialise Void return (for performance)
@inlinable
internal func withReaderLockVoid(_ body: () throws -> Void) rethrows {
func withReaderLockVoid(_ body: () throws -> Void) rethrows {
try self.withReaderLock(body)
}
// specialise Void return (for performance)
@inlinable
internal func withWriterLockVoid(_ body: () throws -> Void) rethrows {
func withWriterLockVoid(_ body: () throws -> Void) rethrows {
try self.withWriterLock(body)
}
}
extension ReadWriteLock: @unchecked Sendable {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
# ``Metrics``
A Metrics API package for Swift.
Refer to `CoreMetrics` module documentation for the majority of types.

View File

@ -11,12 +11,16 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
// swift-format-ignore-file
// Note: Whitespace changes are used to workaround compiler bug
// https://github.com/swiftlang/swift/issues/79285
@_exported import CoreMetrics
@_exported import class CoreMetrics.Timer
import Foundation
public extension Timer {
@_exported import class CoreMetrics.Timer
extension Timer {
/// Convenience for measuring duration of a closure.
///
/// - parameters:
@ -24,7 +28,11 @@ public extension 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 {
public static func measure<T>(
label: String,
dimensions: [(String, String)] = [],
body: @escaping () throws -> T
) rethrows -> T {
let timer = Timer(label: label, dimensions: dimensions)
let start = DispatchTime.now().uptimeNanoseconds
defer {
@ -33,15 +41,24 @@ public extension Timer {
}
return try body()
}
/// Record the time interval (with nanosecond precision) between the passed `since` dispatch time and `end` dispatch time.
///
/// - parameters:
/// - since: Start of the interval as `DispatchTime`.
/// - end: End of the interval, defaulting to `.now()`.
public func recordInterval(since: DispatchTime, end: DispatchTime = .now()) {
self.recordNanoseconds(end.uptimeNanoseconds - since.uptimeNanoseconds)
}
}
public extension Timer {
extension Timer {
/// Convenience for recording a duration based on TimeInterval.
///
/// - parameters:
/// - duration: The duration to record.
@inlinable
func record(_ duration: TimeInterval) {
public func record(_ duration: TimeInterval) {
self.recordSeconds(duration)
}
@ -50,7 +67,17 @@ public extension Timer {
/// - parameters:
/// - duration: The duration to record.
@inlinable
func record(_ duration: DispatchTimeInterval) {
public func record(_ duration: DispatchTimeInterval) {
// This wrapping in a optional is a workaround because DispatchTimeInterval
// is a non-frozen public enum and Dispatch is built with library evolution
// mode turned on.
// This means we should have an `@unknown default` case, but this breaks
// on non-Darwin platforms.
// Switching over an optional means that the `.none` case will map to
// `default` (which means we'll always have a valid case to go into
// the default case), but in reality this case will never exist as this
// optional will never be nil.
let duration = Optional(duration)
switch duration {
case .nanoseconds(let value):
self.recordNanoseconds(value)
@ -62,6 +89,72 @@ public extension Timer {
self.recordSeconds(value)
case .never:
self.record(0)
default:
self.record(0)
}
}
}
extension Timer {
/// Convenience for recording a duration based on `Duration`.
///
/// `Duration` will be converted to an `Int64` number of nanoseconds, and then recorded with nanosecond precision.
///
/// - Parameters:
/// - duration: The `Duration` to record.
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
@inlinable
public func record(duration: Duration) {
// `Duration` doesn't have a nice way to convert it nanoseconds or seconds,
// and manual conversion can overflow.
let seconds = duration.components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
guard !seconds.overflow else { return self.recordNanoseconds(Int64.max) }
let nanoseconds = seconds.partialValue.addingReportingOverflow(duration.components.attoseconds / 1_000_000_000)
guard !nanoseconds.overflow else { return self.recordNanoseconds(Int64.max) }
self.recordNanoseconds(nanoseconds.partialValue)
}
#if compiler(>=6.0)
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
// DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED
// https://github.com/swiftlang/swift/issues/79285
body: () throws(Failure) -> Result) throws(Failure) -> Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try body()
}
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - isolation: The isolation of the method. Defaults to the isolation of the caller.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
isolation: isolated (any Actor)? = #isolation,
// DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED
// https://github.com/swiftlang/swift/issues/79285
body: () async throws(Failure) -> sending Result) async throws(Failure) -> sending Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try await body()
}
#endif
}

View File

@ -0,0 +1,46 @@
# ``MetricsTestKit``
A set of tools for testing Metrics emitting libraries.
## Overview
This module offers a ``TestMetrics`` type which can be used to bootstrap the metrics system and then assert metric values on it.
## Example
```swift
import XCTest
import Metrics
import MetricsTestKit
final class ExampleTests: XCTestCase {
var metrics: TestMetrics! = TestMetrics()
override func setUp() {
MetricsSystem.bootstrapInternal(self.metrics)
}
override func tearDown() async throws {
self.metrics = nil
MetricsSystem.bootstrapInternal(NOOPMetricsHandler.instance)
}
func test_example() async throws {
// Create a metric using the bootstrapped test metrics backend:
Recorder(label: "example").record(100)
// extract the `TestRecorder` out of the
let recorder = try self.metrics.expectRecorder("example")
recorder.lastValue?.shouldEqual(6)
}
}
```
## Topics
### Test metrics
- ``TestCounter``
- ``TestMeter``
- ``TestRecorder``
- ``TestTimer``

View File

@ -0,0 +1,591 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2021 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 source file is part of the Swift Cluster Membership open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.md for the list of Swift Cluster Membership project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import CoreMetrics
import Metrics
import XCTest
/// Taken directly from `swift-cluster-memberships`'s own test target package, which
/// adopts the `TestMetrics` from `swift-metrics`.
///
/// Metrics factory which allows inspecting recorded metrics programmatically.
/// Only intended for tests of the Metrics API itself.
///
/// Created Handlers will store Metrics until they are explicitly destroyed.
///
public final class TestMetrics: MetricsFactory {
private let lock = NSLock()
public typealias Label = String
public typealias Dimensions = String
public struct FullKey: Sendable {
let label: Label
let dimensions: [(String, String)]
}
private var _counters = [FullKey: TestCounter]()
private var _meters = [FullKey: TestMeter]()
private var _recorders = [FullKey: TestRecorder]()
private var _timers = [FullKey: TestTimer]()
public init() {
// nothing to do
}
/// Reset method to destroy all created ``TestCounter``, ``TestMeter``, ``TestRecorder`` and ``TestTimer``.
/// Invoke this method in between test runs to verify that Counters are created as needed.
public func reset() {
self.lock.withLock {
self._counters = [:]
self._recorders = [:]
self._meters = [:]
self._timers = [:]
}
}
public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
self.lock.withLock { () -> CounterHandler in
if let existing = self._counters[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestCounter(label: label, dimensions: dimensions)
self._counters[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
self.lock.withLock { () -> MeterHandler in
if let existing = self._meters[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestMeter(label: label, dimensions: dimensions)
self._meters[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
self.lock.withLock { () -> RecorderHandler in
if let existing = self._recorders[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
self._recorders[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
self.lock.withLock { () -> TimerHandler in
if let existing = self._timers[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestTimer(label: label, dimensions: dimensions)
self._timers[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func destroyCounter(_ handler: CounterHandler) {
if let testCounter = handler as? TestCounter {
self.lock.withLock {
self._counters.removeValue(forKey: testCounter.key)
}
}
}
public func destroyMeter(_ handler: MeterHandler) {
if let testMeter = handler as? TestMeter {
self.lock.withLock { () in
self._meters.removeValue(forKey: testMeter.key)
}
}
}
public func destroyRecorder(_ handler: RecorderHandler) {
if let testRecorder = handler as? TestRecorder {
self.lock.withLock {
self._recorders.removeValue(forKey: testRecorder.key)
}
}
}
public func destroyTimer(_ handler: TimerHandler) {
if let testTimer = handler as? TestTimer {
self.lock.withLock {
self._timers.removeValue(forKey: testTimer.key)
}
}
}
}
extension TestMetrics.FullKey: Hashable {
public func hash(into hasher: inout Hasher) {
self.label.hash(into: &hasher)
for dim in self.dimensions {
dim.0.hash(into: &hasher)
dim.1.hash(into: &hasher)
}
}
public static func == (lhs: TestMetrics.FullKey, rhs: TestMetrics.FullKey) -> Bool {
lhs.label == rhs.label
&& Dictionary(uniqueKeysWithValues: lhs.dimensions) == Dictionary(uniqueKeysWithValues: rhs.dimensions)
}
}
// MARK: - Assertions
extension TestMetrics {
// MARK: - Counter
public func expectCounter(_ metric: Counter) throws -> TestCounter {
guard let counter = metric._handler as? TestCounter else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestCounter.self)")
}
return counter
}
public func expectCounter(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestCounter {
let maybeItem = self.lock.withLock {
self._counters[.init(label: label, dimensions: dimensions)]
}
guard let testCounter = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testCounter
}
/// All the counters which have been created and not destroyed
public var counters: [TestCounter] {
let counters = self.lock.withLock {
self._counters
}
return Array(counters.values)
}
// MARK: - Gauge
public func expectGauge(_ metric: Gauge) throws -> TestRecorder {
try self.expectRecorder(metric)
}
public func expectGauge(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestRecorder {
try self.expectRecorder(label, dimensions)
}
// MARK: - Meter
public func expectMeter(_ metric: Meter) throws -> TestMeter {
guard let meter = metric._handler as? TestMeter else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestMeter.self)")
}
return meter
}
public func expectMeter(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestMeter {
let maybeItem = self.lock.withLock {
self._meters[.init(label: label, dimensions: dimensions)]
}
guard let testMeter = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testMeter
}
/// All the meters which have been created and not destroyed
public var meters: [TestMeter] {
let meters = self.lock.withLock {
self._meters
}
return Array(meters.values)
}
// MARK: - Recorder
public func expectRecorder(_ metric: Recorder) throws -> TestRecorder {
guard let recorder = metric._handler as? TestRecorder else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestRecorder.self)")
}
return recorder
}
public func expectRecorder(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestRecorder {
let maybeItem = self.lock.withLock {
self._recorders[.init(label: label, dimensions: dimensions)]
}
guard let testRecorder = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testRecorder
}
/// All the recorders which have been created and not destroyed
public var recorders: [TestRecorder] {
let recorders = self.lock.withLock {
self._recorders
}
return Array(recorders.values)
}
// MARK: - Timer
public func expectTimer(_ metric: CoreMetrics.Timer) throws -> TestTimer {
guard let timer = metric._handler as? TestTimer else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestTimer.self)")
}
return timer
}
public func expectTimer(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestTimer {
let maybeItem = self.lock.withLock {
self._timers[.init(label: label, dimensions: dimensions)]
}
guard let testTimer = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testTimer
}
/// All the timers which have been created and not destroyed
public var timers: [TestTimer] {
let timers = self.lock.withLock {
self._timers
}
return Array(timers.values)
}
}
// MARK: - Metric type implementations
public protocol TestMetric {
associatedtype Value
var key: TestMetrics.FullKey { get }
var lastValue: Value? { get }
var last: (Date, Value)? { get }
}
public final class TestCounter: TestMetric, CounterHandler, Equatable {
public let id: String
public let label: String
public let dimensions: [(String, String)]
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = UUID().uuidString
self.label = label
self.dimensions = dimensions
}
public func increment(by amount: Int64) {
self.lock.withLock {
self._values.append((Date(), amount))
}
}
public func reset() {
self.lock.withLock {
self._values = []
}
}
public var lastValue: Int64? {
self.last?.1
}
public var totalValue: Int64 {
self.values.reduce(0, +)
}
public var last: (Date, Int64)? {
self.lock.withLock {
self._values.last
}
}
public var values: [Int64] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public static func == (lhs: TestCounter, rhs: TestCounter) -> Bool {
lhs.id == rhs.id
}
}
public final class TestMeter: TestMetric, MeterHandler, Equatable {
public let id: String
public let label: String
public let dimensions: [(String, String)]
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Double)]()
init(label: String, dimensions: [(String, String)]) {
self.id = UUID().uuidString
self.label = label
self.dimensions = dimensions
}
public func set(_ value: Int64) {
self.set(Double(value))
}
public func set(_ value: Double) {
self.lock.withLock {
// this may lose precision but good enough as an example
_values.append((Date(), Double(value)))
}
}
public func increment(by amount: Double) {
// Drop illegal values
// - cannot increment by NaN
guard !amount.isNaN else {
return
}
// - cannot increment by infinite quantities
guard !amount.isInfinite else {
return
}
// - cannot increment by negative values
guard amount.sign == .plus else {
return
}
// - cannot increment by zero
guard !amount.isZero else {
return
}
self.lock.withLock {
let lastValue: Double = self._values.last?.1 ?? 0
let newValue = lastValue + amount
_values.append((Date(), newValue))
}
}
public func decrement(by amount: Double) {
// Drop illegal values
// - cannot decrement by NaN
guard !amount.isNaN else {
return
}
// - cannot decrement by infinite quantities
guard !amount.isInfinite else {
return
}
// - cannot decrement by negative values
guard amount.sign == .plus else {
return
}
// - cannot decrement by zero
guard !amount.isZero else {
return
}
self.lock.withLock {
let lastValue: Double = self._values.last?.1 ?? 0
let newValue = lastValue - amount
_values.append((Date(), newValue))
}
}
public var lastValue: Double? {
self.last?.1
}
public var last: (Date, Double)? {
self.lock.withLock {
self._values.last
}
}
public var values: [Double] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public static func == (lhs: TestMeter, rhs: TestMeter) -> Bool {
lhs.id == rhs.id
}
}
public final class TestRecorder: TestMetric, RecorderHandler, Equatable {
public let id: String
public let label: String
public let dimensions: [(String, String)]
public let aggregate: Bool
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Double)]()
init(label: String, dimensions: [(String, String)], aggregate: Bool) {
self.id = UUID().uuidString
self.label = label
self.dimensions = dimensions
self.aggregate = aggregate
}
public func record(_ value: Int64) {
self.record(Double(value))
}
public func record(_ value: Double) {
self.lock.withLock {
// this may lose precision but good enough as an example
_values.append((Date(), Double(value)))
}
}
public var lastValue: Double? {
self.last?.1
}
public var last: (Date, Double)? {
self.lock.withLock {
self._values.last
}
}
public var values: [Double] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public static func == (lhs: TestRecorder, rhs: TestRecorder) -> Bool {
lhs.id == rhs.id
}
}
public final class TestTimer: TestMetric, TimerHandler, Equatable {
public let id: String
public let label: String
public var displayUnit: TimeUnit?
public let dimensions: [(String, String)]
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = UUID().uuidString
self.label = label
self.displayUnit = nil
self.dimensions = dimensions
}
public func preferDisplayUnit(_ unit: TimeUnit) {
self.lock.withLock {
self.displayUnit = unit
}
}
public func valueInPreferredUnit(atIndex i: Int) -> Double {
let value = self.values[i]
guard let displayUnit = self.displayUnit else {
return Double(value)
}
return Double(value) / Double(displayUnit.scaleFromNanoseconds)
}
public func recordNanoseconds(_ duration: Int64) {
self.lock.withLock {
_values.append((Date(), duration))
}
}
public var lastValue: Int64? {
self.last?.1
}
public var values: [Int64] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public var last: (Date, Int64)? {
self.lock.withLock {
self._values.last
}
}
public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool {
lhs.id == rhs.id
}
}
extension NSLock {
@discardableResult
fileprivate func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}
// MARK: - Errors
public enum TestMetricsError: Error {
case missingMetric(label: String, dimensions: [(String, String)])
case illegalMetricType(metric: Sendable, expected: String)
}
// MARK: - Sendable support
// ideally we would not be using @unchecked here, but concurrency-safety checks do not recognize locks
extension TestMetrics: @unchecked Sendable {}
extension TestCounter: @unchecked Sendable {}
extension TestMeter: @unchecked Sendable {}
extension TestRecorder: @unchecked Sendable {}
extension TestTimer: @unchecked Sendable {}

View File

@ -1,32 +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
//
//===----------------------------------------------------------------------===//
//
// LinuxMain.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.
///
#if os(Linux) || os(FreeBSD)
@testable import MetricsTests
XCTMain([
testCase(MetricsExtensionsTests.allTests),
testCase(MetricsTests.allTests),
])
#endif

View File

@ -1,48 +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
//
//===----------------------------------------------------------------------===//
//
// 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),
("testTimerOverflow", testTimerOverflow),
("testTimerHandlesUnsignedOverflow", testTimerHandlesUnsignedOverflow),
("testGauge", testGauge),
("testGaugeBlock", testGaugeBlock),
("testMUX", testMUX),
("testCustomFactory", testCustomFactory),
("testDestroyingGauge", testDestroyingGauge),
("testDestroyingCounter", testDestroyingCounter),
("testDestroyingTimer", testDestroyingTimer),
]
}
}

View File

@ -12,28 +12,30 @@
//
//===----------------------------------------------------------------------===//
@testable import CoreMetrics
import MetricsTestKit
import XCTest
@testable import CoreMetrics
class MetricsTests: XCTestCase {
func testCounters() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup()
let name = "counter-\(NSUUID().uuidString)"
let name = "counter-\(UUID().uuidString)"
let counter = Counter(label: name)
let testCounter = counter.handler as! TestCounter
let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total {
let testCounter = try metrics.expectCounter(counter)
let total = Int.random(in: 500...1000)
for _ in 0..<total {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
counter.increment(by: Int.random(in: 0 ... 1000))
group.leave()
defer { group.leave() }
counter.increment(by: Int.random(in: 0...1000))
}
}
group.wait()
XCTAssertEqual(testCounter.values.count - 1, total, "expected number of entries to match")
XCTAssertEqual(testCounter.values.count, total, "expected number of entries to match")
testCounter.reset()
XCTAssertEqual(testCounter.values.count, 0, "expected number of entries to match")
}
@ -43,34 +45,126 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "counter-\(NSUUID().uuidString)"
let value = Int.random(in: Int.min ... Int.max)
let name = "counter-\(UUID().uuidString)"
let value = Int.random(in: Int.min...Int.max)
Counter(label: name).increment(by: value)
let counter = metrics.counters[name] as! TestCounter
let counter = try metrics.expectCounter(name)
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
XCTAssertEqual(counter.values[0], Int64(value), "expected value to match")
counter.reset()
XCTAssertEqual(counter.values.count, 0, "expected number of entries to match")
}
func testDefaultFloatingPointCounter_ignoresNan() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: Double.nan)
fpCounter.increment(by: Double.signalingNaN)
XCTAssertEqual(counter.values.count, 0, "expected nan values to be ignored")
}
func testDefaultFloatingPointCounter_ignoresInfinity() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: Double.infinity)
fpCounter.increment(by: -Double.infinity)
XCTAssertEqual(counter.values.count, 0, "expected infinite values to be ignored")
}
func testDefaultFloatingPointCounter_ignoresNegativeValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: -100)
XCTAssertEqual(counter.values.count, 0, "expected negative values to be ignored")
}
func testDefaultFloatingPointCounter_ignoresZero() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: 0)
fpCounter.increment(by: -0)
XCTAssertEqual(counter.values.count, 0, "expected zero values to be ignored")
}
func testDefaultFloatingPointCounter_ceilsExtremeValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
// Just larger than Int64
fpCounter.increment(by: Double(sign: .plus, exponent: 63, significand: 1))
// Much larger than Int64
fpCounter.increment(by: Double.greatestFiniteMagnitude)
let values = counter.values
XCTAssertEqual(values.count, 2, "expected number of entries to match")
XCTAssertEqual(values, [Int64.max, Int64.max], "expected extremely large values to be replaced with Int64.max")
}
func testDefaultFloatingPointCounter_accumulatesFloatingPointDecimalValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let rawFpCounter = fpCounter._handler as! AccumulatingRoundingFloatingPointCounter
let counter = try metrics.expectCounter(label)
// Increment by a small value (perfectly representable)
fpCounter.increment(by: 0.75)
XCTAssertEqual(counter.values.count, 0, "expected number of entries to match")
// Increment by a small value that should grow the accumulated buffer past 1.0 (perfectly representable)
fpCounter.increment(by: 1.5)
var values = counter.values
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values, [2], "expected entries to match")
XCTAssertEqual(rawFpCounter.fraction, 0.25, "")
// Increment by a large value that should leave a fraction in the accumulator
// 1110506744053.76
fpCounter.increment(by: Double(sign: .plus, exponent: 40, significand: 1.01))
values = counter.values
XCTAssertEqual(values.count, 2, "expected number of entries to match")
XCTAssertEqual(values, [2, 1_110_506_744_054], "expected entries to match")
XCTAssertEqual(rawFpCounter.fraction, 0.010009765625, "expected fractional accumulated value")
}
func testRecorders() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup()
let name = "recorder-\(NSUUID().uuidString)"
let name = "recorder-\(UUID().uuidString)"
let recorder = Recorder(label: name)
let testRecorder = recorder.handler as! TestRecorder
let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total {
let testRecorder = try metrics.expectRecorder(recorder)
let total = Int.random(in: 500...1000)
for _ in 0..<total {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
recorder.record(Int.random(in: Int.min ... Int.max))
group.leave()
defer { group.leave() }
recorder.record(Int.random(in: Int.min...Int.max))
}
}
group.wait()
XCTAssertEqual(testRecorder.values.count - 1, total, "expected number of entries to match")
XCTAssertEqual(testRecorder.values.count, total, "expected number of entries to match")
}
func testRecordersInt() throws {
@ -78,14 +172,14 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let recorder = Recorder(label: "test-recorder")
let testRecorder = recorder.handler as! TestRecorder
let values = (0 ... 999).map { _ in Int32.random(in: Int32.min ... Int32.max) }
for i in 0 ... values.count - 1 {
let testRecorder = try metrics.expectRecorder(recorder)
let values = (0...999).map { _ in Int32.random(in: Int32.min...Int32.max) }
for i in 0..<values.count {
recorder.record(values[i])
}
XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
for i in 0 ... values.count - 1 {
XCTAssertEqual(Int32(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
for i in 0..<values.count {
XCTAssertEqual(Int32(testRecorder.values[i]), values[i], "expected value #\(i) to match.")
}
}
@ -94,14 +188,14 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let recorder = Recorder(label: "test-recorder")
let testRecorder = recorder.handler as! TestRecorder
let values = (0 ... 999).map { _ in Float.random(in: Float(Int32.min) ... Float(Int32.max)) }
for i in 0 ... values.count - 1 {
let testRecorder = try metrics.expectRecorder(recorder)
let values = (0...999).map { _ in Float.random(in: Float(Int32.min)...Float(Int32.max)) }
for i in 0..<values.count {
recorder.record(values[i])
}
XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
for i in 0 ... values.count - 1 {
XCTAssertEqual(Float(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
for i in 0..<values.count {
XCTAssertEqual(Float(testRecorder.values[i]), values[i], "expected value #\(i) to match.")
}
}
@ -110,12 +204,12 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "recorder-\(NSUUID().uuidString)"
let value = Double.random(in: Double(Int.min) ... Double(Int.max))
let name = "recorder-\(UUID().uuidString)"
let value = Double.random(in: Double(Int.min)...Double(Int.max))
Recorder(label: name).record(value)
let recorder = metrics.recorders[name] as! TestRecorder
let recorder = try metrics.expectRecorder(name)
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
}
func testTimers() throws {
@ -123,19 +217,19 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup()
let name = "timer-\(NSUUID().uuidString)"
let name = "timer-\(UUID().uuidString)"
let timer = Timer(label: name)
let testTimer = timer.handler as! TestTimer
let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total {
let testTimer = try metrics.expectTimer(timer)
let total = Int.random(in: 500...1000)
for _ in 0..<total {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
timer.recordNanoseconds(Int64.random(in: Int64.min ... Int64.max))
group.leave()
defer { group.leave() }
timer.recordNanoseconds(Int64.random(in: Int64.min...Int64.max))
}
}
group.wait()
XCTAssertEqual(testTimer.values.count - 1, total, "expected number of entries to match")
XCTAssertEqual(testTimer.values.count, total, "expected number of entries to match")
}
func testTimerBlock() throws {
@ -143,12 +237,12 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: Int64.min ... Int64.max)
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: Int64.min...Int64.max)
Timer(label: name).recordNanoseconds(value)
let timer = metrics.timers[name] as! TestTimer
let timer = try metrics.expectTimer(name)
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(timer.values[0].1, value, "expected value to match")
XCTAssertEqual(timer.values[0], value, "expected value to match")
}
func testTimerVariants() throws {
@ -157,27 +251,27 @@ class MetricsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
// nano
let nano = Int64.random(in: 0 ... 5)
let nano = Int64.random(in: 0...5)
timer.recordNanoseconds(nano)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0].1, nano, "expected value to match")
XCTAssertEqual(testTimer.values[0], nano, "expected value to match")
// micro
let micro = Int64.random(in: 0 ... 5)
let micro = Int64.random(in: 0...5)
timer.recordMicroseconds(micro)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1].1, micro * 1000, "expected value to match")
XCTAssertEqual(testTimer.values[1], micro * 1000, "expected value to match")
// milli
let milli = Int64.random(in: 0 ... 5)
let milli = Int64.random(in: 0...5)
timer.recordMilliseconds(milli)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2].1, milli * 1_000_000, "expected value to match")
XCTAssertEqual(testTimer.values[2], milli * 1_000_000, "expected value to match")
// seconds
let sec = Int64.random(in: 0 ... 5)
let sec = Int64.random(in: 0...5)
timer.recordSeconds(sec)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3].1, sec * 1_000_000_000, "expected value to match")
XCTAssertEqual(testTimer.values[3], sec * 1_000_000_000, "expected value to match")
}
func testTimerOverflow() throws {
@ -186,35 +280,35 @@ class MetricsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
// nano (integer)
timer.recordNanoseconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[0], Int64.max, "expected value to match")
// micro (integer)
timer.recordMicroseconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected value to match")
// micro (double)
timer.recordMicroseconds(Double(Int64.max) + 1)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected value to match")
// milli (integer)
timer.recordMilliseconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[2], Int64.max, "expected value to match")
// milli (double)
timer.recordMilliseconds(Double(Int64.max) + 1)
XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[2], Int64.max, "expected value to match")
// seconds (integer)
timer.recordSeconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 6, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
// seconds (double)
timer.recordSeconds(Double(Int64.max) * 1)
XCTAssertEqual(testTimer.values.count, 7, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
}
func testTimerHandlesUnsignedOverflow() throws {
@ -223,23 +317,23 @@ class MetricsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
// nano
timer.recordNanoseconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[0], Int64.max, "expected value to match")
// micro
timer.recordMicroseconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected value to match")
// milli
timer.recordMilliseconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[2], Int64.max, "expected value to match")
// seconds
timer.recordSeconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3].1, Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
}
func testGauge() throws {
@ -247,13 +341,13 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000)
let name = "gauge-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let gauge = Gauge(label: name)
gauge.record(value)
let recorder = gauge.handler as! TestRecorder
let recorder = try metrics.expectRecorder(gauge)
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
}
func testGaugeBlock() throws {
@ -261,61 +355,346 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000)
let name = "gauge-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
Gauge(label: name).record(value)
let recorder = metrics.recorders[name] as! TestRecorder
let recorder = try metrics.expectRecorder(name)
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
}
func testMUX() throws {
func testMeter() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let meter = Meter(label: name)
meter.set(value)
let testMeter = try metrics.expectMeter(meter)
XCTAssertEqual(testMeter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeter.values[0], value, "expected value to match")
}
func testMeterBlock() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
Meter(label: name).set(value)
let testMeter = try metrics.expectMeter(name)
XCTAssertEqual(testMeter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeter.values[0], value, "expected value to match")
}
func testMeterInt() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (0...999).map { _ in Int32.random(in: Int32.min...Int32.max) }
for i in 0..<values.count {
meter.set(values[i])
}
XCTAssertEqual(values.count, testMeter.values.count, "expected number of entries to match")
for i in 0..<values.count {
XCTAssertEqual(Int32(testMeter.values[i]), values[i], "expected value #\(i) to match.")
}
}
func testMeterFloat() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (0...999).map { _ in Float.random(in: Float(Int32.min)...Float(Int32.max)) }
for i in 0..<values.count {
meter.set(values[i])
}
XCTAssertEqual(values.count, testMeter.values.count, "expected number of entries to match")
for i in 0..<values.count {
XCTAssertEqual(Float(testMeter.values[i]), values[i], "expected value #\(i) to match.")
}
}
func testMeterIncrement() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let group = DispatchGroup()
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (500...1000).map { _ in Double.random(in: 0...Double(Int32.max)) }
for i in 0..<values.count {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
meter.increment(by: values[i])
}
}
group.wait()
XCTAssertEqual(testMeter.values.count, values.count, "expected number of entries to match")
XCTAssertEqual(testMeter.values.last!, values.reduce(0.0, +), accuracy: 0.1, "expected total value to match")
}
func testMeterDecrement() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let group = DispatchGroup()
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (500...1000).map { _ in Double.random(in: 0...Double(Int32.max)) }
for i in 0..<values.count {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
meter.decrement(by: values[i])
}
}
group.wait()
XCTAssertEqual(testMeter.values.count, values.count, "expected number of entries to match")
XCTAssertEqual(testMeter.values.last!, values.reduce(0.0, -), accuracy: 0.1, "expected total value to match")
}
func testDefaultMeterIgnoresNan() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: Double.nan)
meter.increment(by: Double.signalingNaN)
XCTAssertEqual(testMeter.values.count, 0, "expected nan values to be ignored")
}
func testDefaultMeterIgnoresInfinity() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: Double.infinity)
meter.increment(by: -Double.infinity)
XCTAssertEqual(testMeter.values.count, 0, "expected infinite values to be ignored")
}
func testDefaultMeterIgnoresNegativeValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: -100)
XCTAssertEqual(testMeter.values.count, 0, "expected negative values to be ignored")
}
func testDefaultMeterIgnoresZero() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: 0)
meter.increment(by: -0)
XCTAssertEqual(testMeter.values.count, 0, "expected zero values to be ignored")
}
func testMUX_Counter() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = NSUUID().uuidString
let value = Int.random(in: Int.min ... Int.max)
let mux = Counter(label: name)
mux.increment(by: value)
factories.forEach { factory in
let counter = factory.counters.first?.1 as! TestCounter
XCTAssertEqual(counter.label, name, "expected label to match")
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
let name = UUID().uuidString
let value = Int.random(in: Int.min...Int.max)
let muxCounter = Counter(label: name)
muxCounter.increment(by: value)
for factory in factories {
let counter = factory.counters.first
XCTAssertEqual(counter?.label, name, "expected label to match")
XCTAssertEqual(counter?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter?.lastValue, Int64(value), "expected value to match")
}
mux.reset()
factories.forEach { factory in
let counter = factory.counters.first?.1 as! TestCounter
XCTAssertEqual(counter.values.count, 0, "expected number of entries to match")
muxCounter.reset()
for factory in factories {
let counter = factory.counters.first
XCTAssertEqual(counter?.values.count, 0, "expected number of entries to match")
}
}
func testCustomFactory() {
class CustomHandler: CounterHandler {
func testMUX_Meter() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let value = Double.random(in: 0...1)
let muxMeter = Meter(label: name)
muxMeter.set(value)
for factory in factories {
let meter = factory.meters.first
XCTAssertEqual(meter?.label, name, "expected label to match")
XCTAssertEqual(meter?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(meter?.values[0], value, "expected value to match")
}
}
func testMUX_Recorder() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let value = Double.random(in: 0...1)
let muxRecorder = Recorder(label: name)
muxRecorder.record(value)
for factory in factories {
let recorder = factory.recorders.first
XCTAssertEqual(recorder?.label, name, "expected label to match")
XCTAssertEqual(recorder?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder?.values[0], value, "expected value to match")
}
}
func testMUX_Timer() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let seconds = Int.random(in: 1...10)
let muxTimer = Timer(label: name, preferredDisplayUnit: .minutes)
muxTimer.recordSeconds(seconds)
for factory in factories {
let timer = factory.timers.first
XCTAssertEqual(timer?.label, name, "expected label to match")
XCTAssertEqual(timer?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(timer?.values[0], Int64(seconds * 1_000_000_000), "expected value to match")
XCTAssertEqual(timer?.displayUnit, .minutes, "expected value to match")
XCTAssertEqual(
timer?.valueInPreferredUnit(atIndex: 0),
Double(seconds) / 60.0,
"seconds should be returned as minutes"
)
}
}
func testCustomHandler() {
final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
}
let counter1 = Counter(label: "foo")
XCTAssertFalse(counter1.handler is CustomHandler, "expected non-custom log handler")
XCTAssertFalse(counter1._handler is CustomHandler, "expected non-custom log handler")
let counter2 = Counter(label: "foo", dimensions: [], handler: CustomHandler())
XCTAssertTrue(counter2.handler is CustomHandler, "expected custom log handler")
XCTAssertTrue(counter2._handler is CustomHandler, "expected custom log handler")
}
func testCustomFactory() {
// @unchecked Sendable is okay here - locking is done manually.
final class CustomFactory: MetricsFactory, @unchecked Sendable {
init(handler: CustomHandler) {
self.handler = handler
}
final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
}
private let handler: CustomHandler
private let lock: NSLock = NSLock()
private var locked_didCallDestroyCounter: Bool = false
var didCallDestroyCounter: Bool {
self.lock.lock()
defer {
lock.unlock()
}
return self.locked_didCallDestroyCounter
}
func makeCounter(label: String, dimensions: [(String, String)]) -> any CoreMetrics.CounterHandler {
handler
}
func makeRecorder(
label: String,
dimensions: [(String, String)],
aggregate: Bool
) -> any CoreMetrics.RecorderHandler {
fatalError("Unsupported")
}
func makeTimer(label: String, dimensions: [(String, String)]) -> any CoreMetrics.TimerHandler {
fatalError("Unsupported")
}
func destroyCounter(_ handler: any CoreMetrics.CounterHandler) {
XCTAssertTrue(
handler === self.handler,
"The handler to be destroyed doesn't match the expected handler."
)
self.lock.lock()
defer {
lock.unlock()
}
self.locked_didCallDestroyCounter = true
}
func destroyRecorder(_ handler: any CoreMetrics.RecorderHandler) {
fatalError("Unsupported")
}
func destroyTimer(_ handler: any CoreMetrics.TimerHandler) {
fatalError("Unsupported")
}
}
let handler = CustomFactory.CustomHandler()
let factory = CustomFactory(handler: handler)
XCTAssertFalse(factory.didCallDestroyCounter)
do {
let counter1 = Counter(label: "foo", factory: factory)
XCTAssertTrue(counter1._handler is CustomFactory.CustomHandler, "expected a custom metrics handler")
XCTAssertTrue(counter1._factory is CustomFactory, "expected a custom metrics factory")
counter1.destroy()
}
XCTAssertTrue(factory.didCallDestroyCounter)
}
func testDestroyingGauge() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000)
let name = "gauge-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let gauge = Gauge(label: name)
gauge.record(value)
let recorder = gauge.handler as! TestRecorder
let recorder = try metrics.expectRecorder(gauge)
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values.first!.1, value, "expected value to match")
XCTAssertEqual(recorder.values.first, value, "expected value to match")
XCTAssertEqual(metrics.recorders.count, 1, "recorder should have been stored")
let identity = ObjectIdentifier(recorder)
@ -325,27 +704,65 @@ class MetricsTests: XCTestCase {
let gaugeAgain = Gauge(label: name)
gaugeAgain.record(-value)
let recorderAgain = gaugeAgain.handler as! TestRecorder
let recorderAgain = try metrics.expectRecorder(gaugeAgain)
XCTAssertEqual(recorderAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorderAgain.values.first!.1, -value, "expected value to match")
XCTAssertEqual(recorderAgain.values.first, -value, "expected value to match")
let identityAgain = ObjectIdentifier(recorderAgain)
XCTAssertNotEqual(identity, identityAgain, "since the cached metric was released, the created a new should have a different identity")
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
}
func testDestroyingMeter() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let meter = Meter(label: name)
meter.set(value)
let testMeter = try metrics.expectMeter(meter)
XCTAssertEqual(testMeter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeter.values.first, value, "expected value to match")
XCTAssertEqual(metrics.meters.count, 1, "recorder should have been stored")
let identity = ObjectIdentifier(testMeter)
meter.destroy()
XCTAssertEqual(metrics.recorders.count, 0, "recorder should have been released")
let meterAgain = Meter(label: name)
meterAgain.set(-value)
let testMeterAgain = try metrics.expectMeter(meterAgain)
XCTAssertEqual(testMeterAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeterAgain.values.first, -value, "expected value to match")
let identityAgain = ObjectIdentifier(testMeterAgain)
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
}
func testDestroyingCounter() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "counter-\(NSUUID().uuidString)"
let value = Int.random(in: 0 ... 1000)
let name = "counter-\(UUID().uuidString)"
let value = Int.random(in: 0...1000)
let counter = Counter(label: name)
counter.increment(by: value)
let testCounter = counter.handler as! TestCounter
let testCounter = try metrics.expectCounter(counter)
XCTAssertEqual(testCounter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testCounter.values.first!.1, Int64(value), "expected value to match")
XCTAssertEqual(testCounter.values.first, Int64(value), "expected value to match")
XCTAssertEqual(metrics.counters.count, 1, "counter should have been stored")
let identity = ObjectIdentifier(counter)
@ -355,27 +772,31 @@ class MetricsTests: XCTestCase {
let counterAgain = Counter(label: name)
counterAgain.increment(by: value)
let testCounterAgain = counterAgain.handler as! TestCounter
let testCounterAgain = try metrics.expectCounter(counterAgain)
XCTAssertEqual(testCounterAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testCounterAgain.values.first!.1, Int64(value), "expected value to match")
XCTAssertEqual(testCounterAgain.values.first, Int64(value), "expected value to match")
let identityAgain = ObjectIdentifier(counterAgain)
XCTAssertNotEqual(identity, identityAgain, "since the cached metric was released, the created a new should have a different identity")
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
}
func testDestroyingTimer() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: 0 ... 1000)
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: 0...1000)
let timer = Timer(label: name)
timer.recordNanoseconds(value)
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first!.1, value, "expected value to match")
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
let identity = ObjectIdentifier(timer)
@ -384,11 +805,35 @@ class MetricsTests: XCTestCase {
let timerAgain = Timer(label: name)
timerAgain.recordNanoseconds(value)
let testTimerAgain = timerAgain.handler as! TestTimer
let testTimerAgain = try metrics.expectTimer(timerAgain)
XCTAssertEqual(testTimerAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimerAgain.values.first!.1, value, "expected value to match")
XCTAssertEqual(testTimerAgain.values.first, value, "expected value to match")
let identityAgain = ObjectIdentifier(timerAgain)
XCTAssertNotEqual(identity, identityAgain, "since the cached metric was released, the created a new should have a different identity")
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
}
func testDescriptions() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let counter = Counter(label: "hello.counter")
XCTAssertEqual("\(counter)", "Counter(hello.counter, dimensions: [])")
let gauge = Gauge(label: "hello.gauge")
XCTAssertEqual("\(gauge)", "Gauge(hello.gauge, dimensions: [], aggregate: false)")
let meter = Meter(label: "hello.meter")
XCTAssertEqual("\(meter)", "Meter(hello.meter, dimensions: [])")
let timer = Timer(label: "hello.timer")
XCTAssertEqual("\(timer)", "Timer(hello.timer, dimensions: [])")
let recorder = Recorder(label: "hello.recorder")
XCTAssertEqual("\(recorder)", "Recorder(hello.recorder, dimensions: [], aggregate: true)")
}
}

View File

@ -1,35 +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
//
//===----------------------------------------------------------------------===//
//
// 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),
("testTimerUnits", testTimerUnits),
("testPreferDisplayUnit", testPreferDisplayUnit),
]
}
}

View File

@ -12,9 +12,11 @@
//
//===----------------------------------------------------------------------===//
import MetricsTestKit
import XCTest
@testable import CoreMetrics
@testable import Metrics
import XCTest
class MetricsExtensionsTests: XCTestCase {
func testTimerBlock() throws {
@ -22,14 +24,14 @@ class MetricsExtensionsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(NSUUID().uuidString)"
let name = "timer-\(UUID().uuidString)"
let delay = 0.05
Timer.measure(label: name) {
Thread.sleep(forTimeInterval: delay)
}
let timer = metrics.timers[name] as! TestTimer
let timer = try metrics.expectTimer(name)
XCTAssertEqual(1, timer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(timer.values[0].1, Int64(delay * 1_000_000_000), "expected delay to match")
XCTAssertGreaterThan(timer.values[0], Int64(delay * 1_000_000_000), "expected delay to match")
}
func testTimerWithTimeInterval() throws {
@ -38,11 +40,11 @@ class MetricsExtensionsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
let timeInterval = TimeInterval(Double.random(in: 1 ... 500))
let testTimer = try metrics.expectTimer(timer)
let timeInterval = TimeInterval(Double.random(in: 1...500))
timer.record(timeInterval)
XCTAssertEqual(1, testTimer.values.count, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match")
XCTAssertEqual(testTimer.values[0], Int64(timeInterval * 1_000_000_000), "expected value to match")
}
func testTimerWithDispatchTime() throws {
@ -51,95 +53,226 @@ class MetricsExtensionsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
// nano
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500))
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1...500))
timer.record(nano)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[0].1), nano.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[0]), nano.nano(), "expected value to match")
// micro
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500))
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1...500))
timer.record(micro)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[1].1), micro.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[1]), micro.nano(), "expected value to match")
// milli
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500))
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1...500))
timer.record(milli)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[2].1), milli.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[2]), milli.nano(), "expected value to match")
// seconds
let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500))
let sec = DispatchTimeInterval.seconds(Int.random(in: 1...500))
timer.record(sec)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[3].1), sec.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[3]), 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")
XCTAssertEqual(testTimer.values[4], 0, "expected value to match")
}
func testTimerWithDispatchTimeInterval() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)"
let timer = Timer(label: name)
let start = DispatchTime.now()
let end = DispatchTime(uptimeNanoseconds: start.uptimeNanoseconds + 1000 * 1000 * 1000)
timer.recordInterval(since: start, end: end)
let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(
UInt64(testTimer.values.first!),
end.uptimeNanoseconds - start.uptimeNanoseconds,
"expected value to match"
)
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
}
func testTimerDuration() throws {
// Wrapping only the insides of the test case so that the generated
// tests on Linux in MetricsTests+XCTest don't complain that the func does not exist.
guard #available(iOS 16, macOS 13, tvOS 15, watchOS 8, *) else {
throw XCTSkip("Timer.record(_ duration: Duration) is not available on this platform")
}
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)"
let timer = Timer(label: name)
let duration = Duration(secondsComponent: 3, attosecondsComponent: 123_000_000_000_000_000)
let nanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000
timer.record(duration: duration)
// Record a Duration that would overflow,
// expect Int64.max to be recorded.
timer.record(duration: Duration(secondsComponent: 10_000_000_000, attosecondsComponent: 123))
let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first, nanoseconds, "expected value to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected to record Int64.max if Durataion overflows")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
}
func testTimerUnits() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: 0 ... 1000)
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: 0...1000)
let timer = Timer(label: name)
timer.recordNanoseconds(value)
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first!.1, value, "expected value to match")
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
let secondsName = "timer-seconds-\(NSUUID().uuidString)"
let secondsValue = Int64.random(in: 0 ... 1000)
let secondsName = "timer-seconds-\(UUID().uuidString)"
let secondsValue = Int64.random(in: 0...1000)
let secondsTimer = Timer(label: secondsName, preferredDisplayUnit: .seconds)
secondsTimer.recordSeconds(secondsValue)
let testSecondsTimer = secondsTimer.handler as! TestTimer
let testSecondsTimer = try metrics.expectTimer(secondsTimer)
XCTAssertEqual(testSecondsTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(metrics.timers.count, 2, "timer should have been stored")
}
func testPreferDisplayUnit() {
func testPreferDisplayUnit() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let value = Double.random(in: 0 ... 1000)
let value = Double.random(in: 0...1000)
let timer = Timer(label: "test", preferredDisplayUnit: .seconds)
timer.recordSeconds(value)
let testTimer = timer.handler as! TestTimer
let testTimer = try metrics.expectTimer(timer)
testTimer.preferDisplayUnit(.nanoseconds)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000 * 1000, accuracy: 1.0, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value * 1000 * 1000 * 1000,
accuracy: 1.0,
"expected value to match"
)
testTimer.preferDisplayUnit(.microseconds)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000, accuracy: 0.001, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value * 1000 * 1000,
accuracy: 0.001,
"expected value to match"
)
testTimer.preferDisplayUnit(.milliseconds)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000, accuracy: 0.000001, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value * 1000,
accuracy: 0.000001,
"expected value to match"
)
testTimer.preferDisplayUnit(.seconds)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value, accuracy: 0.000000001, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value,
accuracy: 0.000000001,
"expected value to match"
)
testTimer.preferDisplayUnit(.minutes)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / 60, accuracy: 0.000000001, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value / 60,
accuracy: 0.000000001,
"expected value to match"
)
testTimer.preferDisplayUnit(.hours)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60), accuracy: 0.000000001, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value / (60 * 60),
accuracy: 0.000000001,
"expected value to match"
)
testTimer.preferDisplayUnit(.days)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match")
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value / (60 * 60 * 24),
accuracy: 0.000000001,
"expected value to match"
)
}
#if compiler(>=6.0)
func testTimerMeasure() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}
let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
@MainActor
func testTimerMeasureFromMainActor() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}
let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
#endif
}
// https://bugs.swift.org/browse/SR-6310
extension DispatchTimeInterval {
func nano() -> Int {
switch self {
// This wrapping in a optional is a workaround because DispatchTimeInterval
// is a non-frozen public enum and Dispatch is built with library evolution
// mode turned on.
// This means we should have an `@unknown default` case, but this breaks
// on non-Darwin platforms.
// Switching over an optional means that the `.none` case will map to
// `default` (which means we'll always have a valid case to go into
// the default case), but in reality this case will never exist as this
// optional will never be nil.
let interval = Optional(self)
switch interval {
case .nanoseconds(let value):
return value
case .microseconds(let value):
@ -150,6 +283,30 @@ extension DispatchTimeInterval {
return value * 1_000_000_000
case .never:
return 0
default:
return 0
}
}
}
#if swift(>=5.7)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
extension Swift.Duration {
fileprivate var nanosecondsClamped: Int64 {
let components = self.components
let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
let attosCompononentNanos = components.attoseconds / 1_000_000_000
let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos)
guard
!secondsComponentNanos.overflow,
!combinedNanos.overflow
else {
return .max
}
return combinedNanos.partialValue
}
}
#endif

View File

@ -1,187 +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
//
//===----------------------------------------------------------------------===//
@testable import CoreMetrics
@testable import class CoreMetrics.Timer
import Foundation
/// Metrics factory which allows inspecting recorded metrics programmatically.
/// Only intended for tests of the Metrics API itself.
internal final class TestMetrics: MetricsFactory {
private let lock = NSLock()
var counters = [String: CounterHandler]()
var recorders = [String: RecorderHandler]()
var timers = [String: TimerHandler]()
public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return self.make(label: label, dimensions: dimensions, registry: &self.counters, maker: TestCounter.init)
}
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let maker = { (label: String, dimensions: [(String, String)]) -> RecorderHandler in
TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
}
return self.make(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker)
}
public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return self.make(label: label, dimensions: dimensions, registry: &self.timers, maker: TestTimer.init)
}
private func make<Item>(label: String, dimensions: [(String, String)], registry: inout [String: Item], maker: (String, [(String, String)]) -> Item) -> Item {
return self.lock.withLock {
let item = maker(label, dimensions)
registry[label] = item
return item
}
}
func destroyCounter(_ handler: CounterHandler) {
if let testCounter = handler as? TestCounter {
self.counters.removeValue(forKey: testCounter.label)
}
}
func destroyRecorder(_ handler: RecorderHandler) {
if let testRecorder = handler as? TestRecorder {
self.recorders.removeValue(forKey: testRecorder.label)
}
}
func destroyTimer(_ handler: TimerHandler) {
if let testTimer = handler as? TestTimer {
self.timers.removeValue(forKey: testTimer.label)
}
}
}
internal class TestCounter: CounterHandler, Equatable {
let id: String
let label: String
let dimensions: [(String, String)]
let lock = NSLock()
var values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = NSUUID().uuidString
self.label = label
self.dimensions = dimensions
}
func increment(by amount: Int64) {
self.lock.withLock {
self.values.append((Date(), amount))
}
print("adding \(amount) to \(self.label)")
}
func reset() {
self.lock.withLock {
self.values = []
}
print("reseting \(self.label)")
}
public static func == (lhs: TestCounter, rhs: TestCounter) -> Bool {
return lhs.id == rhs.id
}
}
internal class TestRecorder: RecorderHandler, Equatable {
let id: String
let label: String
let dimensions: [(String, String)]
let aggregate: Bool
let lock = NSLock()
var values = [(Date, Double)]()
init(label: String, dimensions: [(String, String)], aggregate: Bool) {
self.id = NSUUID().uuidString
self.label = label
self.dimensions = dimensions
self.aggregate = aggregate
}
func record(_ value: Int64) {
self.record(Double(value))
}
func record(_ value: Double) {
self.lock.withLock {
// this may loose precision but good enough as an example
values.append((Date(), Double(value)))
}
print("recording \(value) in \(self.label)")
}
public static func == (lhs: TestRecorder, rhs: TestRecorder) -> Bool {
return lhs.id == rhs.id
}
}
internal class TestTimer: TimerHandler, Equatable {
let id: String
let label: String
var displayUnit: TimeUnit?
let dimensions: [(String, String)]
let lock = NSLock()
var values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = NSUUID().uuidString
self.label = label
self.displayUnit = nil
self.dimensions = dimensions
}
func preferDisplayUnit(_ unit: TimeUnit) {
self.lock.withLock {
self.displayUnit = unit
}
}
func retriveValueInPreferredUnit(atIndex i: Int) -> Double {
return self.lock.withLock {
let value = values[i].1
guard let displayUnit = self.displayUnit else {
return Double(value)
}
return Double(value) / Double(displayUnit.scaleFromNanoseconds)
}
}
func recordNanoseconds(_ duration: Int64) {
self.lock.withLock {
values.append((Date(), duration))
}
print("recording \(duration) \(self.label)")
}
public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool {
return lhs.id == rhs.id
}
}
private extension NSLock {
func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}

View File

@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2022 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 Dispatch
import MetricsTestKit
import XCTest
@testable import CoreMetrics
class SendableTest: XCTestCase {
func testSendableMetrics() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
do {
let name = "counter-\(UUID().uuidString)"
let value = Int.random(in: 0...1000)
let counter = Counter(label: name)
let task = Task.detached { () -> [Int64] in
counter.increment(by: value)
let handler = try metrics.expectCounter(counter)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], Int64(value), "expected value to match")
}
do {
let name = "floating-point-counter-\(UUID().uuidString)"
let value = Double.random(in: 0...0.9999)
let counter = FloatingPointCounter(label: name)
let task = Task.detached { () -> Double in
counter.increment(by: value)
let handler = counter._handler as! AccumulatingRoundingFloatingPointCounter
return handler.fraction
}
let fraction = await task.value
XCTAssertEqual(fraction, value)
}
do {
let name = "recorder-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let recorder = Recorder(label: name)
let task = Task.detached { () -> [Double] in
recorder.record(value)
let handler = try metrics.expectRecorder(recorder)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], value, "expected value to match")
}
do {
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let meter = Meter(label: name)
let task = Task.detached { () -> [Double] in
meter.set(value)
let handler = try metrics.expectMeter(meter)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], value, "expected value to match")
}
do {
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: 0...1000)
let timer = Timer(label: name)
let task = Task.detached { () -> [Int64] in
timer.recordNanoseconds(value)
let handler = try metrics.expectTimer(timer)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], value, "expected value to match")
}
}
}

View File

@ -1,37 +0,0 @@
ARG swift_version=5.0
ARG ubuntu_version=bionic
# backwards compatibility for 4.2 images
ARG image_version=$swift_version-$ubuntu_version
FROM swift:$image_version
# needed to do again after FROM due to docker limitation
ARG swift_version
ARG ubuntu_version
# set as UTF-8
RUN apt-get update && apt-get install -y locales locales-all
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
# dependencies
RUN apt-get update && apt-get install -y wget
RUN apt-get update && apt-get install -y lsof dnsutils netcat-openbsd net-tools curl jq # used by integration tests
# ruby and jazzy for docs generation
RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev
RUN gem install jazzy --no-ri --no-rdoc
# tools
RUN mkdir -p $HOME/.tools
RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile
# script to allow mapping framepointers on linux (until part of the toolchain)
RUN wget -q https://raw.githubusercontent.com/apple/swift/master/utils/symbolicate-linux-fatal -O $HOME/.tools/symbolicate-linux-fatal
RUN chmod 755 $HOME/.tools/symbolicate-linux-fatal
# swiftformat (until part of the toolchain)
ARG swiftformat_version=0.40.12
RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format
RUN cd $HOME/.tools/swift-format && swift build -c release
RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat

View File

@ -1,15 +0,0 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-4.2
build:
args:
image_version: "4.2"
test:
image: swift-metrics:18.04-4.2
shell:
image: swift-metrics:18.04-4.2

View File

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

View File

@ -1,18 +0,0 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-5.1
build:
args:
ubuntu_version: "bionic"
swift_version: "5.1"
test:
image: swift-metrics:18.04-5.1
environment: []
#- SANITIZER_ARG=--sanitize=thread
shell:
image: swift-metrics:18.04-5.1

View File

@ -1,43 +0,0 @@
# 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:z
working_dir: /code
cap_drop:
- CAP_NET_RAW
- CAP_NET_BIND_SERVICE
sanity:
<<: *common
command: /bin/bash -xcl "./scripts/sanity.sh"
docs:
<<: *common
environment:
- CI
command: /bin/bash -xcl "./scripts/generate_docs.sh"
test:
<<: *common
command: /bin/bash -xcl "swift test -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}"
# util
shell:
<<: *common
entrypoint: /bin/bash

View File

@ -1,94 +0,0 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 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 source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
function usage() {
echo "$0 [-u] version"
echo
echo "OPTIONS:"
echo " -u: Additionally upload the podspec"
}
upload=false
while getopts ":u" opt; do
case $opt in
u)
upload=true
;;
\?)
usage
exit 1
;;
esac
done
shift "$((OPTIND-1))"
if [[ $# -eq 0 ]]; then
echo "Must provide target version"
exit 1
fi
version=$1
name="SwiftMetrics"
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
tmpdir=$(mktemp -d /tmp/.build_podspecsXXXXXX)
echo "Building podspec in $tmpdir"
cat >> "${tmpdir}/${name}.podspec" <<- EOF
Pod::Spec.new do |s|
s.name = '$name'
s.version = '$version'
s.license = { :type => 'Apache 2.0', :file => 'LICENSE.txt' }
s.summary = 'A Metrics API for Swift.'
s.homepage = 'https://github.com/apple/swift-metrics'
s.author = 'Apple Inc.'
s.source = { :git => 'https://github.com/apple/swift-metrics.git', :tag => s.version.to_s }
s.documentation_url = 'https://apple.github.io/swift-metrics'
s.module_name = '$name'
s.swift_version = '4.2'
s.cocoapods_version = '>=1.6.0'
s.ios.deployment_target = '8.0'
s.osx.deployment_target = '10.9'
s.tvos.deployment_target = '9.0'
s.watchos.deployment_target = '2.0'
s.source_files = 'Sources/CoreMetrics/**/*.swift'
end
EOF
if $upload; then
echo "Uploading ${tmpdir}/${name}.podspec"
pod trunk push "${tmpdir}/${name}.podspec"
else
echo "Linting ${tmpdir}/${name}.podspec"
pod spec lint "${tmpdir}/${name}.podspec"
fi

View File

@ -1,136 +0,0 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 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 source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
# repodir
function all_modules() {
local repodir="$1"
(
set -eu
cd "$repodir"
swift package dump-package | jq '.products |
map(select(.type | has("library") )) |
map(.name) | .[]' | tr -d '"'
)
}
# repodir tag output
function build_and_do() {
local repodir=$1
local tag=$2
local output=$3
(
cd "$repodir"
git checkout -q "$tag"
swift build
while read -r module; do
swift api-digester -sdk "$sdk" -dump-sdk -module "$module" \
-o "$output/$module.json" -I "$repodir/.build/debug"
done < <(all_modules "$repodir")
)
}
function usage() {
echo >&2 "Usage: $0 REPO-GITHUB-URL NEW-VERSION OLD-VERSIONS..."
echo >&2
echo >&2 "This script requires a Swift 5.1+ toolchain."
echo >&2
echo >&2 "Examples:"
echo >&2
echo >&2 "Check between master and tag 2.1.1 of swift-nio:"
echo >&2 " $0 https://github.com/apple/swift-nio master 2.1.1"
echo >&2
echo >&2 "Check between HEAD and commit 64cf63d7 using the provided toolchain:"
echo >&2 " xcrun --toolchain org.swift.5120190702a $0 ../some-local-repo HEAD 64cf63d7"
}
if [[ $# -lt 3 ]]; then
usage
exit 1
fi
sdk=/
if [[ "$(uname -s)" == Darwin ]]; then
sdk=$(xcrun --show-sdk-path)
fi
hash jq 2> /dev/null || { echo >&2 "ERROR: jq must be installed"; exit 1; }
tmpdir=$(mktemp -d /tmp/.check-api_XXXXXX)
repo_url=$1
new_tag=$2
shift 2
repodir="$tmpdir/repo"
git clone "$repo_url" "$repodir"
git -C "$repodir" fetch -q origin '+refs/pull/*:refs/remotes/origin/pr/*'
errors=0
for old_tag in "$@"; do
mkdir "$tmpdir/api-old"
mkdir "$tmpdir/api-new"
echo "Checking public API breakages from $old_tag to $new_tag"
build_and_do "$repodir" "$new_tag" "$tmpdir/api-new/"
build_and_do "$repodir" "$old_tag" "$tmpdir/api-old/"
for f in "$tmpdir/api-new"/*; do
f=$(basename "$f")
report="$tmpdir/$f.report"
if [[ ! -f "$tmpdir/api-old/$f" ]]; then
echo "NOTICE: NEW MODULE $f"
continue
fi
echo -n "Checking $f... "
swift api-digester -sdk "$sdk" -diagnose-sdk \
--input-paths "$tmpdir/api-old/$f" -input-paths "$tmpdir/api-new/$f" 2>&1 \
> "$report" 2>&1
if ! shasum "$report" | grep -q cefc4ee5bb7bcdb7cb5a7747efa178dab3c794d5; then
echo ERROR
echo >&2 "=============================="
echo >&2 "ERROR: public API change in $f"
echo >&2 "=============================="
cat >&2 "$report"
errors=$(( errors + 1 ))
else
echo OK
fi
done
rm -rf "$tmpdir/api-new" "$tmpdir/api-old"
done
if [[ "$errors" == 0 ]]; then
echo "OK, all seems good"
fi
echo done
exit "$errors"

View File

@ -1,39 +0,0 @@
#!/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 )"
contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' )
cat > "$here/../CONTRIBUTORS.txt" <<- EOF
For the purpose of tracking copyright, this is the list of individuals and
organizations who have contributed source code to the Swift Metrics API.
For employees of an organization/company where the copyright of work done
by employees of that company is held by the company itself, only the company
needs to be listed here.
## COPYRIGHT HOLDERS
- Apple Inc. (all contributors with '@apple.com')
### Contributors
$contributors
**Updating this list**
Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\`
EOF

View File

@ -1,122 +0,0 @@
#!/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
jazzy_dir="$root_path/.build/jazzy"
rm -rf "$jazzy_dir"
mkdir -p "$jazzy_dir"
module_switcher="$jazzy_dir/README.md"
jazzy_args=(--clean
--author 'SwiftMetrics team'
--readme "$module_switcher"
--author_url https://github.com/apple/swift-metrics
--github_url https://github.com/apple/swift-metrics
--github-file-prefix https://github.com/apple/swift-metrics/tree/$version
--theme fullwidth
--xcodebuild-arguments -scheme,swift-metrics-Package)
cat > "$module_switcher" <<"EOF"
# SwiftMetrics Docs
SwiftMetrics is a Swift metrics API package.
To get started with SwiftMetrics, [`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)
SwiftMetrics 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 "$jazzy_dir/docs/$version/$module" --docset-path "$jazzy_dir/docset/$version/$module"
--module "$module" --module-version $version
--root-url "https://apple.github.io/swift-metrics/docs/$version/$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 [[ $PUSH == 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/$version"
rm -rf "docs/current"
cp -r "$jazzy_dir/docs/$version" 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

View File

@ -1,231 +0,0 @@
#!/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"
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"
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

View File

@ -1,153 +0,0 @@
#!/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
##
##===----------------------------------------------------------------------===##
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2019 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
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/'
}
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 format... "
FIRST_OUT="$(git status --porcelain)"
swiftformat . > /dev/null 2>&1
SECOND_OUT="$(git status --porcelain)"
if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then
printf "\033[0;31mformatting issues!\033[0m\n"
git --no-pager diff
exit 1
else
printf "\033[0;32mokay.\033[0m\n"
fi
printf "=> Checking license headers\n"
tmp=$(mktemp /tmp/.swift-metrics-sanity_XXXXXX)
for language in swift-or-c bash dtrace; do
printf " * $language... "
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) YEARS 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) YEARS 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) YEARS 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" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then
printf "\033[0;31mmissing headers in file '$line'!\033[0m\n"
diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp"
exit 1
fi
done
printf "\033[0;32mokay.\033[0m\n"
)
done
rm "$tmp"