-
Notifications
You must be signed in to change notification settings - Fork 36
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
Changes from all commits
84a0a97
b2cf8f3
69b44ae
a198d5c
b3071b5
11d4480
77f4c39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# file options | ||
|
||
--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 |
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", | ||
] | ||
), | ||
] | ||
) |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I eventually want to change swift-metrics to not require AnyObject anymore. This will make NoOps much cheaper.
Yes. I think users should be able to use the Prometheus lib without going through swift-metrics. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
FranzBusch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public func increment(by amount: Int64) { | ||
precondition(amount >= 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# ``Prometheus`` | ||
|
||
A prometheus client library for Swift. | ||
|
||
## Overview | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`` |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")) | ||
} | ||
} |
There was a problem hiding this comment.
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.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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