Skip to content
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
25 changes: 6 additions & 19 deletions Sources/App/Core/AppMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,6 @@ import Vapor

enum AppMetrics {

static let initialized = Mutex(false)

static func bootstrap() {
// prevent tests from boostrapping multiple times
guard !initialized.withLock({ $0 }) else { return }
initialized.withLock {
let client = PrometheusClient()
MetricsSystem.bootstrap(PrometheusMetricsFactory(client: client))
$0 = true
}
}

// metrics

static var analyzeCandidatesCount: PromGauge<Int>? {
gauge("spi_analyze_candidates_count")
}
Expand Down Expand Up @@ -155,13 +141,13 @@ enum AppMetrics {
extension AppMetrics {

static func counter<V: Numeric>(_ name: String) -> PromCounter<V>? {
try? MetricsSystem.prometheus()
.createCounter(forType: V.self, named: name)
@Dependency(\.metricsSystem.prometheus) var prometheus
return try? prometheus().createCounter(forType: V.self, named: name)
}

static func gauge<V: DoubleRepresentable>(_ name: String) -> PromGauge<V>? {
try? MetricsSystem.prometheus()
.createGauge(forType: V.self, named: name)
@Dependency(\.metricsSystem.prometheus) var prometheus
return try? prometheus().createGauge(forType: V.self, named: name)
}

}
Expand All @@ -176,14 +162,15 @@ extension AppMetrics {
static func push(client: Client, jobName: String) async throws {
@Dependency(\.environment) var environment
@Dependency(\.logger) var logger
@Dependency(\.metricsSystem.prometheus) var prometheus

guard let pushGatewayUrl = environment.metricsPushGatewayUrl() else {
throw AppError.envVariableNotSet("METRICS_PUSHGATEWAY_URL")
}
let url = URI(string: "\(pushGatewayUrl)/metrics/job/\(jobName)")

do {
let metrics: String = try await MetricsSystem.prometheus().collect()
let metrics: String = try await prometheus().collect()
_ = try await client.post(url) { req in
// append "\n" to avoid
// text format parsing error in line 4: unexpected end of input stream
Expand Down
69 changes: 69 additions & 0 deletions Sources/App/Core/Dependencies/MetricsSystemClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Dependencies
import Metrics
import Synchronization
@preconcurrency import Prometheus


struct MetricsSystemClient {
var prometheus: @Sendable () throws -> PrometheusClient
}


extension MetricsSystemClient {
private static let initialized = Mutex(false)

func bootstrap() {
guard !Self.initialized.withLock({ $0 }) else { return }
Self.initialized.withLock {
let client = PrometheusClient()
MetricsSystem.bootstrap(PrometheusMetricsFactory(client: client))
$0 = true
}
}
}


extension MetricsSystemClient: DependencyKey {
static var liveValue: Self {
.init(prometheus: { try MetricsSystem.prometheus() })
}
}


extension MetricsSystemClient: TestDependencyKey {
static var testValue: Self {
.init(prometheus: { unimplemented("testValue"); return .init() })
}
}


extension DependencyValues {
var metricsSystem: MetricsSystemClient {
get { self[MetricsSystemClient.self] }
set { self[MetricsSystemClient.self] = newValue }
}
}


#if DEBUG
extension MetricsSystemClient {
static var mock: Self {
let prometheus = PrometheusClient()
return .init(prometheus: { prometheus })
}
}
#endif
3 changes: 2 additions & 1 deletion Sources/App/configure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ public func configure(_ app: Application, databasePort: Int? = nil) async throws
try routes(app)

// bootstrap app metrics
AppMetrics.bootstrap()
@Dependency(\.metricsSystem) var metricsSystem
metricsSystem.bootstrap()

return host
}
3 changes: 2 additions & 1 deletion Sources/App/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ func routes(_ app: Application) throws {

do { // Metrics
app.get("metrics") { req -> String in
try await MetricsSystem.prometheus().collect()
@Dependency(\.metricsSystem.prometheus) var prometheus
return try await prometheus().collect()
}
}
}
9 changes: 7 additions & 2 deletions Tests/AppTests/AllTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Testing
@testable import App

import Dependencies
import Testing


@Suite(.dependency(\.date.now, .t0)) struct AllTests { }
@Suite(
.dependency(\.date.now, .t0),
.dependency(\.metricsSystem, .mock)
) struct AllTests { }


extension AllTests {
Expand Down
21 changes: 4 additions & 17 deletions Tests/AppTests/MetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,6 @@ extension AllTests.MetricsTests {
@Test func versions_added() async throws {
try await withApp { app in
// setup
let initialAddedBranch = try #require(
AppMetrics.analyzeVersionsAddedCount?.get(.versionLabels(kind: .branch))
)
let initialAddedTag = try #require(
AppMetrics.analyzeVersionsAddedCount?.get(.versionLabels(kind: .tag))
)
let initialDeletedBranch = try #require(
AppMetrics.analyzeVersionsDeletedCount?.get(.versionLabels(kind: .branch))
)
let initialDeletedTag = try #require(
AppMetrics.analyzeVersionsDeletedCount?.get(.versionLabels(kind: .tag))
)
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't need to baseline the counts anymore because we have a new, 0-based prometheus client on each test run.

let pkg = try await savePackage(on: app.db, "1")
let new = [
try Version(package: pkg, reference: .branch("main")),
Expand All @@ -91,16 +79,16 @@ extension AllTests.MetricsTests {

// validation
#expect(
AppMetrics.analyzeVersionsAddedCount?.get(.versionLabels(kind: .branch)) == initialAddedBranch + 1
AppMetrics.analyzeVersionsAddedCount?.get(.versionLabels(kind: .branch)) == 1
)
#expect(
AppMetrics.analyzeVersionsAddedCount?.get(.versionLabels(kind: .tag)) == initialAddedTag + 2
AppMetrics.analyzeVersionsAddedCount?.get(.versionLabels(kind: .tag)) == 2
)
#expect(
AppMetrics.analyzeVersionsDeletedCount?.get(.versionLabels(kind: .branch)) == initialDeletedBranch + 1
AppMetrics.analyzeVersionsDeletedCount?.get(.versionLabels(kind: .branch)) == 1
)
#expect(
AppMetrics.analyzeVersionsDeletedCount?.get(.versionLabels(kind: .tag)) == initialDeletedTag + 1
AppMetrics.analyzeVersionsDeletedCount?.get(.versionLabels(kind: .tag)) == 1
)
}
}
Expand Down Expand Up @@ -164,7 +152,6 @@ extension AllTests.MetricsTests {

// validation
#expect((AppMetrics.buildTriggerDurationSeconds?.get()) ?? 0 > 0)
print(AppMetrics.buildTriggerDurationSeconds!.get())
}
}
}
Expand Down
Loading