Skip to content

New Version 2.0 API #92

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# file options
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all of our recent projects, we actually adopted swift-format since it has a more stable formatting between versions. Would recommend picking it here as well.

Copy link
Member Author

@fabianfett fabianfett Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this in a follow up? #94


--swiftversion 5.7
--exclude .build

# format options

--self insert
--patternlet inline
--ranges nospace
--stripunusedargs unnamed-only
--ifdef no-indent
--extensionacl on-declarations
--disable typeSugar # https://github.com/nicklockwood/SwiftFormat/issues/636
--disable andOperator
--disable wrapMultilineStatementBraces
--disable enumNamespaces
--disable redundantExtensionACL
--disable redundantReturn
--disable preferKeyPath
--disable sortedSwitchCases
--disable numberFormatting

# rules
49 changes: 32 additions & 17 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
// swift-tools-version:5.2
// swift-tools-version:5.7
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftPrometheus open source project
//
// Copyright (c) 2018-2023 SwiftPrometheus project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import PackageDescription

let package = Package(
name: "SwiftPrometheus",
name: "swift-prometheus",
platforms: [.macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16)],
products: [
.library(
name: "SwiftPrometheus",
targets: ["Prometheus"])
name: "Prometheus",
targets: ["Prometheus"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.2.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.4.1"),

// ~~~ SwiftPM Plugins ~~~
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"),
],
targets: [
.target(
name: "Prometheus",
dependencies: [
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "CoreMetrics", package: "swift-metrics"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
.product(name: "NIO", package: "swift-nio"),
]),
.target(
name: "PrometheusExample",
dependencies: [
.target(name: "Prometheus"),
.product(name: "Metrics", package: "swift-metrics"),
]),
]
),
.testTarget(
name: "SwiftPrometheusTests",
dependencies: [.target(name: "Prometheus")]),
name: "PrometheusTests",
dependencies: [
"Prometheus",
]
),
]
)
99 changes: 99 additions & 0 deletions Sources/Prometheus/Counter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftPrometheus open source project
//
// Copyright (c) 2018-2023 SwiftPrometheus project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Atomics
import CoreMetrics

/// 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.
///
/// Do not use a counter to expose a value that can decrease. For example, do not use a counter for the
/// number of currently running processes; instead use a ``Gauge``.
public final class Counter: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we require that all metric backing handlers are classes? This is a bit sad since this could just be a struct with reference semantics.

Second question, does this need to be public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we require that all metric backing handlers are classes? This is a bit sad since this could just be a struct with reference semantics.

I eventually want to change swift-metrics to not require AnyObject anymore. This will make NoOps much cheaper.

Second question, does this need to be public?

Yes. I think users should be able to use the Prometheus lib without going through swift-metrics.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on both, eventually we can do a revamp of metrics lib. It's one of the earliest and we've learned much since.

Yes, types should be public, prom has more detailed semantics than swift metrics so an "app" can definitely use it direcly

private let intAtomic = ManagedAtomic(Int64(0))
private let floatAtomic = ManagedAtomic(Double(0).bitPattern)

let name: String
let labels: [(String, String)]
private let prerenderedExport: [UInt8]

