├── .spi.yml ├── CODE_OF_CONDUCT.md ├── .gitignore ├── .mailmap ├── .editorconfig ├── Sources ├── Metrics │ ├── Docs.docc │ │ └── index.md │ └── Metrics.swift ├── CoreMetrics │ ├── Docs.docc │ │ ├── curation │ │ │ ├── counterhandler.md │ │ │ ├── gauge.md │ │ │ ├── recorderhandler.md │ │ │ ├── floatingpointcounterhandler.md │ │ │ ├── meterhandler.md │ │ │ ├── timerhandler.md │ │ │ ├── metricssystem.md │ │ │ ├── timeunit.md │ │ │ ├── counter.md │ │ │ ├── recorder.md │ │ │ ├── meter.md │ │ │ ├── floatingpointcounter.md │ │ │ ├── multiplexmetricshandler.md │ │ │ ├── metricsfactory.md │ │ │ ├── timer.md │ │ │ └── noopmetricshandler.md │ │ └── index.md │ └── Locks.swift └── MetricsTestKit │ ├── Docs.docc │ └── index.md │ └── TestMetrics.swift ├── dev └── git.commit.template ├── .github ├── release.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ ├── pull_request_label.yml │ ├── main.yml │ └── pull_request.yml ├── .licenseignore ├── CONTRIBUTORS.txt ├── NOTICE.txt ├── SECURITY.md ├── RELEASING.md ├── Package.swift ├── .swift-format ├── CONTRIBUTING.md ├── Tests └── MetricsTests │ ├── TestSendable.swift │ ├── GlobalMetricsSystemTests.swift │ ├── MetricsTests.swift │ └── CoreMetricsTests.swift ├── LICENSE.txt └── README.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [CoreMetrics, Metrics, MetricsTestKit] 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .xcode 6 | .SourceKitten 7 | *.orig 8 | .swiftpm 9 | Package.resolved 10 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | tomer doron 2 | Konrad `ktoso` Malawski 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /Sources/Metrics/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``Metrics`` 2 | 3 | A Metrics API package for Swift. 4 | 5 | Refer to the `CoreMetrics` module documentation for the majority of types. 6 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/counterhandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/CounterHandler`` 2 | 3 | ## Topics 4 | 5 | ### Updating a counter 6 | 7 | - ``increment(by:)`` 8 | - ``reset()`` 9 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/gauge.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/Gauge`` 2 | 3 | ## Topics 4 | 5 | ### Creating a gauge 6 | 7 | - ``init(label:dimensions:)`` 8 | - ``init(label:dimensions:factory:)`` 9 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/recorderhandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/RecorderHandler`` 2 | 3 | ## Topics 4 | 5 | ### Updating a recorder 6 | 7 | - ``record(_:)-8tmoj`` 8 | - ``record(_:)-8ck98`` 9 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/floatingpointcounterhandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/FloatingPointCounterHandler`` 2 | 3 | ## Topics 4 | 5 | ### Updating a floating-point counter 6 | 7 | - ``increment(by:)`` 8 | - ``reset()`` 9 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/meterhandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/MeterHandler`` 2 | 3 | ## Topics 4 | 5 | ### Updating a meter 6 | 7 | - ``increment(by:)`` 8 | - ``decrement(by:)`` 9 | - ``set(_:)-8u6gx`` 10 | - ``set(_:)-7rx8l`` 11 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/timerhandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/TimerHandler`` 2 | 3 | ## Topics 4 | 5 | ### Updating a timer 6 | 7 | - ``preferDisplayUnit(_:)-8st88`` 8 | - ``preferDisplayUnit(_:)`` 9 | - ``recordNanoseconds(_:)`` 10 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/metricssystem.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/MetricsSystem`` 2 | 3 | ## Topics 4 | 5 | ### Bootstraping 6 | 7 | - ``bootstrap(_:)`` 8 | 9 | ### Inspecting the system 10 | 11 | - ``factory`` 12 | 13 | ### Writing to the system 14 | 15 | - ``withWriterLock(_:)`` 16 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/timeunit.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/TimeUnit`` 2 | 3 | ### Choosing a unit of time 4 | 5 | - ``nanoseconds`` 6 | - ``microseconds`` 7 | - ``milliseconds`` 8 | - ``seconds`` 9 | - ``minutes`` 10 | - ``hours`` 11 | - ``days`` 12 | 13 | ### Scaling the unit of time 14 | 15 | - ``scaleFromNanoseconds`` 16 | -------------------------------------------------------------------------------- /dev/git.commit.template: -------------------------------------------------------------------------------- 1 | One line description of your change 2 | 3 | Motivation: 4 | 5 | Explain here the context, and why you're making that change. 6 | What is the problem you're trying to solve. 7 | 8 | Modifications: 9 | 10 | Describe the modifications you've done. 11 | 12 | Result: 13 | 14 | After your change, what will change. 15 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - ⚠️ semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - 🆕 semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - 🔨 semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _[One line description of your change]_ 2 | 3 | ### Motivation: 4 | 5 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ 6 | 7 | ### Modifications: 8 | 9 | _[Describe the modifications you've done.]_ 10 | 11 | ### Result: 12 | 13 | _[After your change, what will change.]_ 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | _[what you expected to happen]_ 3 | 4 | ### Actual behavior 5 | _[what actually happened]_ 6 | 7 | ### Steps to reproduce 8 | 9 | 1. ... 10 | 2. ... 11 | 12 | ### If possible, minimal yet complete reproducer code (or URL to code) 13 | 14 | _[anything to help us reproducing the issue]_ 15 | 16 | ### SwiftMetrics version/commit hash 17 | 18 | _[the SwiftMetrics tag/commit hash]_ 19 | 20 | ### Swift & OS version (output of `swift --version && uname -a`) 21 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/counter.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/Counter`` 2 | 3 | ## Topics 4 | 5 | ### Creating a counter 6 | 7 | - ``init(label:dimensions:)`` 8 | - ``init(label:dimensions:factory:)`` 9 | - ``init(label:dimensions:handler:)`` 10 | - ``init(label:dimensions:handler:factory:)`` 11 | 12 | ### Updating a counter 13 | 14 | - ``increment()`` 15 | - ``increment(by:)`` 16 | - ``reset()`` 17 | 18 | ### Inspecting a counter 19 | 20 | - ``dimensions`` 21 | - ``label`` 22 | 23 | ### Destroying a counter 24 | 25 | - ``destroy()`` 26 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/recorder.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/Recorder`` 2 | 3 | ## Topics 4 | 5 | ### Creating a recorder 6 | 7 | - ``init(label:dimensions:aggregate:)`` 8 | - ``init(label:dimensions:aggregate:factory:)`` 9 | - ``init(label:dimensions:aggregate:handler:)`` 10 | - ``init(label:dimensions:aggregate:handler:factory:)`` 11 | 12 | ### Updating a recorder 13 | 14 | - ``record(_:)-4v6dk`` 15 | - ``record(_:)-8gebr`` 16 | 17 | ### Inspecting a recorder 18 | 19 | - ``dimensions`` 20 | - ``aggregate`` 21 | - ``label`` 22 | 23 | ### Destroying a recorder 24 | 25 | - ``destroy()`` 26 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/meter.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/Meter`` 2 | 3 | ## Topics 4 | 5 | ### Creating a meter 6 | 7 | - ``init(label:dimensions:)`` 8 | - ``init(label:dimensions:factory:)`` 9 | - ``init(label:dimensions:handler:)`` 10 | - ``init(label:dimensions:handler:factory:)`` 11 | 12 | ### Updating a meter 13 | 14 | - ``increment()`` 15 | - ``increment(by:)`` 16 | - ``decrement()`` 17 | - ``decrement(by:)`` 18 | - ``set(_:)-8bkhi`` 19 | - ``set(_:)-6f38`` 20 | 21 | ### Inspecting a meter 22 | 23 | - ``dimensions`` 24 | - ``label`` 25 | 26 | ### Destroying a meter 27 | 28 | - ``destroy()`` 29 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/floatingpointcounter.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/FloatingPointCounter`` 2 | 3 | ## Topics 4 | 5 | ### Creating a floating-point counter 6 | 7 | - ``init(label:dimensions:)`` 8 | - ``init(label:dimensions:factory:)`` 9 | - ``init(label:dimensions:handler:)`` 10 | - ``init(label:dimensions:handler:factory:)`` 11 | 12 | ### Updating a floating-point counter 13 | 14 | - ``increment()`` 15 | - ``increment(by:)`` 16 | - ``reset()`` 17 | 18 | ### Inspecting a floating-point counter 19 | 20 | - ``dimensions`` 21 | - ``label`` 22 | 23 | ### Destroying a floating-point counter 24 | 25 | - ``destroy()`` 26 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_label.yml: -------------------------------------------------------------------------------- 1 | name: PR label 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | types: [labeled, unlabeled, opened, reopened, synchronize] 9 | 10 | jobs: 11 | semver-label-check: 12 | name: Semantic version label check 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 1 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - name: Check for Semantic Version label 21 | uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main 22 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | **/.gitignore 3 | .licenseignore 4 | .gitattributes 5 | .git-blame-ignore-revs 6 | .mailfilter 7 | .mailmap 8 | .spi.yml 9 | .swift-format 10 | .editorconfig 11 | .github/* 12 | *.md 13 | *.txt 14 | *.yml 15 | *.yaml 16 | *.json 17 | Package.swift 18 | **/Package.swift 19 | Package@-*.swift 20 | **/Package@-*.swift 21 | Package.resolved 22 | **/Package.resolved 23 | Makefile 24 | *.modulemap 25 | **/*.modulemap 26 | **/*.docc/* 27 | *.xcprivacy 28 | **/*.xcprivacy 29 | *.symlink 30 | **/*.symlink 31 | Dockerfile 32 | **/Dockerfile 33 | Snippets/* 34 | dev/git.commit.template 35 | .unacceptablelanguageignore 36 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/multiplexmetricshandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/MultiplexMetricsHandler`` 2 | 3 | ## Topics 4 | 5 | ### Creating a multiplex metrics handler 6 | 7 | - ``init(factories:)`` 8 | 9 | ### Creating handlers 10 | 11 | - ``makeCounter(label:dimensions:)`` 12 | - ``makeFloatingPointCounter(label:dimensions:)`` 13 | - ``makeMeter(label:dimensions:)`` 14 | - ``makeTimer(label:dimensions:)`` 15 | - ``makeRecorder(label:dimensions:aggregate:)`` 16 | 17 | ### Destroying handlers 18 | 19 | - ``destroyCounter(_:)`` 20 | - ``destroyFloatingPointCounter(_:)`` 21 | - ``destroyMeter(_:)`` 22 | - ``destroyTimer(_:)`` 23 | - ``destroyRecorder(_:)`` 24 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/metricsfactory.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/MetricsFactory`` 2 | 3 | ## Topics 4 | 5 | ### Creating handlers 6 | 7 | - ``makeCounter(label:dimensions:)`` 8 | - ``makeFloatingPointCounter(label:dimensions:)`` 9 | - ``makeFloatingPointCounter(label:dimensions:)-i3cj`` 10 | - ``makeMeter(label:dimensions:)`` 11 | - ``makeMeter(label:dimensions:)-1u7ae`` 12 | - ``makeTimer(label:dimensions:)`` 13 | - ``makeRecorder(label:dimensions:aggregate:)`` 14 | 15 | ### Destroying handlers 16 | 17 | - ``destroyCounter(_:)`` 18 | - ``destroyFloatingPointCounter(_:)`` 19 | - ``destroyFloatingPointCounter(_:)-8zdha`` 20 | - ``destroyMeter(_:)`` 21 | - ``destroyMeter(_:)-8alaj`` 22 | - ``destroyTimer(_:)`` 23 | - ``destroyRecorder(_:)`` 24 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to the Swift Metrics API. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Apple Inc. (all contributors with '@apple.com') 11 | 12 | ### Contributors 13 | 14 | - Cory Benfield 15 | - Jari (LotU) 16 | - Konrad `ktoso` Malawski 17 | - tomer doron 18 | 19 | **Updating this list** 20 | 21 | 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` 22 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/timer.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/Timer`` 2 | 3 | ## Topics 4 | 5 | ### Creating a timer 6 | 7 | - ``init(label:dimensions:)`` 8 | - ``init(label:dimensions:preferredDisplayUnit:)`` 9 | - ``init(label:dimensions:factory:)`` 10 | - ``init(label:dimensions:preferredDisplayUnit:factory:)`` 11 | - ``init(label:dimensions:handler:)`` 12 | - ``init(label:dimensions:handler:factory:)`` 13 | 14 | ### Updating a timer 15 | 16 | - ``recordNanoseconds(_:)-59nis`` 17 | - ``recordNanoseconds(_:)-232ir`` 18 | - ``recordMicroseconds(_:)-3qrc6`` 19 | - ``recordMicroseconds(_:)-35aca`` 20 | - ``recordMilliseconds(_:)-6kuy8`` 21 | - ``recordMilliseconds(_:)-1eh0n`` 22 | - ``recordSeconds(_:)-7vp6b`` 23 | - ``recordSeconds(_:)-740dd`` 24 | 25 | ### Inspecting a timer 26 | 27 | - ``dimensions`` 28 | - ``label`` 29 | 30 | ### Destroying a timer 31 | 32 | - ``destroy()`` 33 | 34 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/curation/noopmetricshandler.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics/NOOPMetricsHandler`` 2 | 3 | ## Topics 4 | 5 | ### Inspecting the handler 6 | 7 | - ``instance`` 8 | 9 | ### Creating handlers 10 | 11 | - ``makeCounter(label:dimensions:)`` 12 | - ``makeFloatingPointCounter(label:dimensions:)`` 13 | - ``makeMeter(label:dimensions:)`` 14 | - ``makeTimer(label:dimensions:)`` 15 | - ``makeRecorder(label:dimensions:aggregate:)`` 16 | 17 | ### No-op operatings 18 | 19 | - ``increment(by:)-3li5k`` 20 | - ``increment(by:)-7bah6`` 21 | - ``decrement(by:)`` 22 | - ``record(_:)-33tjf`` 23 | - ``record(_:)-2jh2w`` 24 | - ``recordNanoseconds(_:)`` 25 | - ``set(_:)-9r6pn`` 26 | - ``set(_:)-11de`` 27 | - ``reset()`` 28 | 29 | ### Destroying handlers 30 | 31 | - ``destroyCounter(_:)`` 32 | - ``destroyFloatingPointCounter(_:)`` 33 | - ``destroyMeter(_:)`` 34 | - ``destroyTimer(_:)`` 35 | - ``destroyRecorder(_:)`` 36 | 37 | -------------------------------------------------------------------------------- /Sources/MetricsTestKit/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``MetricsTestKit`` 2 | 3 | A set of tools for testing Metrics emitting libraries. 4 | 5 | ## Overview 6 | 7 | This module offers a ``TestMetrics`` type which you use to bootstrap the metrics system and assert metric values on it. 8 | 9 | ### Example 10 | 11 | ```swift 12 | import Metrics 13 | import MetricsTestKit 14 | import Testing 15 | 16 | struct ExampleTests { 17 | @Test func recorderWithCustomMetrics() async throws { 18 | // Create a local metrics object 19 | let metrics: TestMetrics = TestMetrics() 20 | 21 | // Explicitly use metrics object to create a recorder, 22 | // this allows you to avoid relying on the global system 23 | Recorder(label: "example", factory: metrics).record(300) 24 | 25 | // Extract the `TestRecorder` from the test metrics system 26 | let localRecorder = try metrics.expectRecorder("example") 27 | #expect(localRecorder.lastValue! == 300) 28 | } 29 | } 30 | ``` 31 | 32 | ## Topics 33 | 34 | ### Test metrics 35 | 36 | - ``TestCounter`` 37 | - ``TestMeter`` 38 | - ``TestRecorder`` 39 | - ``TestTimer`` 40 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | schedule: 10 | - cron: "0 8,20 * * *" 11 | 12 | jobs: 13 | unit-tests: 14 | name: Unit tests 15 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 16 | with: 17 | linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 18 | linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 19 | linux_6_2_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 20 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 21 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 22 | 23 | macos-tests: 24 | name: macOS tests 25 | uses: apple/swift-nio/.github/workflows/macos_tests.yml@main 26 | with: 27 | runner_pool: nightly 28 | build_scheme: swift-metrics-Package 29 | 30 | release-builds: 31 | name: Release builds 32 | uses: apple/swift-nio/.github/workflows/release_builds.yml@main 33 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The SwiftMetrics Project 3 | ======================== 4 | 5 | Please visit the SwiftMetrics web site for more information: 6 | 7 | * https://github.com/apple/swift-metrics 8 | 9 | Copyright 2018, 2019 The SwiftMetrics Project 10 | 11 | The SwiftMetrics Project licenses this file to you under the Apache License, 12 | version 2.0 (the "License"); you may not use this file except in compliance 13 | with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | Also, please refer to each LICENSE..txt file, which is located in 24 | the 'license' directory of the distribution file, for the license terms of the 25 | components that this product depends on. 26 | 27 | ------------------------------------------------------------------------------- 28 | 29 | This product contains a derivation of the lock implementation and various scripts from SwiftNIO. 30 | 31 | * LICENSE (Apache License 2.0): 32 | * https://www.apache.org/licenses/LICENSE-2.0 33 | * HOMEPAGE: 34 | * https://github.com/apple/swift-nio 35 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | soundness: 12 | name: Soundness 13 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 14 | with: 15 | license_header_check_project_name: "Swift Metrics API" 16 | docs_check_container_image: "swift:6.1-noble" 17 | 18 | unit-tests: 19 | name: Unit tests 20 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 21 | with: 22 | linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 23 | linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 24 | linux_6_2_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 25 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 26 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 27 | 28 | cxx-interop: 29 | name: Cxx interop 30 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 31 | 32 | macos-tests: 33 | name: macOS tests 34 | uses: apple/swift-nio/.github/workflows/macos_tests.yml@main 35 | with: 36 | runner_pool: general 37 | build_scheme: swift-metrics-Package 38 | 39 | release-builds: 40 | name: Release builds 41 | uses: apple/swift-nio/.github/workflows/release_builds.yml@main 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | This document specifies the security process for the SwiftMetrics project. 4 | 5 | ## Disclosures 6 | 7 | ### Private Disclosure Process 8 | 9 | The SwiftMetrics maintainers ask that known and suspected vulnerabilities be 10 | privately and responsibly disclosed by emailing 11 | [sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org) 12 | with the all the required detail. 13 | **Do not file a public issue.** 14 | 15 | #### When to report a vulnerability 16 | 17 | * You think you have discovered a potential security vulnerability in SwiftMetrics. 18 | * You are unsure how a vulnerability affects SwiftMetrics. 19 | 20 | #### What happens next? 21 | 22 | * A member of the team will acknowledge receipt of the report within 3 23 | working days (United States). This may include a request for additional 24 | information about reproducing the vulnerability. 25 | * We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the 26 | vulnerability within 10 days of the report as per their [security 27 | guidelines][sswg-security]. 28 | * Once we have identified a fix we may ask you to validate it. We aim to do this 29 | within 30 days. In some cases this may not be possible, for example when the 30 | vulnerability exists at the protocol level and the industry must coordinate on 31 | the disclosure process. 32 | * If a CVE number is required, one will be requested from [MITRE][mitre] 33 | providing you with full credit for the discovery. 34 | * We will decide on a planned release date and let you know when it is. 35 | * Prior to release, we will inform major dependents that a security-related 36 | patch is impending. 37 | * Once the fix has been released we will publish a security advisory on GitHub 38 | and in the Server → Security Updates category on the [Swift forums][swift-forums-sec]. 39 | 40 | [sswg]: https://github.com/swift-server/sswg 41 | [sswg-security]: https://www.swift.org/sswg/security/ 42 | [swift-forums-sec]: https://forums.swift.org/c/server/security-updates/ 43 | [mitre]: https://cveform.mitre.org/ 44 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | Swift metrics follows a relatively simple release process, outlined below. 4 | 5 | ### Issue and PR management basics 6 | 7 | The overall goal of Issue and Pull Request management is to allow referring to them in the future, and being able to 8 | consistently and easily find out in which release a certain issue was fixed. 9 | 10 | - 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. 11 | - 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. 12 | 13 | - if a pull request was made directly, and has no associated issue, the pull request is associated with the milestone instead. 14 | - 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". 15 | 16 | ### Release process 17 | 18 | Once it is decided that a release should be cut, follow these steps to make sure the release is nice and clean. 19 | 20 | In our example let's consider we're cutting a release for the version `1.2.3`. 21 | 22 | - check all outstanding PRs, if any can be merged right away for this release, consider doing so, 23 | - 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), 24 | - create the "next" release milestone, for example `1.2.4` (or `1.3.0` if necessary) and move remaining issues to is, 25 | - this way these tickets are carried over to the "next" release and are a bit easier to find and prioritize. 26 | - close the current milestone (`1.2.3`), 27 | - pull and tag the current commit with `1.2.3` 28 | - prefer signing your tag (`git tag -s`) so it can be confirmed who performed the release and the tag is trustworthy, 29 | - push the tag, 30 | - update and upload the documentation, 31 | - e.g. use jazzy to generate and push the documentation branch (TODO: more details here). 32 | - finally, go to the GitHub releases page and [draft a new release](https://github.com/apple/swift-metrics/releases/new). -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the Swift Metrics API open source project 5 | // 6 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import PackageDescription 17 | 18 | let package = Package( 19 | name: "swift-metrics", 20 | products: [ 21 | .library(name: "CoreMetrics", targets: ["CoreMetrics"]), 22 | .library(name: "Metrics", targets: ["Metrics"]), 23 | .library(name: "MetricsTestKit", targets: ["MetricsTestKit"]), 24 | ], 25 | targets: [ 26 | .target( 27 | name: "CoreMetrics" 28 | ), 29 | .target( 30 | name: "Metrics", 31 | dependencies: ["CoreMetrics"] 32 | ), 33 | .target( 34 | name: "MetricsTestKit", 35 | dependencies: ["Metrics"] 36 | ), 37 | .testTarget( 38 | name: "MetricsTests", 39 | dependencies: ["Metrics", "MetricsTestKit"] 40 | ), 41 | ] 42 | ) 43 | 44 | for target in package.targets { 45 | var settings = target.swiftSettings ?? [] 46 | settings.append(.enableExperimentalFeature("StrictConcurrency=complete")) 47 | target.swiftSettings = settings 48 | } 49 | 50 | // --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 51 | for target in package.targets { 52 | switch target.type { 53 | case .regular, .test, .executable: 54 | var settings = target.swiftSettings ?? [] 55 | // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md 56 | settings.append(.enableUpcomingFeature("MemberImportVisibility")) 57 | target.swiftSettings = settings 58 | case .macro, .plugin, .system, .binary: 59 | () // not applicable 60 | @unknown default: 61 | () // we don't know what to do here, do nothing 62 | } 63 | } 64 | // --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 65 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version" : 1, 3 | "indentation" : { 4 | "spaces" : 4 5 | }, 6 | "tabWidth" : 4, 7 | "fileScopedDeclarationPrivacy" : { 8 | "accessLevel" : "private" 9 | }, 10 | "spacesAroundRangeFormationOperators" : false, 11 | "indentConditionalCompilationBlocks" : false, 12 | "indentSwitchCaseLabels" : false, 13 | "lineBreakAroundMultilineExpressionChainComponents" : false, 14 | "lineBreakBeforeControlFlowKeywords" : false, 15 | "lineBreakBeforeEachArgument" : true, 16 | "lineBreakBeforeEachGenericRequirement" : true, 17 | "lineLength" : 120, 18 | "maximumBlankLines" : 1, 19 | "respectsExistingLineBreaks" : true, 20 | "prioritizeKeepingFunctionOutputTogether" : true, 21 | "noAssignmentInExpressions" : { 22 | "allowedFunctions" : [ 23 | "XCTAssertNoThrow", 24 | "XCTAssertThrowsError" 25 | ] 26 | }, 27 | "rules" : { 28 | "AllPublicDeclarationsHaveDocumentation" : false, 29 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 30 | "AlwaysUseLowerCamelCase" : false, 31 | "AmbiguousTrailingClosureOverload" : true, 32 | "BeginDocumentationCommentWithOneLineSummary" : false, 33 | "DoNotUseSemicolons" : true, 34 | "DontRepeatTypeInStaticProperties" : true, 35 | "FileScopedDeclarationPrivacy" : true, 36 | "FullyIndirectEnum" : true, 37 | "GroupNumericLiterals" : true, 38 | "IdentifiersMustBeASCII" : true, 39 | "NeverForceUnwrap" : false, 40 | "NeverUseForceTry" : false, 41 | "NeverUseImplicitlyUnwrappedOptionals" : false, 42 | "NoAccessLevelOnExtensionDeclaration" : true, 43 | "NoAssignmentInExpressions" : true, 44 | "NoBlockComments" : true, 45 | "NoCasesWithOnlyFallthrough" : true, 46 | "NoEmptyTrailingClosureParentheses" : true, 47 | "NoLabelsInCasePatterns" : true, 48 | "NoLeadingUnderscores" : false, 49 | "NoParensAroundConditions" : true, 50 | "NoVoidReturnOnFunctionSignature" : true, 51 | "OmitExplicitReturns" : true, 52 | "OneCasePerLine" : true, 53 | "OneVariableDeclarationPerLine" : true, 54 | "OnlyOneTrailingClosureArgument" : true, 55 | "OrderedImports" : true, 56 | "ReplaceForEachWithForLoop" : true, 57 | "ReturnVoidInsteadOfEmptyTuple" : true, 58 | "UseEarlyExits" : false, 59 | "UseExplicitNilCheckInConditions" : false, 60 | "UseLetInEveryBoundCaseVariable" : false, 61 | "UseShorthandTypeNames" : true, 62 | "UseSingleLinePropertyGetter" : false, 63 | "UseSynthesizedInitializer" : false, 64 | "UseTripleSlashForDocumentationComments" : true, 65 | "UseWhereClausesInForLoops" : false, 66 | "ValidateDocumentationComments" : false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to Apple and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license (see 6 | `LICENSE.txt`). 7 | 8 | ## How to submit a bug report 9 | 10 | Please ensure to specify the following: 11 | 12 | * SwiftMetrics commit hash 13 | * Contextual information (e.g. what you were trying to achieve with SwiftMetrics) 14 | * Simplest possible steps to reproduce 15 | * More complex the steps are, lower the priority will be. 16 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. 17 | * Anything that might be relevant in your opinion, such as: 18 | * Swift version or the output of `swift --version` 19 | * OS version and the output of `uname -a` 20 | * Network configuration 21 | 22 | ### Example 23 | 24 | ``` 25 | SwiftMetrics commit hash: b17a8a9f0f814c01a56977680cb68d8a779c951f 26 | 27 | Context: 28 | While testing my application that uses with SwiftMetrics, I noticed that ... 29 | 30 | Steps to reproduce: 31 | 1. ... 32 | 2. ... 33 | 3. ... 34 | 4. ... 35 | 36 | $ swift --version 37 | Swift version 4.0.2 (swift-4.0.2-RELEASE) 38 | Target: x86_64-unknown-linux-gnu 39 | 40 | Operating system: Ubuntu Linux 16.04 64-bit 41 | 42 | $ uname -a 43 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 44 | 45 | My system has IPv6 disabled. 46 | ``` 47 | 48 | ## Writing a Patch 49 | 50 | A good SwiftMetrics patch is: 51 | 52 | 1. Concise, and contains as few changes as needed to achieve the end result. 53 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 54 | 3. Documented, adding API documentation as needed to cover new functions and properties. 55 | 4. Accompanied by a great commit message, using our commit message template. 56 | 57 | ### Commit Message Template 58 | 59 | We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run: 60 | 61 | git config commit.template dev/git.commit.template 62 | 63 | ### Run CI checks locally 64 | 65 | You can run the Github Actions workflows locally using 66 | [act](https://github.com/nektos/act). To run all the jobs that run on a pull 67 | request, use the following command: 68 | 69 | ``` 70 | % act pull_request 71 | ``` 72 | 73 | To run just a single job, use `workflow_call -j `, and specify the inputs 74 | the job expects. For example, to run just shellcheck: 75 | 76 | ``` 77 | % act workflow_call -j soundness --input shell_check_enabled=true 78 | ``` 79 | 80 | To bind-mount the working directory to the container, rather than a copy, use 81 | `--bind`. For example, to run just the formatting, and have the results 82 | reflected in your working directory: 83 | 84 | ``` 85 | % act --bind workflow_call -j soundness --input format_check_enabled=true 86 | ``` 87 | 88 | If you'd like `act` to always run with certain flags, these can be be placed in 89 | an `.actrc` file either in the current working directory or your home 90 | directory, for example: 91 | 92 | ``` 93 | --container-architecture=linux/amd64 94 | --remote-name upstream 95 | --action-offline-mode 96 | ``` 97 | ## How to contribute your work 98 | 99 | Please open a pull request at https://github.com/apple/swift-metrics. Make sure the CI passes, and then wait for code review. 100 | -------------------------------------------------------------------------------- /Tests/MetricsTests/TestSendable.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Dispatch 16 | import Foundation 17 | import MetricsTestKit 18 | import Testing 19 | 20 | @testable import CoreMetrics 21 | 22 | struct SendableTest { 23 | @Test func sendableMetrics() async throws { 24 | // create our test metrics, avoid bootstrapping global MetricsSystem 25 | let metrics = TestMetrics() 26 | 27 | do { 28 | let name = "counter-\(UUID().uuidString)" 29 | let value = Int.random(in: 0...1000) 30 | let counter = Counter(label: name, factory: metrics) 31 | 32 | let task = Task.detached { () -> [Int64] in 33 | counter.increment(by: value) 34 | let handler = try metrics.expectCounter(counter) 35 | return handler.values 36 | } 37 | let values = try await task.value 38 | #expect(values.count == 1, "expected number of entries to match") 39 | #expect(values[0] == Int64(value), "expected value to match") 40 | } 41 | 42 | do { 43 | let name = "floating-point-counter-\(UUID().uuidString)" 44 | let value = Double.random(in: 0...0.9999) 45 | let counter = FloatingPointCounter(label: name, factory: metrics) 46 | 47 | let task = Task.detached { () -> Double in 48 | counter.increment(by: value) 49 | let handler = counter._handler as! AccumulatingRoundingFloatingPointCounter 50 | return handler.fraction 51 | } 52 | let fraction = await task.value 53 | #expect(fraction == value) 54 | } 55 | 56 | do { 57 | let name = "recorder-\(UUID().uuidString)" 58 | let value = Double.random(in: -1000...1000) 59 | let recorder = Recorder(label: name, factory: metrics) 60 | 61 | let task = Task.detached { () -> [Double] in 62 | recorder.record(value) 63 | let handler = try metrics.expectRecorder(recorder) 64 | return handler.values 65 | } 66 | let values = try await task.value 67 | #expect(values.count == 1, "expected number of entries to match") 68 | #expect(values[0] == value, "expected value to match") 69 | } 70 | 71 | do { 72 | let name = "meter-\(UUID().uuidString)" 73 | let value = Double.random(in: -1000...1000) 74 | let meter = Meter(label: name, factory: metrics) 75 | 76 | let task = Task.detached { () -> [Double] in 77 | meter.set(value) 78 | let handler = try metrics.expectMeter(meter) 79 | return handler.values 80 | } 81 | let values = try await task.value 82 | #expect(values.count == 1, "expected number of entries to match") 83 | #expect(values[0] == value, "expected value to match") 84 | } 85 | 86 | do { 87 | let name = "timer-\(UUID().uuidString)" 88 | let value = Int64.random(in: 0...1000) 89 | let timer = Timer(label: name, factory: metrics) 90 | 91 | let task = Task.detached { () -> [Int64] in 92 | timer.recordNanoseconds(value) 93 | let handler = try metrics.expectTimer(timer) 94 | return handler.values 95 | } 96 | let values = try await task.value 97 | #expect(values.count == 1, "expected number of entries to match") 98 | #expect(values[0] == value, "expected value to match") 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/MetricsTests/GlobalMetricsSystemTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import MetricsTestKit 17 | import Testing 18 | 19 | @testable import CoreMetrics 20 | @testable import Metrics 21 | 22 | struct GlobalMetricsSystemTests { 23 | let metrics = TestMetrics() 24 | 25 | init() async throws { 26 | // bootstrap global system with our test metrics 27 | MetricsSystem.bootstrapInternal(self.metrics) 28 | } 29 | 30 | @Test func counters() throws { 31 | let group = DispatchGroup() 32 | let name = "counter-\(UUID().uuidString)" 33 | let counter = Counter(label: name) 34 | let testCounter = try metrics.expectCounter(counter) 35 | let total = Int.random(in: 500...1000) 36 | for _ in 0.. Int64(delay * 1_000_000_000), "expected delay to match") 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``CoreMetrics`` 2 | 3 | A Metrics API package for Swift. 4 | 5 | ## Overview 6 | 7 | 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, and so on. 8 | 9 | This is a community-driven open-source project that actively seeks contributions, be it code, documentation, or ideas. 10 | Apart from contributing to SwiftMetrics itself, the project needs metrics-compatible libraries which send the metrics over to backend systems such as the ones mentioned above. 11 | What SwiftMetrics provides today is covered in the [API docs](https://apple.github.io/swift-metrics/), and evolves with community input. 12 | 13 | ### Getting started 14 | 15 | If you have a server-side Swift application, or maybe a cross-platform (for example, Linux and macOS) application or library, and you would like to emit metrics, targeting this metrics API package is a great idea. 16 | Below you'll find all you need to know to get started. 17 | 18 | ### Adding the dependency 19 | 20 | To add a dependency on the metrics API package, declare it in your `Package.swift`: 21 | 22 | ```swift 23 | // swift-metrics 1.x and 2.x are almost API compatible, so most clients should use 24 | .package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"), 25 | ``` 26 | 27 | and to your application/library target, add "Metrics" to your dependencies: 28 | 29 | ```swift 30 | .target( 31 | name: "BestExampleApp", 32 | dependencies: [ 33 | // ... 34 | .product(name: "Metrics", package: "swift-metrics"), 35 | ] 36 | ), 37 | ``` 38 | 39 | ### Emitting metrics information 40 | 41 | ```swift 42 | // 1) Import the metrics API package. 43 | import Metrics 44 | 45 | // 2) Create a concrete metric object, the label works similarly to a `DispatchQueue` label. 46 | let counter = Counter(label: "com.example.BestExampleApp.numberOfRequests") 47 | 48 | // 3) Use the metric. 49 | counter.increment() 50 | ``` 51 | 52 | ### Selecting a metrics backend implementation (applications only) 53 | 54 | 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 decide which metrics backend to use. Libraries should never change the metrics implementation, as that choice is owned by the application. 55 | 56 | SwiftMetrics only provides the metrics system API. As an application owner, you choose a metrics backend (such as the ones mentioned above) to make the metrics information useful. 57 | 58 | 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: 59 | 60 | ```swift 61 | MetricsSystem.bootstrap(SelectedMetricsImplementation()) 62 | ``` 63 | 64 | This instructs the `MetricsSystem` to install `SelectedMetricsImplementation` (the actual name will differ) as the metrics backend to use. 65 | 66 | > Tip: Refer to the project's [README](https://github.com/apple/swift-metrics) for an up-to-date list of backend implementations. 67 | 68 | ### Swift Metrics Extras 69 | 70 | 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. 71 | It provides additional helpers for recording, aggregating, and exporting metrics, including gathering common system metrics. 72 | 73 | ### API Architecture 74 | 75 | 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 provide metrics information. More concretely, all the metrics events from all libraries should end up in the same place: one of the backends mentioned above or wherever the application owner chooses. 76 | 77 | 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 that it's not feasible to wait for one metrics package to support everything while still being simple enough to use and remain performant. For this reason, the problem is split into two parts: 78 | 79 | 1. a metrics API 80 | 2. a metrics backend implementation 81 | 82 | This package only provides the metrics API, and therefore, SwiftMetrics is a "metrics API package." 83 | SwiftMetrics can be configured (using `MetricsSystem.bootstrap`) to use any compatible metrics backend implementation. 84 | This mechanism allows libraries to adopt the API and support the application choosing a compatible backend implementation without requiring any changes to the libraries. 85 | 86 | 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. 87 | 88 | [pitch](https://forums.swift.org/t/metrics/19353) | 89 | [discussion](https://forums.swift.org/t/discussion-server-metrics-api/) | 90 | [feedback](https://forums.swift.org/t/feedback-server-metrics-api/) 91 | 92 | ## Topics 93 | 94 | ### Metric types 95 | 96 | - ``Counter`` 97 | - ``FloatingPointCounter`` 98 | - ``Meter`` 99 | - ``Gauge`` 100 | - ``Recorder`` 101 | - ``Timer`` 102 | 103 | ### Metrics Handlers 104 | 105 | - ``CounterHandler`` 106 | - ``FloatingPointCounterHandler`` 107 | - ``MeterHandler`` 108 | - ``RecorderHandler`` 109 | - ``TimerHandler`` 110 | - ``MultiplexMetricsHandler`` 111 | - ``NOOPMetricsHandler`` 112 | 113 | ### Bootstraping 114 | 115 | - ``MetricsFactory`` 116 | - ``MetricsSystem`` 117 | 118 | ### Supporting Types 119 | 120 | - ``TimeUnit`` 121 | -------------------------------------------------------------------------------- /Sources/Metrics/Metrics.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | // swift-format-ignore-file 15 | // Note: Whitespace changes are used to workaround compiler bug 16 | // https://github.com/swiftlang/swift/issues/79285 17 | 18 | @_exported import CoreMetrics 19 | import Foundation 20 | 21 | @_exported import class CoreMetrics.Timer 22 | 23 | extension Timer { 24 | /// Convenience for measuring duration of a closure. 25 | /// 26 | /// - parameters: 27 | /// - label: The label for the Timer. 28 | /// - dimensions: The dimensions for the Timer. 29 | /// - body: Closure to run & record. 30 | @inlinable 31 | public static func measure( 32 | label: String, 33 | dimensions: [(String, String)] = [], 34 | body: @escaping () throws -> T 35 | ) rethrows -> T { 36 | return try measure( 37 | label: label, 38 | dimensions: dimensions, 39 | factory: MetricsSystem.factory, 40 | body: body 41 | ) 42 | } 43 | 44 | /// Convenience for measuring duration of a closure. 45 | /// 46 | /// - parameters: 47 | /// - label: The label for the Timer. 48 | /// - dimensions: The dimensions for the Timer. 49 | /// - factory: The custom metrics factory 50 | /// - body: Closure to run & record. 51 | @inlinable 52 | public static func measure( 53 | label: String, 54 | dimensions: [(String, String)] = [], 55 | factory: MetricsFactory, 56 | body: @escaping () throws -> T 57 | ) rethrows -> T { 58 | let timer = Timer(label: label, dimensions: dimensions, factory: factory) 59 | let start = DispatchTime.now().uptimeNanoseconds 60 | defer { 61 | let delta = DispatchTime.now().uptimeNanoseconds - start 62 | timer.recordNanoseconds(delta) 63 | } 64 | return try body() 65 | } 66 | 67 | /// Record the time interval (with nanosecond precision) between the passed `since` dispatch time and `end` dispatch time. 68 | /// 69 | /// - parameters: 70 | /// - since: Start of the interval as `DispatchTime`. 71 | /// - end: End of the interval, defaulting to `.now()`. 72 | public func recordInterval(since: DispatchTime, end: DispatchTime = .now()) { 73 | self.recordNanoseconds(end.uptimeNanoseconds - since.uptimeNanoseconds) 74 | } 75 | } 76 | 77 | extension Timer { 78 | /// Convenience for recording a duration based on TimeInterval. 79 | /// 80 | /// - parameters: 81 | /// - duration: The duration to record. 82 | @inlinable 83 | public func record(_ duration: TimeInterval) { 84 | self.recordSeconds(duration) 85 | } 86 | 87 | /// Convenience for recording a duration based on DispatchTimeInterval. 88 | /// 89 | /// - parameters: 90 | /// - duration: The duration to record. 91 | @inlinable 92 | public func record(_ duration: DispatchTimeInterval) { 93 | // This wrapping in a optional is a workaround because DispatchTimeInterval 94 | // is a non-frozen public enum and Dispatch is built with library evolution 95 | // mode turned on. 96 | // This means we should have an `@unknown default` case, but this breaks 97 | // on non-Darwin platforms. 98 | // Switching over an optional means that the `.none` case will map to 99 | // `default` (which means we'll always have a valid case to go into 100 | // the default case), but in reality this case will never exist as this 101 | // optional will never be nil. 102 | let duration = Optional(duration) 103 | switch duration { 104 | case .nanoseconds(let value): 105 | self.recordNanoseconds(value) 106 | case .microseconds(let value): 107 | self.recordMicroseconds(value) 108 | case .milliseconds(let value): 109 | self.recordMilliseconds(value) 110 | case .seconds(let value): 111 | self.recordSeconds(value) 112 | case .never: 113 | self.record(0) 114 | default: 115 | self.record(0) 116 | } 117 | } 118 | } 119 | 120 | extension Timer { 121 | /// Convenience for recording a duration based on `Duration`. 122 | /// 123 | /// `Duration` will be converted to an `Int64` number of nanoseconds, and then recorded with nanosecond precision. 124 | /// 125 | /// - Parameters: 126 | /// - duration: The `Duration` to record. 127 | @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) 128 | @inlinable 129 | public func record(duration: Duration) { 130 | // `Duration` doesn't have a nice way to convert it nanoseconds or seconds, 131 | // and manual conversion can overflow. 132 | let seconds = duration.components.seconds.multipliedReportingOverflow(by: 1_000_000_000) 133 | guard !seconds.overflow else { return self.recordNanoseconds(Int64.max) } 134 | 135 | let nanoseconds = seconds.partialValue.addingReportingOverflow(duration.components.attoseconds / 1_000_000_000) 136 | guard !nanoseconds.overflow else { return self.recordNanoseconds(Int64.max) } 137 | 138 | self.recordNanoseconds(nanoseconds.partialValue) 139 | } 140 | 141 | /// Convenience for measuring duration of a closure. 142 | /// 143 | /// - Parameters: 144 | /// - clock: The clock used for measuring the duration. Defaults to the continuous clock. 145 | /// - body: The closure to record the duration of. 146 | @inlinable 147 | @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) 148 | public func measure( 149 | clock: Clock = .continuous, 150 | body: () throws(Failure) -> Result 151 | ) throws(Failure) -> Result where Clock.Duration == Duration { 152 | let start = clock.now 153 | defer { 154 | self.record(duration: start.duration(to: clock.now)) 155 | } 156 | return try body() 157 | } 158 | 159 | /// Convenience for measuring duration of a closure. 160 | /// 161 | /// - Parameters: 162 | /// - clock: The clock used for measuring the duration. Defaults to the continuous clock. 163 | /// - isolation: The isolation of the method. Defaults to the isolation of the caller. 164 | /// - body: The closure to record the duration of. 165 | @inlinable 166 | @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) 167 | public func measure( 168 | clock: Clock = .continuous, 169 | isolation: isolated (any Actor)? = #isolation, 170 | body: () async throws(Failure) -> sending Result 171 | ) async throws(Failure) -> sending Result where Clock.Duration == Duration { 172 | let start = clock.now 173 | defer { 174 | self.record(duration: start.duration(to: clock.now)) 175 | } 176 | return try await body() 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/CoreMetrics/Locks.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | //===----------------------------------------------------------------------===// 16 | // 17 | // This source file is part of the SwiftNIO open source project 18 | // 19 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 20 | // Licensed under Apache License v2.0 21 | // 22 | // See LICENSE.txt for license information 23 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 24 | // 25 | // SPDX-License-Identifier: Apache-2.0 26 | // 27 | //===----------------------------------------------------------------------===// 28 | 29 | #if canImport(WASILibc) 30 | // No locking on WASILibc 31 | #elseif canImport(Darwin) 32 | import Darwin 33 | #elseif os(Windows) 34 | import WinSDK 35 | #elseif canImport(Glibc) 36 | import Glibc 37 | #elseif canImport(Android) 38 | import Android 39 | #elseif canImport(Musl) 40 | import Musl 41 | #else 42 | #error("Unsupported runtime") 43 | #endif 44 | 45 | /// A threading lock based on `libpthread` instead of `libdispatch`. 46 | /// 47 | /// This object provides a lock on top of a single `pthread_mutex_t`. This kind 48 | /// of lock is safe to use with `libpthread`-based threading models, such as the 49 | /// one used by NIO. On Windows, the lock is based on the substantially similar 50 | /// `SRWLOCK` type. 51 | internal final class Lock { 52 | #if os(Windows) 53 | fileprivate let mutex: UnsafeMutablePointer = 54 | UnsafeMutablePointer.allocate(capacity: 1) 55 | #else 56 | fileprivate let mutex: UnsafeMutablePointer = 57 | UnsafeMutablePointer.allocate(capacity: 1) 58 | #endif 59 | 60 | /// Create a new lock. 61 | public init() { 62 | #if os(Windows) 63 | InitializeSRWLock(self.mutex) 64 | #else 65 | var attr = pthread_mutexattr_t() 66 | pthread_mutexattr_init(&attr) 67 | pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) 68 | 69 | let err = pthread_mutex_init(self.mutex, &attr) 70 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 71 | #endif 72 | } 73 | 74 | deinit { 75 | #if os(Windows) 76 | // SRWLOCK does not need to be free'd 77 | #else 78 | let err = pthread_mutex_destroy(self.mutex) 79 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 80 | #endif 81 | self.mutex.deallocate() 82 | } 83 | 84 | /// Acquire the lock. 85 | /// 86 | /// Whenever possible, consider using `withLock` instead of this method and 87 | /// `unlock`, to simplify lock handling. 88 | public func lock() { 89 | #if os(Windows) 90 | AcquireSRWLockExclusive(self.mutex) 91 | #else 92 | let err = pthread_mutex_lock(self.mutex) 93 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 94 | #endif 95 | } 96 | 97 | /// Release the lock. 98 | /// 99 | /// Whenever possible, consider using `withLock` instead of this method and 100 | /// `lock`, to simplify lock handling. 101 | public func unlock() { 102 | #if os(Windows) 103 | ReleaseSRWLockExclusive(self.mutex) 104 | #else 105 | let err = pthread_mutex_unlock(self.mutex) 106 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 107 | #endif 108 | } 109 | } 110 | 111 | extension Lock { 112 | /// Acquire the lock for the duration of the given block. 113 | /// 114 | /// This convenience method should be preferred to `lock` and `unlock` in 115 | /// most situations, as it ensures that the lock will be released regardless 116 | /// of how `body` exits. 117 | /// 118 | /// - Parameter body: The block to execute while holding the lock. 119 | /// - Returns: The value returned by the block. 120 | @inlinable 121 | func withLock(_ body: () throws -> T) rethrows -> T { 122 | self.lock() 123 | defer { 124 | self.unlock() 125 | } 126 | return try body() 127 | } 128 | 129 | // specialise Void return (for performance) 130 | @inlinable 131 | func withLockVoid(_ body: () throws -> Void) rethrows { 132 | try self.withLock(body) 133 | } 134 | } 135 | 136 | extension Lock: @unchecked Sendable {} 137 | 138 | /// A reader/writer threading lock based on `libpthread` instead of `libdispatch`. 139 | /// 140 | /// This object provides a lock on top of a single `pthread_rwlock_t`. This kind 141 | /// of lock is safe to use with `libpthread`-based threading models, such as the 142 | /// one used by NIO. On Windows, the lock is based on the substantially similar 143 | /// `SRWLOCK` type. 144 | internal final class ReadWriteLock { 145 | #if canImport(WASILibc) 146 | // WASILibc is single threaded, provides no locks 147 | #elseif os(Windows) 148 | fileprivate let rwlock: UnsafeMutablePointer = 149 | UnsafeMutablePointer.allocate(capacity: 1) 150 | fileprivate var shared: Bool = true 151 | #else 152 | fileprivate let rwlock: UnsafeMutablePointer = 153 | UnsafeMutablePointer.allocate(capacity: 1) 154 | #endif 155 | 156 | /// Create a new lock. 157 | public init() { 158 | #if canImport(WASILibc) 159 | // WASILibc is single threaded, provides no locks 160 | #elseif os(Windows) 161 | InitializeSRWLock(self.rwlock) 162 | #else 163 | let err = pthread_rwlock_init(self.rwlock, nil) 164 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 165 | #endif 166 | } 167 | 168 | deinit { 169 | #if canImport(WASILibc) 170 | // WASILibc is single threaded, provides no locks 171 | #elseif os(Windows) 172 | // SRWLOCK does not need to be free'd 173 | #else 174 | let err = pthread_rwlock_destroy(self.rwlock) 175 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 176 | #endif 177 | self.rwlock.deallocate() 178 | } 179 | 180 | /// Acquire a reader lock. 181 | /// 182 | /// Whenever possible, consider using `withReaderLock` instead of this 183 | /// method and `unlock`, to simplify lock handling. 184 | public func lockRead() { 185 | #if canImport(WASILibc) 186 | // WASILibc is single threaded, provides no locks 187 | #elseif os(Windows) 188 | AcquireSRWLockShared(self.rwlock) 189 | self.shared = true 190 | #else 191 | let err = pthread_rwlock_rdlock(self.rwlock) 192 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 193 | #endif 194 | } 195 | 196 | /// Acquire a writer lock. 197 | /// 198 | /// Whenever possible, consider using `withWriterLock` instead of this 199 | /// method and `unlock`, to simplify lock handling. 200 | public func lockWrite() { 201 | #if canImport(WASILibc) 202 | // WASILibc is single threaded, provides no locks 203 | #elseif os(Windows) 204 | AcquireSRWLockExclusive(self.rwlock) 205 | self.shared = false 206 | #else 207 | let err = pthread_rwlock_wrlock(self.rwlock) 208 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 209 | #endif 210 | } 211 | 212 | /// Release the lock. 213 | /// 214 | /// Whenever possible, consider using `withReaderLock` and `withWriterLock` 215 | /// instead of this method and `lockRead` and `lockWrite`, to simplify lock 216 | /// handling. 217 | public func unlock() { 218 | #if canImport(WASILibc) 219 | // WASILibc is single threaded, provides no locks 220 | #elseif os(Windows) 221 | if self.shared { 222 | ReleaseSRWLockShared(self.rwlock) 223 | } else { 224 | ReleaseSRWLockExclusive(self.rwlock) 225 | } 226 | #else 227 | let err = pthread_rwlock_unlock(self.rwlock) 228 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 229 | #endif 230 | } 231 | } 232 | 233 | extension ReadWriteLock { 234 | /// Acquire the reader lock for the duration of the given block. 235 | /// 236 | /// This convenience method should be preferred to `lockRead` and `unlock` 237 | /// in most situations, as it ensures that the lock will be released 238 | /// regardless of how `body` exits. 239 | /// 240 | /// - Parameter body: The block to execute while holding the reader lock. 241 | /// - Returns: The value returned by the block. 242 | @inlinable 243 | func withReaderLock(_ body: () throws -> T) rethrows -> T { 244 | self.lockRead() 245 | defer { 246 | self.unlock() 247 | } 248 | return try body() 249 | } 250 | 251 | /// Acquire the writer lock for the duration of the given block. 252 | /// 253 | /// This convenience method should be preferred to `lockWrite` and `unlock` 254 | /// in most situations, as it ensures that the lock will be released 255 | /// regardless of how `body` exits. 256 | /// 257 | /// - Parameter body: The block to execute while holding the writer lock. 258 | /// - Returns: The value returned by the block. 259 | @inlinable 260 | func withWriterLock(_ body: () throws -> T) rethrows -> T { 261 | self.lockWrite() 262 | defer { 263 | self.unlock() 264 | } 265 | return try body() 266 | } 267 | 268 | // specialise Void return (for performance) 269 | @inlinable 270 | func withReaderLockVoid(_ body: () throws -> Void) rethrows { 271 | try self.withReaderLock(body) 272 | } 273 | 274 | // specialise Void return (for performance) 275 | @inlinable 276 | func withWriterLockVoid(_ body: () throws -> Void) rethrows { 277 | try self.withWriterLock(body) 278 | } 279 | } 280 | 281 | extension ReadWriteLock: @unchecked Sendable {} 282 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Tests/MetricsTests/MetricsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import MetricsTestKit 17 | import Testing 18 | 19 | @testable import CoreMetrics 20 | @testable import Metrics 21 | 22 | struct MetricsExtensionsTests { 23 | @Test func timerBlock() throws { 24 | // create our test metrics, avoid bootstrapping global MetricsSystem 25 | let metrics = TestMetrics() 26 | // run the test 27 | let name = "timer-\(UUID().uuidString)" 28 | let delay = 0.05 29 | Timer.measure(label: name, factory: metrics) { 30 | Thread.sleep(forTimeInterval: delay) 31 | } 32 | let timer = try metrics.expectTimer(name) 33 | #expect(timer.values.count == 1, "expected number of entries to match") 34 | #expect(timer.values[0] > Int64(delay * 1_000_000_000), "expected delay to match") 35 | } 36 | 37 | @Test func timerWithTimeInterval() throws { 38 | // create our test metrics, avoid bootstrapping global MetricsSystem 39 | let metrics = TestMetrics() 40 | // run the test 41 | let timer = Timer(label: "test-timer", factory: metrics) 42 | let testTimer = try metrics.expectTimer(timer) 43 | let timeInterval = TimeInterval(Double.random(in: 1...500)) 44 | timer.record(timeInterval) 45 | #expect(testTimer.values.count == 1, "expected number of entries to match") 46 | #expect(testTimer.values[0] == Int64(timeInterval * 1_000_000_000), "expected value to match") 47 | } 48 | 49 | @Test func timerWithDispatchTime() throws { 50 | // create our test metrics, avoid bootstrapping global MetricsSystem 51 | let metrics = TestMetrics() 52 | // run the test 53 | let timer = Timer(label: "test-timer", factory: metrics) 54 | let testTimer = try metrics.expectTimer(timer) 55 | // nano 56 | let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1...500)) 57 | timer.record(nano) 58 | #expect(testTimer.values.count == 1, "expected number of entries to match") 59 | #expect(Int(testTimer.values[0]) == nano.nano(), "expected value to match") 60 | // micro 61 | let micro = DispatchTimeInterval.microseconds(Int.random(in: 1...500)) 62 | timer.record(micro) 63 | #expect(testTimer.values.count == 2, "expected number of entries to match") 64 | #expect(Int(testTimer.values[1]) == micro.nano(), "expected value to match") 65 | // milli 66 | let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1...500)) 67 | timer.record(milli) 68 | #expect(testTimer.values.count == 3, "expected number of entries to match") 69 | #expect(Int(testTimer.values[2]) == milli.nano(), "expected value to match") 70 | // seconds 71 | let sec = DispatchTimeInterval.seconds(Int.random(in: 1...500)) 72 | timer.record(sec) 73 | #expect(testTimer.values.count == 4, "expected number of entries to match") 74 | #expect(Int(testTimer.values[3]) == sec.nano(), "expected value to match") 75 | // never 76 | timer.record(DispatchTimeInterval.never) 77 | #expect(testTimer.values.count == 5, "expected number of entries to match") 78 | #expect(testTimer.values[4] == 0, "expected value to match") 79 | } 80 | 81 | @Test func timerWithDispatchTimeInterval() throws { 82 | let metrics = TestMetrics() 83 | 84 | let name = "timer-\(UUID().uuidString)" 85 | 86 | let timer = Timer(label: name, factory: metrics) 87 | let start = DispatchTime.now() 88 | let end = DispatchTime(uptimeNanoseconds: start.uptimeNanoseconds + 1000 * 1000 * 1000) 89 | timer.recordInterval(since: start, end: end) 90 | 91 | let testTimer = try metrics.expectTimer(timer) 92 | #expect(testTimer.values.count == 1, "expected number of entries to match") 93 | #expect( 94 | UInt64(testTimer.values.first!) == end.uptimeNanoseconds - start.uptimeNanoseconds, 95 | "expected value to match" 96 | ) 97 | #expect(metrics.timers.count == 1, "timer should have been stored") 98 | } 99 | 100 | @Test func timerDuration() throws { 101 | let metrics = TestMetrics() 102 | 103 | let name = "timer-\(UUID().uuidString)" 104 | let timer = Timer(label: name, factory: metrics) 105 | 106 | let duration = Duration(secondsComponent: 3, attosecondsComponent: 123_000_000_000_000_000) 107 | let nanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000 108 | timer.record(duration: duration) 109 | 110 | // Record a Duration that would overflow, 111 | // expect Int64.max to be recorded. 112 | timer.record(duration: Duration(secondsComponent: 10_000_000_000, attosecondsComponent: 123)) 113 | 114 | let testTimer = try metrics.expectTimer(timer) 115 | #expect(testTimer.values.count == 2, "expected number of entries to match") 116 | #expect(testTimer.values.first == nanoseconds, "expected value to match") 117 | #expect(testTimer.values[1] == Int64.max, "expected to record Int64.max if Durataion overflows") 118 | #expect(metrics.timers.count == 1, "timer should have been stored") 119 | } 120 | 121 | @Test func timerUnits() throws { 122 | let metrics = TestMetrics() 123 | 124 | let name = "timer-\(UUID().uuidString)" 125 | let value = Int64.random(in: 0...1000) 126 | 127 | let timer = Timer(label: name, factory: metrics) 128 | timer.recordNanoseconds(value) 129 | 130 | let testTimer = try metrics.expectTimer(timer) 131 | #expect(testTimer.values.count == 1, "expected number of entries to match") 132 | #expect(testTimer.values.first == value, "expected value to match") 133 | #expect(metrics.timers.count == 1, "timer should have been stored") 134 | 135 | let secondsName = "timer-seconds-\(UUID().uuidString)" 136 | let secondsValue = Int64.random(in: 0...1000) 137 | let secondsTimer = Timer(label: secondsName, preferredDisplayUnit: .seconds, factory: metrics) 138 | secondsTimer.recordSeconds(secondsValue) 139 | 140 | let testSecondsTimer = try metrics.expectTimer(secondsTimer) 141 | #expect(testSecondsTimer.values.count == 1, "expected number of entries to match") 142 | #expect(metrics.timers.count == 2, "timer should have been stored") 143 | } 144 | 145 | @Test func preferDisplayUnit() throws { 146 | let metrics = TestMetrics() 147 | 148 | let value = Double.random(in: 0...1000) 149 | let timer = Timer(label: "test", preferredDisplayUnit: .seconds, factory: metrics) 150 | timer.recordSeconds(value) 151 | 152 | let testTimer = try metrics.expectTimer(timer) 153 | 154 | // The suggested way for comparing float numbers is to use swift-numerics, 155 | // but this would add a dependency. Instead, as we fully control the values 156 | // used in tests, we can get away with the simple `abs(x - y) < accuracy`. 157 | // See https://developer.apple.com/documentation/testing/migratingfromxctest 158 | 159 | testTimer.preferDisplayUnit(.nanoseconds) 160 | #expect( 161 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value * 1000 * 1000 * 1000) < 1.0, 162 | "expected value to match" 163 | ) 164 | 165 | testTimer.preferDisplayUnit(.microseconds) 166 | #expect( 167 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value * 1000 * 1000) < 0.1, 168 | "expected value to match" 169 | ) 170 | 171 | testTimer.preferDisplayUnit(.milliseconds) 172 | #expect( 173 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value * 1000) < 0.1, 174 | "expected value to match" 175 | ) 176 | 177 | testTimer.preferDisplayUnit(.seconds) 178 | #expect( 179 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value) < 0.000000001, 180 | "expected value to match" 181 | ) 182 | 183 | testTimer.preferDisplayUnit(.minutes) 184 | #expect( 185 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value / 60) < 0.000000001, 186 | "expected value to match" 187 | ) 188 | 189 | testTimer.preferDisplayUnit(.hours) 190 | #expect( 191 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value / (60 * 60)) < 0.000000001, 192 | "expected value to match" 193 | ) 194 | 195 | testTimer.preferDisplayUnit(.days) 196 | #expect( 197 | abs(testTimer.valueInPreferredUnit(atIndex: 0) - value / (60 * 60 * 24)) < 0.000000001, 198 | "expected value to match" 199 | ) 200 | } 201 | 202 | @Test func timerMeasure() async throws { 203 | // create our test metrics, avoid bootstrapping global MetricsSystem 204 | let metrics = TestMetrics() 205 | // run the test 206 | let name = "timer-\(UUID().uuidString)" 207 | let delay = Duration.milliseconds(5) 208 | let timer = Timer(label: name, factory: metrics) 209 | try await timer.measure { 210 | try await Task.sleep(for: delay) 211 | } 212 | 213 | let expectedTimer = try metrics.expectTimer(name) 214 | #expect(expectedTimer.values.count == 1, "expected number of entries to match") 215 | #expect(expectedTimer.values[0] > delay.nanosecondsClamped, "expected delay to match") 216 | } 217 | 218 | @Test func timerMeasureSync() async throws { 219 | // create our test metrics, avoid bootstrapping global MetricsSystem 220 | let metrics = TestMetrics() 221 | // run the test 222 | let name = "timer-\(UUID().uuidString)" 223 | let delay = 0.5 224 | let timer = Timer(label: name, factory: metrics) 225 | timer.measure { 226 | Thread.sleep(forTimeInterval: delay) 227 | } 228 | 229 | let expectedTimer = try metrics.expectTimer(name) 230 | #expect(expectedTimer.values.count == 1, "expected number of entries to match") 231 | #expect(expectedTimer.values[0] > Int64(delay * 1_000_000_000), "expected delay to match in nanoseconds") 232 | } 233 | 234 | @MainActor 235 | @Test func timerMeasureFromMainActor() async throws { 236 | // create our test metrics, avoid bootstrapping global MetricsSystem 237 | let metrics = TestMetrics() 238 | // run the test 239 | let name = "timer-\(UUID().uuidString)" 240 | let delay = Duration.milliseconds(5) 241 | let timer = Timer(label: name, factory: metrics) 242 | try await timer.measure { 243 | try await Task.sleep(for: delay) 244 | } 245 | 246 | let expectedTimer = try metrics.expectTimer(name) 247 | #expect(expectedTimer.values.count == 1, "expected number of entries to match") 248 | #expect(expectedTimer.values[0] > delay.nanosecondsClamped, "expected delay to match") 249 | } 250 | } 251 | 252 | // https://bugs.swift.org/browse/SR-6310 253 | extension DispatchTimeInterval { 254 | func nano() -> Int { 255 | // This wrapping in a optional is a workaround because DispatchTimeInterval 256 | // is a non-frozen public enum and Dispatch is built with library evolution 257 | // mode turned on. 258 | // This means we should have an `@unknown default` case, but this breaks 259 | // on non-Darwin platforms. 260 | // Switching over an optional means that the `.none` case will map to 261 | // `default` (which means we'll always have a valid case to go into 262 | // the default case), but in reality this case will never exist as this 263 | // optional will never be nil. 264 | let interval = Optional(self) 265 | switch interval { 266 | case .nanoseconds(let value): 267 | return value 268 | case .microseconds(let value): 269 | return value * 1000 270 | case .milliseconds(let value): 271 | return value * 1_000_000 272 | case .seconds(let value): 273 | return value * 1_000_000_000 274 | case .never: 275 | return 0 276 | default: 277 | return 0 278 | } 279 | } 280 | } 281 | 282 | @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) 283 | extension Swift.Duration { 284 | fileprivate var nanosecondsClamped: Int64 { 285 | let components = self.components 286 | 287 | let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000) 288 | let attosCompononentNanos = components.attoseconds / 1_000_000_000 289 | let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos) 290 | 291 | guard 292 | !secondsComponentNanos.overflow, 293 | !combinedNanos.overflow 294 | else { 295 | return .max 296 | } 297 | 298 | return combinedNanos.partialValue 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftMetrics 2 | 3 | A Metrics API package for Swift. 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | ## Getting started 10 | 11 | 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. 12 | 13 | ### Adding the dependency 14 | 15 | To add a dependency on the metrics API package, you need to declare it in your `Package.swift`: 16 | 17 | ```swift 18 | // swift-metrics 1.x and 2.x are almost API compatible, so most clients should use 19 | .package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"), 20 | ``` 21 | 22 | and to your application/library target, add "Metrics" to your dependencies: 23 | 24 | ```swift 25 | .target( 26 | name: "BestExampleApp", 27 | dependencies: [ 28 | // ... 29 | .product(name: "Metrics", package: "swift-metrics"), 30 | ] 31 | ), 32 | ``` 33 | 34 | ### Emitting metrics information 35 | 36 | ```swift 37 | // 1) let's import the metrics API package 38 | import Metrics 39 | 40 | // 2) we need to create a concrete metric object, the label works similarly to a `DispatchQueue` label 41 | let counter = Counter(label: "com.example.BestExampleApp.numberOfRequests") 42 | 43 | // 3) we're now ready to use it 44 | counter.increment() 45 | ``` 46 | 47 | ### Selecting a metrics backend implementation (applications only) 48 | 49 | 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. 50 | 51 | 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. 52 | 53 | 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: 54 | 55 | ```swift 56 | MetricsSystem.bootstrap(SelectedMetricsImplementation()) 57 | ``` 58 | 59 | This instructs the `MetricsSystem` to install `SelectedMetricsImplementation` (actual name will differ) as the metrics backend to use. 60 | 61 | As the API has just launched, not many implementations exist yet. If you are interested in implementing one see the "Implementing a metrics backend" section below explaining how to do so. List of existing SwiftMetrics API compatible libraries: 62 | 63 | - [SwiftPrometheus](https://github.com/MrLotU/SwiftPrometheus), support for [Prometheus](https://prometheus.io) 64 | - [StatsD Client](https://github.com/apple/swift-statsd-client), support for StatsD 65 | - [OpenTelemetry Swift](https://github.com/open-telemetry/opentelemetry-swift), support for [OpenTelemetry](https://opentelemetry.io/) which also implements other metrics and tracing backends 66 | - Your library? [Get in touch!](https://forums.swift.org/c/server) 67 | 68 | ### Swift Metrics Extras 69 | 70 | 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. 71 | 72 | ## Detailed design 73 | 74 | ### Architecture 75 | 76 | 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. 77 | 78 | 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: 79 | 80 | 1. a metrics API 81 | 2. a metrics backend implementation 82 | 83 | 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. 84 | 85 | 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. 86 | 87 | [pitch](https://forums.swift.org/t/metrics/19353) | 88 | [discussion](https://forums.swift.org/t/discussion-server-metrics-api/) | 89 | [feedback](https://forums.swift.org/t/feedback-server-metrics-api/) 90 | 91 | ### Metric types 92 | 93 | The API supports six metric types: 94 | 95 | `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. 96 | 97 | ```swift 98 | counter.increment(by: 100) 99 | ``` 100 | 101 | - `FloatingPointCounter`: A variation of a `Counter` that records a floating point value, instead of an integer. 102 | 103 | ```swift 104 | floatingPointCounter.increment(by: 10.5) 105 | ``` 106 | 107 | `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. 108 | 109 | ```swift 110 | gauge.record(100) 111 | ``` 112 | 113 | `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 increments and decrements. 114 | 115 | ```swift 116 | meter.record(100) 117 | ``` 118 | 119 | `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. 120 | 121 | ```swift 122 | recorder.record(100) 123 | ``` 124 | 125 | `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. 126 | 127 | ```swift 128 | timer.recordMilliseconds(100) 129 | ``` 130 | 131 | ### Implementing a metrics backend (e.g. Prometheus client library) 132 | 133 | Note: Unless you need to implement a custom metrics backend, everything in this section is likely not relevant, so please feel free to skip. 134 | 135 | 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: 136 | 137 | ```swift 138 | let metricsImplementation = MyFavoriteMetricsImplementation() 139 | MetricsSystem.bootstrap(metricsImplementation) 140 | ``` 141 | 142 | This instructs the `MetricsSystem` to install `MyFavoriteMetricsImplementation` as the metrics backend (`MetricsFactory`) to use. This should only be done once at the beginning of the program. 143 | 144 | Given the above, an implementation of a metric backend needs to conform to `protocol MetricsFactory`: 145 | 146 | ```swift 147 | public protocol MetricsFactory { 148 | func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler 149 | func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler 150 | func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler 151 | func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler 152 | 153 | func destroyCounter(_ handler: CounterHandler) 154 | func destroyMeter(_ handler: MeterHandler) 155 | func destroyRecorder(_ handler: RecorderHandler) 156 | func destroyTimer(_ handler: TimerHandler) 157 | } 158 | ``` 159 | 160 | The `MetricsFactory` is responsible for instantiating the concrete metrics classes that capture the metrics and perform aggregation and calculation of various quantiles as needed. 161 | 162 | **Counter** 163 | 164 | ```swift 165 | public protocol CounterHandler: AnyObject { 166 | func increment(by: Int64) 167 | func reset() 168 | } 169 | ``` 170 | 171 | **Meter** 172 | 173 | ```swift 174 | public protocol MeterHandler: AnyObject { 175 | func set(_ value: Int64) 176 | func set(_ value: Double) 177 | func increment(by: Double) 178 | func decrement(by: Double) 179 | } 180 | ``` 181 | 182 | **Recorder** 183 | 184 | ```swift 185 | public protocol RecorderHandler: AnyObject { 186 | func record(_ value: Int64) 187 | func record(_ value: Double) 188 | } 189 | ``` 190 | 191 | **Timer** 192 | 193 | ```swift 194 | public protocol TimerHandler: AnyObject { 195 | func recordNanoseconds(_ duration: Int64) 196 | } 197 | ``` 198 | 199 | #### Dealing with Overflows 200 | 201 | 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: 202 | 203 | ```swift 204 | class ExampleCounter: CounterHandler { 205 | var value: Int64 = 0 206 | func increment(by amount: Int64) { 207 | let result = self.value.addingReportingOverflow(amount) 208 | if result.overflow { 209 | self.value = Int64.max 210 | } else { 211 | self.value = result.partialValue 212 | } 213 | } 214 | } 215 | ``` 216 | 217 | #### Full example 218 | 219 | Here is a full, but contrived, example of an in-memory implementation: 220 | 221 | ```swift 222 | class SimpleMetricsLibrary: MetricsFactory { 223 | init() {} 224 | 225 | func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { 226 | return ExampleCounter(label, dimensions) 227 | } 228 | 229 | func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler { 230 | return ExampleMeter(label, dimensions) 231 | } 232 | 233 | func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { 234 | return ExampleRecorder(label, dimensions, aggregate) 235 | } 236 | 237 | func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { 238 | return ExampleTimer(label, dimensions) 239 | } 240 | 241 | // implementation is stateless, so nothing to do on destroy calls 242 | func destroyCounter(_ handler: CounterHandler) {} 243 | func destroyMeter(_ handler: TimerHandler) {} 244 | func destroyRecorder(_ handler: RecorderHandler) {} 245 | func destroyTimer(_ handler: TimerHandler) {} 246 | 247 | private class ExampleCounter: CounterHandler { 248 | init(_: String, _: [(String, String)]) {} 249 | 250 | let lock = NSLock() 251 | var value: Int64 = 0 252 | func increment(by amount: Int64) { 253 | self.lock.withLock { 254 | self.value += amount 255 | } 256 | } 257 | 258 | func reset() { 259 | self.lock.withLock { 260 | self.value = 0 261 | } 262 | } 263 | } 264 | 265 | private class ExampleMeter: MeterHandler { 266 | init(_: String, _: [(String, String)]) {} 267 | 268 | let lock = NSLock() 269 | var _value: Double = 0 270 | 271 | func set(_ value: Int64) { 272 | self.set(Double(value)) 273 | } 274 | 275 | func set(_ value: Double) { 276 | self.lock.withLock { _value = value } 277 | } 278 | 279 | func increment(by value: Double) { 280 | self.lock.withLock { self._value += value } 281 | } 282 | 283 | func decrement(by value: Double) { 284 | self.lock.withLock { self._value -= value } 285 | } 286 | } 287 | 288 | private class ExampleRecorder: RecorderHandler { 289 | init(_: String, _: [(String, String)], _: Bool) {} 290 | 291 | private let lock = NSLock() 292 | var values = [(Int64, Double)]() 293 | func record(_ value: Int64) { 294 | self.record(Double(value)) 295 | } 296 | 297 | func record(_ value: Double) { 298 | // TODO: sliding window 299 | lock.withLock { 300 | values.append((Date().nanoSince1970, value)) 301 | self._count += 1 302 | self._sum += value 303 | self._min = Swift.min(self._min, value) 304 | self._max = Swift.max(self._max, value) 305 | } 306 | } 307 | 308 | var _sum: Double = 0 309 | var sum: Double { 310 | return self.lock.withLock { _sum } 311 | } 312 | 313 | private var _count: Int = 0 314 | var count: Int { 315 | return self.lock.withLock { _count } 316 | } 317 | 318 | private var _min: Double = 0 319 | var min: Double { 320 | return self.lock.withLock { _min } 321 | } 322 | 323 | private var _max: Double = 0 324 | var max: Double { 325 | return self.lock.withLock { _max } 326 | } 327 | } 328 | 329 | private class ExampleTimer: TimerHandler { 330 | init(_: String, _: [(String, String)]) {} 331 | 332 | let lock = NSLock() 333 | var _value: Int64 = 0 334 | 335 | func recordNanoseconds(_ duration: Int64) { 336 | self.lock.withLock { _value = duration } 337 | } 338 | } 339 | } 340 | ``` 341 | 342 | ## Security 343 | 344 | Please see [SECURITY.md](SECURITY.md) for details on the security process. 345 | 346 | ## Getting involved 347 | 348 | Do not hesitate to get in touch as well, over on https://forums.swift.org/c/server 349 | -------------------------------------------------------------------------------- /Sources/MetricsTestKit/TestMetrics.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | //===----------------------------------------------------------------------===// 15 | // 16 | // This source file is part of the Swift Cluster Membership open source project 17 | // 18 | // Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors 19 | // Licensed under Apache License v2.0 20 | // 21 | // See LICENSE.txt for license information 22 | // See CONTRIBUTORS.md for the list of Swift Cluster Membership project authors 23 | // 24 | // SPDX-License-Identifier: Apache-2.0 25 | // 26 | //===----------------------------------------------------------------------===// 27 | 28 | import CoreMetrics 29 | import Foundation 30 | import Metrics 31 | import Testing 32 | 33 | /// A metrics factory which allows you to inspect recorded metrics programmatically. 34 | /// 35 | /// This instance is only intended for tests of the Metrics API itself. 36 | /// 37 | /// Taken directly from `swift-cluster-memberships`'s own test target package, which 38 | /// adopts the `TestMetrics` from `swift-metrics`. 39 | /// 40 | /// Created Handlers will store Metrics until they are explicitly destroyed. 41 | public final class TestMetrics: MetricsFactory { 42 | private let lock = NSLock() 43 | 44 | public typealias Label = String 45 | public typealias Dimensions = String 46 | 47 | public struct FullKey: Sendable { 48 | let label: Label 49 | let dimensions: [(String, String)] 50 | } 51 | 52 | private var _counters = [FullKey: TestCounter]() 53 | private var _meters = [FullKey: TestMeter]() 54 | private var _recorders = [FullKey: TestRecorder]() 55 | private var _timers = [FullKey: TestTimer]() 56 | 57 | public init() { 58 | // nothing to do 59 | } 60 | 61 | /// Reset method to destroy all created ``TestCounter``, ``TestMeter``, ``TestRecorder`` and ``TestTimer``. 62 | /// Invoke this method in between test runs to verify that Counters are created as needed. 63 | public func reset() { 64 | self.lock.withLock { 65 | self._counters = [:] 66 | self._recorders = [:] 67 | self._meters = [:] 68 | self._timers = [:] 69 | } 70 | } 71 | 72 | public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { 73 | self.lock.withLock { () -> CounterHandler in 74 | if let existing = self._counters[.init(label: label, dimensions: dimensions)] { 75 | return existing 76 | } 77 | let item = TestCounter(label: label, dimensions: dimensions) 78 | self._counters[.init(label: label, dimensions: dimensions)] = item 79 | return item 80 | } 81 | } 82 | 83 | public func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler { 84 | self.lock.withLock { () -> MeterHandler in 85 | if let existing = self._meters[.init(label: label, dimensions: dimensions)] { 86 | return existing 87 | } 88 | let item = TestMeter(label: label, dimensions: dimensions) 89 | self._meters[.init(label: label, dimensions: dimensions)] = item 90 | return item 91 | } 92 | } 93 | 94 | public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { 95 | self.lock.withLock { () -> RecorderHandler in 96 | if let existing = self._recorders[.init(label: label, dimensions: dimensions)] { 97 | return existing 98 | } 99 | let item = TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate) 100 | self._recorders[.init(label: label, dimensions: dimensions)] = item 101 | return item 102 | } 103 | } 104 | 105 | public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { 106 | self.lock.withLock { () -> TimerHandler in 107 | if let existing = self._timers[.init(label: label, dimensions: dimensions)] { 108 | return existing 109 | } 110 | let item = TestTimer(label: label, dimensions: dimensions) 111 | self._timers[.init(label: label, dimensions: dimensions)] = item 112 | return item 113 | } 114 | } 115 | 116 | public func destroyCounter(_ handler: CounterHandler) { 117 | if let testCounter = handler as? TestCounter { 118 | self.lock.withLock { 119 | self._counters.removeValue(forKey: testCounter.key) 120 | } 121 | } 122 | } 123 | 124 | public func destroyMeter(_ handler: MeterHandler) { 125 | if let testMeter = handler as? TestMeter { 126 | self.lock.withLock { () in 127 | self._meters.removeValue(forKey: testMeter.key) 128 | } 129 | } 130 | } 131 | 132 | public func destroyRecorder(_ handler: RecorderHandler) { 133 | if let testRecorder = handler as? TestRecorder { 134 | self.lock.withLock { 135 | self._recorders.removeValue(forKey: testRecorder.key) 136 | } 137 | } 138 | } 139 | 140 | public func destroyTimer(_ handler: TimerHandler) { 141 | if let testTimer = handler as? TestTimer { 142 | self.lock.withLock { 143 | self._timers.removeValue(forKey: testTimer.key) 144 | } 145 | } 146 | } 147 | } 148 | 149 | extension TestMetrics.FullKey: Hashable { 150 | public func hash(into hasher: inout Hasher) { 151 | hasher.combine(self.label) 152 | hasher.combine(Dictionary(uniqueKeysWithValues: self.dimensions)) 153 | } 154 | 155 | public static func == (lhs: TestMetrics.FullKey, rhs: TestMetrics.FullKey) -> Bool { 156 | lhs.label == rhs.label 157 | && Dictionary(uniqueKeysWithValues: lhs.dimensions) == Dictionary(uniqueKeysWithValues: rhs.dimensions) 158 | } 159 | } 160 | 161 | // MARK: - Assertions 162 | 163 | extension TestMetrics { 164 | // MARK: - Counter 165 | 166 | public func expectCounter(_ metric: Counter) throws -> TestCounter { 167 | guard let counter = metric._handler as? TestCounter else { 168 | throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestCounter.self)") 169 | } 170 | return counter 171 | } 172 | 173 | public func expectCounter(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestCounter { 174 | let maybeItem = self.lock.withLock { 175 | self._counters[.init(label: label, dimensions: dimensions)] 176 | } 177 | guard let testCounter = maybeItem else { 178 | throw TestMetricsError.missingMetric(label: label, dimensions: dimensions) 179 | } 180 | return testCounter 181 | } 182 | 183 | /// All the counters which have been created and not destroyed 184 | public var counters: [TestCounter] { 185 | let counters = self.lock.withLock { 186 | self._counters 187 | } 188 | return Array(counters.values) 189 | } 190 | 191 | // MARK: - Gauge 192 | 193 | public func expectGauge(_ metric: Gauge) throws -> TestRecorder { 194 | try self.expectRecorder(metric) 195 | } 196 | 197 | public func expectGauge(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestRecorder { 198 | try self.expectRecorder(label, dimensions) 199 | } 200 | 201 | // MARK: - Meter 202 | 203 | public func expectMeter(_ metric: Meter) throws -> TestMeter { 204 | guard let meter = metric._handler as? TestMeter else { 205 | throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestMeter.self)") 206 | } 207 | return meter 208 | } 209 | 210 | public func expectMeter(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestMeter { 211 | let maybeItem = self.lock.withLock { 212 | self._meters[.init(label: label, dimensions: dimensions)] 213 | } 214 | guard let testMeter = maybeItem else { 215 | throw TestMetricsError.missingMetric(label: label, dimensions: dimensions) 216 | } 217 | return testMeter 218 | } 219 | 220 | /// All the meters which have been created and not destroyed 221 | public var meters: [TestMeter] { 222 | let meters = self.lock.withLock { 223 | self._meters 224 | } 225 | return Array(meters.values) 226 | } 227 | 228 | // MARK: - Recorder 229 | 230 | public func expectRecorder(_ metric: Recorder) throws -> TestRecorder { 231 | guard let recorder = metric._handler as? TestRecorder else { 232 | throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestRecorder.self)") 233 | } 234 | return recorder 235 | } 236 | 237 | public func expectRecorder(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestRecorder { 238 | let maybeItem = self.lock.withLock { 239 | self._recorders[.init(label: label, dimensions: dimensions)] 240 | } 241 | guard let testRecorder = maybeItem else { 242 | throw TestMetricsError.missingMetric(label: label, dimensions: dimensions) 243 | } 244 | return testRecorder 245 | } 246 | 247 | /// All the recorders which have been created and not destroyed 248 | public var recorders: [TestRecorder] { 249 | let recorders = self.lock.withLock { 250 | self._recorders 251 | } 252 | return Array(recorders.values) 253 | } 254 | 255 | // MARK: - Timer 256 | 257 | public func expectTimer(_ metric: CoreMetrics.Timer) throws -> TestTimer { 258 | guard let timer = metric._handler as? TestTimer else { 259 | throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestTimer.self)") 260 | } 261 | return timer 262 | } 263 | 264 | public func expectTimer(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestTimer { 265 | let maybeItem = self.lock.withLock { 266 | self._timers[.init(label: label, dimensions: dimensions)] 267 | } 268 | guard let testTimer = maybeItem else { 269 | throw TestMetricsError.missingMetric(label: label, dimensions: dimensions) 270 | } 271 | return testTimer 272 | } 273 | 274 | /// All the timers which have been created and not destroyed 275 | public var timers: [TestTimer] { 276 | let timers = self.lock.withLock { 277 | self._timers 278 | } 279 | return Array(timers.values) 280 | } 281 | } 282 | 283 | // MARK: - Metric type implementations 284 | 285 | /// A type that represents a test metric key and value. 286 | public protocol TestMetric { 287 | associatedtype Value 288 | 289 | var key: TestMetrics.FullKey { get } 290 | 291 | var lastValue: Value? { get } 292 | var last: (Date, Value)? { get } 293 | } 294 | 295 | /// A test counter that you can inspect. 296 | public final class TestCounter: TestMetric, CounterHandler, Equatable { 297 | public let id: String 298 | public let label: String 299 | public let dimensions: [(String, String)] 300 | 301 | public var key: TestMetrics.FullKey { 302 | TestMetrics.FullKey(label: self.label, dimensions: self.dimensions) 303 | } 304 | 305 | let lock = NSLock() 306 | private var _values = [(Date, Int64)]() 307 | 308 | init(label: String, dimensions: [(String, String)]) { 309 | self.id = UUID().uuidString 310 | self.label = label 311 | self.dimensions = dimensions 312 | } 313 | 314 | public func increment(by amount: Int64) { 315 | self.lock.withLock { 316 | self._values.append((Date(), amount)) 317 | } 318 | } 319 | 320 | public func reset() { 321 | self.lock.withLock { 322 | self._values = [] 323 | } 324 | } 325 | 326 | public var lastValue: Int64? { 327 | self.last?.1 328 | } 329 | 330 | public var totalValue: Int64 { 331 | self.values.reduce(0, +) 332 | } 333 | 334 | public var last: (Date, Int64)? { 335 | self.lock.withLock { 336 | self._values.last 337 | } 338 | } 339 | 340 | public var values: [Int64] { 341 | self.lock.withLock { 342 | self._values.map { $0.1 } 343 | } 344 | } 345 | 346 | public static func == (lhs: TestCounter, rhs: TestCounter) -> Bool { 347 | lhs.id == rhs.id 348 | } 349 | } 350 | 351 | /// A test meter that you can inspect. 352 | public final class TestMeter: TestMetric, MeterHandler, Equatable { 353 | public let id: String 354 | public let label: String 355 | public let dimensions: [(String, String)] 356 | 357 | public var key: TestMetrics.FullKey { 358 | TestMetrics.FullKey(label: self.label, dimensions: self.dimensions) 359 | } 360 | 361 | let lock = NSLock() 362 | private var _values = [(Date, Double)]() 363 | 364 | init(label: String, dimensions: [(String, String)]) { 365 | self.id = UUID().uuidString 366 | self.label = label 367 | self.dimensions = dimensions 368 | } 369 | 370 | public func set(_ value: Int64) { 371 | self.set(Double(value)) 372 | } 373 | 374 | public func set(_ value: Double) { 375 | self.lock.withLock { 376 | // this may lose precision but good enough as an example 377 | _values.append((Date(), Double(value))) 378 | } 379 | } 380 | 381 | public func increment(by amount: Double) { 382 | // Drop illegal values 383 | // - cannot increment by NaN 384 | guard !amount.isNaN else { 385 | return 386 | } 387 | // - cannot increment by infinite quantities 388 | guard !amount.isInfinite else { 389 | return 390 | } 391 | // - cannot increment by negative values 392 | guard amount.sign == .plus else { 393 | return 394 | } 395 | // - cannot increment by zero 396 | guard !amount.isZero else { 397 | return 398 | } 399 | 400 | self.lock.withLock { 401 | let lastValue: Double = self._values.last?.1 ?? 0 402 | let newValue = lastValue + amount 403 | _values.append((Date(), newValue)) 404 | } 405 | } 406 | 407 | public func decrement(by amount: Double) { 408 | // Drop illegal values 409 | // - cannot decrement by NaN 410 | guard !amount.isNaN else { 411 | return 412 | } 413 | // - cannot decrement by infinite quantities 414 | guard !amount.isInfinite else { 415 | return 416 | } 417 | // - cannot decrement by negative values 418 | guard amount.sign == .plus else { 419 | return 420 | } 421 | // - cannot decrement by zero 422 | guard !amount.isZero else { 423 | return 424 | } 425 | 426 | self.lock.withLock { 427 | let lastValue: Double = self._values.last?.1 ?? 0 428 | let newValue = lastValue - amount 429 | _values.append((Date(), newValue)) 430 | } 431 | } 432 | 433 | public var lastValue: Double? { 434 | self.last?.1 435 | } 436 | 437 | public var last: (Date, Double)? { 438 | self.lock.withLock { 439 | self._values.last 440 | } 441 | } 442 | 443 | public var values: [Double] { 444 | self.lock.withLock { 445 | self._values.map { $0.1 } 446 | } 447 | } 448 | 449 | public static func == (lhs: TestMeter, rhs: TestMeter) -> Bool { 450 | lhs.id == rhs.id 451 | } 452 | } 453 | 454 | /// A test recorder that you can inspect. 455 | public final class TestRecorder: TestMetric, RecorderHandler, Equatable { 456 | public let id: String 457 | public let label: String 458 | public let dimensions: [(String, String)] 459 | public let aggregate: Bool 460 | 461 | public var key: TestMetrics.FullKey { 462 | TestMetrics.FullKey(label: self.label, dimensions: self.dimensions) 463 | } 464 | 465 | let lock = NSLock() 466 | private var _values = [(Date, Double)]() 467 | 468 | init(label: String, dimensions: [(String, String)], aggregate: Bool) { 469 | self.id = UUID().uuidString 470 | self.label = label 471 | self.dimensions = dimensions 472 | self.aggregate = aggregate 473 | } 474 | 475 | public func record(_ value: Int64) { 476 | self.record(Double(value)) 477 | } 478 | 479 | public func record(_ value: Double) { 480 | self.lock.withLock { 481 | // this may lose precision but good enough as an example 482 | _values.append((Date(), Double(value))) 483 | } 484 | } 485 | 486 | public var lastValue: Double? { 487 | self.last?.1 488 | } 489 | 490 | public var last: (Date, Double)? { 491 | self.lock.withLock { 492 | self._values.last 493 | } 494 | } 495 | 496 | public var values: [Double] { 497 | self.lock.withLock { 498 | self._values.map { $0.1 } 499 | } 500 | } 501 | 502 | public static func == (lhs: TestRecorder, rhs: TestRecorder) -> Bool { 503 | lhs.id == rhs.id 504 | } 505 | } 506 | 507 | /// A test timer that you can inspect. 508 | public final class TestTimer: TestMetric, TimerHandler, Equatable { 509 | public let id: String 510 | public let label: String 511 | public var displayUnit: TimeUnit? 512 | public let dimensions: [(String, String)] 513 | 514 | public var key: TestMetrics.FullKey { 515 | TestMetrics.FullKey(label: self.label, dimensions: self.dimensions) 516 | } 517 | 518 | let lock = NSLock() 519 | private var _values = [(Date, Int64)]() 520 | 521 | init(label: String, dimensions: [(String, String)]) { 522 | self.id = UUID().uuidString 523 | self.label = label 524 | self.displayUnit = nil 525 | self.dimensions = dimensions 526 | } 527 | 528 | public func preferDisplayUnit(_ unit: TimeUnit) { 529 | self.lock.withLock { 530 | self.displayUnit = unit 531 | } 532 | } 533 | 534 | public func valueInPreferredUnit(atIndex i: Int) -> Double { 535 | let value = self.values[i] 536 | guard let displayUnit = self.displayUnit else { 537 | return Double(value) 538 | } 539 | return Double(value) / Double(displayUnit.scaleFromNanoseconds) 540 | } 541 | 542 | public func recordNanoseconds(_ duration: Int64) { 543 | self.lock.withLock { 544 | _values.append((Date(), duration)) 545 | } 546 | } 547 | 548 | public var lastValue: Int64? { 549 | self.last?.1 550 | } 551 | 552 | public var values: [Int64] { 553 | self.lock.withLock { 554 | self._values.map { $0.1 } 555 | } 556 | } 557 | 558 | public var last: (Date, Int64)? { 559 | self.lock.withLock { 560 | self._values.last 561 | } 562 | } 563 | 564 | public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool { 565 | lhs.id == rhs.id 566 | } 567 | } 568 | 569 | extension NSLock { 570 | @discardableResult 571 | fileprivate func withLock(_ body: () -> T) -> T { 572 | self.lock() 573 | defer { 574 | self.unlock() 575 | } 576 | return body() 577 | } 578 | } 579 | 580 | // MARK: - Errors 581 | 582 | /// Metric testing errors 583 | public enum TestMetricsError: Error { 584 | case missingMetric(label: String, dimensions: [(String, String)]) 585 | case illegalMetricType(metric: Sendable, expected: String) 586 | } 587 | 588 | // MARK: - Sendable support 589 | 590 | // ideally we would not be using @unchecked here, but concurrency-safety checks do not recognize locks 591 | extension TestMetrics: @unchecked Sendable {} 592 | extension TestCounter: @unchecked Sendable {} 593 | extension TestMeter: @unchecked Sendable {} 594 | extension TestRecorder: @unchecked Sendable {} 595 | extension TestTimer: @unchecked Sendable {} 596 | -------------------------------------------------------------------------------- /Tests/MetricsTests/CoreMetricsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Metrics API open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Swift Metrics API project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import MetricsTestKit 17 | import Testing 18 | 19 | @testable import CoreMetrics 20 | 21 | struct MetricsTests { 22 | @Test func counters() throws { 23 | // create our test metrics, avoid bootstrapping global MetricsSystem 24 | let metrics = TestMetrics() 25 | let group = DispatchGroup() 26 | let name = "counter-\(UUID().uuidString)" 27 | let counter = Counter(label: name, factory: metrics) 28 | let testCounter = try metrics.expectCounter(counter) 29 | let total = Int.random(in: 500...1000) 30 | for _ in 0..(by: DataType) where DataType: BinaryInteger {} 586 | func reset() {} 587 | } 588 | 589 | let counter1 = Counter(label: "foo") 590 | #expect(!(counter1._handler is CustomHandler), "expected non-custom log handler") 591 | let counter2 = Counter(label: "foo", dimensions: [], handler: CustomHandler()) 592 | #expect(counter2._handler is CustomHandler, "expected custom log handler") 593 | } 594 | 595 | @Test func customFactory() { 596 | // @unchecked Sendable is okay here - locking is done manually. 597 | final class CustomFactory: MetricsFactory, @unchecked Sendable { 598 | 599 | init(handler: CustomHandler) { 600 | self.handler = handler 601 | } 602 | 603 | final class CustomHandler: CounterHandler { 604 | func increment(by: DataType) where DataType: BinaryInteger {} 605 | func reset() {} 606 | } 607 | private let handler: CustomHandler 608 | private let lock: NSLock = NSLock() 609 | private var locked_didCallDestroyCounter: Bool = false 610 | var didCallDestroyCounter: Bool { 611 | self.lock.lock() 612 | defer { 613 | lock.unlock() 614 | } 615 | return self.locked_didCallDestroyCounter 616 | } 617 | 618 | func makeCounter(label: String, dimensions: [(String, String)]) -> any CoreMetrics.CounterHandler { 619 | handler 620 | } 621 | 622 | func makeRecorder( 623 | label: String, 624 | dimensions: [(String, String)], 625 | aggregate: Bool 626 | ) -> any CoreMetrics.RecorderHandler { 627 | fatalError("Unsupported") 628 | } 629 | 630 | func makeTimer(label: String, dimensions: [(String, String)]) -> any CoreMetrics.TimerHandler { 631 | fatalError("Unsupported") 632 | } 633 | 634 | func destroyCounter(_ handler: any CoreMetrics.CounterHandler) { 635 | #expect( 636 | handler === self.handler, 637 | "The handler to be destroyed doesn't match the expected handler." 638 | ) 639 | self.lock.lock() 640 | defer { 641 | lock.unlock() 642 | } 643 | self.locked_didCallDestroyCounter = true 644 | } 645 | 646 | func destroyRecorder(_ handler: any CoreMetrics.RecorderHandler) { 647 | fatalError("Unsupported") 648 | } 649 | 650 | func destroyTimer(_ handler: any CoreMetrics.TimerHandler) { 651 | fatalError("Unsupported") 652 | } 653 | } 654 | 655 | let handler = CustomFactory.CustomHandler() 656 | let factory = CustomFactory(handler: handler) 657 | 658 | #expect(!factory.didCallDestroyCounter) 659 | do { 660 | let counter1 = Counter(label: "foo", factory: factory) 661 | #expect(counter1._handler is CustomFactory.CustomHandler, "expected a custom metrics handler") 662 | #expect(counter1._factory is CustomFactory, "expected a custom metrics factory") 663 | counter1.destroy() 664 | } 665 | #expect(factory.didCallDestroyCounter) 666 | } 667 | 668 | @Test func destroyingGauge() throws { 669 | let metrics = TestMetrics() 670 | 671 | let name = "gauge-\(UUID().uuidString)" 672 | let value = Double.random(in: -1000...1000) 673 | 674 | let gauge = Gauge(label: name, factory: metrics) 675 | gauge.record(value) 676 | 677 | let recorder = try metrics.expectRecorder(gauge) 678 | #expect(recorder.values.count == 1, "expected number of entries to match") 679 | #expect(recorder.values.first == value, "expected value to match") 680 | #expect(metrics.recorders.count == 1, "recorder should have been stored") 681 | 682 | let identity = ObjectIdentifier(recorder) 683 | gauge.destroy() 684 | #expect(metrics.recorders.count == 0, "recorder should have been released") 685 | 686 | let gaugeAgain = Gauge(label: name, factory: metrics) 687 | gaugeAgain.record(-value) 688 | 689 | let recorderAgain = try metrics.expectRecorder(gaugeAgain) 690 | #expect(recorderAgain.values.count == 1, "expected number of entries to match") 691 | #expect(recorderAgain.values.first == -value, "expected value to match") 692 | 693 | let identityAgain = ObjectIdentifier(recorderAgain) 694 | #expect( 695 | identity != identityAgain, 696 | "since the cached metric was released, the created a new should have a different identity" 697 | ) 698 | } 699 | 700 | @Test func destroyingMeter() throws { 701 | let metrics = TestMetrics() 702 | 703 | let name = "meter-\(UUID().uuidString)" 704 | let value = Double.random(in: -1000...1000) 705 | 706 | let meter = Meter(label: name, factory: metrics) 707 | meter.set(value) 708 | 709 | let testMeter = try metrics.expectMeter(meter) 710 | #expect(testMeter.values.count == 1, "expected number of entries to match") 711 | #expect(testMeter.values.first == value, "expected value to match") 712 | #expect(metrics.meters.count == 1, "recorder should have been stored") 713 | 714 | let identity = ObjectIdentifier(testMeter) 715 | meter.destroy() 716 | #expect(metrics.recorders.count == 0, "recorder should have been released") 717 | 718 | let meterAgain = Meter(label: name, factory: metrics) 719 | meterAgain.set(-value) 720 | 721 | let testMeterAgain = try metrics.expectMeter(meterAgain) 722 | #expect(testMeterAgain.values.count == 1, "expected number of entries to match") 723 | #expect(testMeterAgain.values.first == -value, "expected value to match") 724 | 725 | let identityAgain = ObjectIdentifier(testMeterAgain) 726 | #expect( 727 | identity != identityAgain, 728 | "since the cached metric was released, the created a new should have a different identity" 729 | ) 730 | } 731 | 732 | @Test func destroyingCounter() throws { 733 | let metrics = TestMetrics() 734 | 735 | let name = "counter-\(UUID().uuidString)" 736 | let value = Int.random(in: 0...1000) 737 | 738 | let counter = Counter(label: name, factory: metrics) 739 | counter.increment(by: value) 740 | 741 | let testCounter = try metrics.expectCounter(counter) 742 | #expect(testCounter.values.count == 1, "expected number of entries to match") 743 | #expect(testCounter.values.first == Int64(value), "expected value to match") 744 | #expect(metrics.counters.count == 1, "counter should have been stored") 745 | 746 | let identity = ObjectIdentifier(counter) 747 | counter.destroy() 748 | #expect(metrics.counters.count == 0, "counter should have been released") 749 | 750 | let counterAgain = Counter(label: name, factory: metrics) 751 | counterAgain.increment(by: value) 752 | 753 | let testCounterAgain = try metrics.expectCounter(counterAgain) 754 | #expect(testCounterAgain.values.count == 1, "expected number of entries to match") 755 | #expect(testCounterAgain.values.first == Int64(value), "expected value to match") 756 | 757 | let identityAgain = ObjectIdentifier(counterAgain) 758 | #expect( 759 | identity != identityAgain, 760 | "since the cached metric was released, the created a new should have a different identity" 761 | ) 762 | } 763 | 764 | @Test func destroyingTimer() throws { 765 | let metrics = TestMetrics() 766 | 767 | let name = "timer-\(UUID().uuidString)" 768 | let value = Int64.random(in: 0...1000) 769 | 770 | let timer = Timer(label: name, factory: metrics) 771 | timer.recordNanoseconds(value) 772 | 773 | let testTimer = try metrics.expectTimer(timer) 774 | #expect(testTimer.values.count == 1, "expected number of entries to match") 775 | #expect(testTimer.values.first == value, "expected value to match") 776 | #expect(metrics.timers.count == 1, "timer should have been stored") 777 | 778 | let identity = ObjectIdentifier(timer) 779 | timer.destroy() 780 | #expect(metrics.timers.count == 0, "timer should have been released") 781 | 782 | let timerAgain = Timer(label: name, factory: metrics) 783 | timerAgain.recordNanoseconds(value) 784 | let testTimerAgain = try metrics.expectTimer(timerAgain) 785 | #expect(testTimerAgain.values.count == 1, "expected number of entries to match") 786 | #expect(testTimerAgain.values.first == value, "expected value to match") 787 | 788 | let identityAgain = ObjectIdentifier(timerAgain) 789 | #expect( 790 | identity != identityAgain, 791 | "since the cached metric was released, the created a new should have a different identity" 792 | ) 793 | } 794 | 795 | @Test func descriptions() throws { 796 | let metrics = TestMetrics() 797 | 798 | let counter = Counter(label: "hello.counter", factory: metrics) 799 | #expect("\(counter)" == "Counter(hello.counter, dimensions: [])") 800 | 801 | let gauge = Gauge(label: "hello.gauge", factory: metrics) 802 | #expect("\(gauge)" == "Gauge(hello.gauge, dimensions: [], aggregate: false)") 803 | 804 | let meter = Meter(label: "hello.meter", factory: metrics) 805 | #expect("\(meter)" == "Meter(hello.meter, dimensions: [])") 806 | 807 | let timer = Timer(label: "hello.timer", factory: metrics) 808 | #expect("\(timer)" == "Timer(hello.timer, dimensions: [])") 809 | 810 | let recorder = Recorder(label: "hello.recorder", factory: metrics) 811 | #expect("\(recorder)" == "Recorder(hello.recorder, dimensions: [], aggregate: true)") 812 | } 813 | } 814 | --------------------------------------------------------------------------------