init(name: String, labels: [(String, String)]) {
self.name = name
self.labels = labels

var prerendered = [UInt8]()
// 64 bytes is a good tradeoff to prevent reallocs lots of reallocs when appending names
// and memory footprint.
prerendered.reserveCapacity(64)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you comment why we need to reserve 64 here?

prerendered.append(contentsOf: name.utf8)
if let prerenderedLabels = Self.prerenderLabels(labels) {
prerendered.append(UInt8(ascii: "{"))
prerendered.append(contentsOf: prerenderedLabels)
prerendered.append(contentsOf: #"} "#.utf8)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't use ascii here?

} else {
prerendered.append(UInt8(ascii: " "))
}

self.prerenderedExport = prerendered
}

public func increment() {
self.increment(by: Int64(1))
}

public func increment(by amount: Int64) {
precondition(amount >= 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we give those preconditions good messages always please? It's much nicer user experience when crashes contain message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW crashed don't contain precondition messages. Only fatal errors messages are included. I have personally switched over to fatal error exclusively because of that reason

self.intAtomic.wrappingIncrement(by: amount, ordering: .relaxed)
}

public func increment(by amount: Double) {
precondition(amount >= 0)
// We busy loop here until we can update the atomic successfully.
// Using relaxed ordering here is sufficient, since the as-if rules guarantess that
// the following operations are executed in the order presented here. Every statement
// depends on the execution before.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's clear here but thx for comments around busy loops, always good to document those 💯

while true {
let bits = self.floatAtomic.load(ordering: .relaxed)
let value = Double(bitPattern: bits) + amount
let (exchanged, _) = self.floatAtomic.compareExchange(expected: bits, desired: value.bitPattern, ordering: .relaxed)
if exchanged {
break
}
}
}

public func reset() {
self.intAtomic.store(0, ordering: .relaxed)
self.floatAtomic.store(Double.zero.bitPattern, ordering: .relaxed)
}
}

extension Counter: CoreMetrics.CounterHandler {}
extension Counter: CoreMetrics.FloatingPointCounterHandler {}

extension Counter: PrometheusMetric {
func emit(into buffer: inout [UInt8]) {
buffer.append(contentsOf: self.prerenderedExport)
let doubleValue = Double(bitPattern: self.floatAtomic.load(ordering: .relaxed))
let intValue = self.intAtomic.load(ordering: .relaxed)
if doubleValue == .zero {
buffer.append(contentsOf: "\(intValue)".utf8)
} else {
buffer.append(contentsOf: "\(doubleValue + Double(intValue))".utf8)
}
buffer.append(UInt8(ascii: "\n"))
}
}
19 changes: 19 additions & 0 deletions Sources/Prometheus/Docs.docc/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ``Prometheus``

A prometheus client library for Swift.

## Overview
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add installation instructions to the index here as well. Allows us to point people directly at the documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this in a follow up?


``Prometheus`` supports creating ``Counter``s, ``Gauge``s and ``Histogram``s and exporting their
values in the Prometheus export format.

## Topics

### Collectors

- ``Counter``
- ``Gauge``
- ``Histogram``
- ``Bucketable``
- ``DurationHistogram``
- ``ValueHistogram``
91 changes: 91 additions & 0 deletions Sources/Prometheus/Gauge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftPrometheus open source project
//
// Copyright (c) 2018-2023 SwiftPrometheus project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Atomics
import CoreMetrics

/// 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 concurrent requests.
public final class Gauge: Sendable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In follow up please add docs to all those types

let atomic = ManagedAtomic(Double.zero.bitPattern)

let name: String
let labels: [(String, String)]
let prerenderedExport: [UInt8]

init(name: String, labels: [(String, String)]) {
self.name = name
self.labels = labels

var prerendered = [UInt8]()
// 64 bytes is a good tradeoff to prevent reallocs lots of reallocs when appending names
// and memory footprint.
prerendered.reserveCapacity(64)
prerendered.append(contentsOf: name.utf8)
if let prerenderedLabels = Self.prerenderLabels(labels) {
prerendered.append(UInt8(ascii: "{"))
prerendered.append(contentsOf: prerenderedLabels)
prerendered.append(contentsOf: #"} "#.utf8)
} else {
prerendered.append(UInt8(ascii: " "))
}

self.prerenderedExport = prerendered
}

public func set(to value: Double) {
self.atomic.store(value.bitPattern, ordering: .relaxed)
}

public func increment(by amount: Double = 1.0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit inconsistent, on purpose? The counter had Int64 variants of APIs as well, no need here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope not un porpose

// We busy loop here until we can update the atomic successfully.
// Using relaxed ordering here is sufficient, since the as-if rules guarantess that
// the following operations are executed in the order presented here. Every statement
// depends on the execution before.
while true {
let bits = self.atomic.load(ordering: .relaxed)
let value = Double(bitPattern: bits) + amount
let (exchanged, _) = self.atomic.compareExchange(expected: bits, desired: value.bitPattern, ordering: .relaxed)
if exchanged {
break
}
}
}

public func decrement(by amount: Double = 1.0) {
self.increment(by: -amount)
}
}

extension Gauge: CoreMetrics.RecorderHandler {
public func record(_ value: Int64) {
self.record(Double(value))
}

public func record(_ value: Double) {
self.set(to: value)
}
}

extension Gauge: PrometheusMetric {
func emit(into buffer: inout [UInt8]) {
let value = Double(bitPattern: self.atomic.load(ordering: .relaxed))

buffer.append(contentsOf: self.prerenderedExport)
buffer.append(contentsOf: "\(value)".utf8)
buffer.append(UInt8(ascii: "\n"))
}
}
Loading