diff --git a/Sources/App/Core/AppMetrics.swift b/Sources/App/Core/AppMetrics.swift index 2c3fb00d1..861a46530 100644 --- a/Sources/App/Core/AppMetrics.swift +++ b/Sources/App/Core/AppMetrics.swift @@ -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? { gauge("spi_analyze_candidates_count") } @@ -155,13 +141,13 @@ enum AppMetrics { extension AppMetrics { static func counter(_ name: String) -> PromCounter? { - 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(_ name: String) -> PromGauge? { - try? MetricsSystem.prometheus() - .createGauge(forType: V.self, named: name) + @Dependency(\.metricsSystem.prometheus) var prometheus + return try? prometheus().createGauge(forType: V.self, named: name) } } @@ -176,6 +162,7 @@ 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") @@ -183,7 +170,7 @@ extension AppMetrics { 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 diff --git a/Sources/App/Core/Dependencies/MetricsSystemClient.swift b/Sources/App/Core/Dependencies/MetricsSystemClient.swift new file mode 100644 index 000000000..ac64f26da --- /dev/null +++ b/Sources/App/Core/Dependencies/MetricsSystemClient.swift @@ -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 diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index e3b3f0716..2d442bb8d 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -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 } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index dbfb05536..f21c9e372 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -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() } } } diff --git a/Tests/AppTests/AllTests.swift b/Tests/AppTests/AllTests.swift index 6800ebdb2..a7f4c67d0 100644 --- a/Tests/AppTests/AllTests.swift +++ b/Tests/AppTests/AllTests.swift @@ -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 { diff --git a/Tests/AppTests/MetricsTests.swift b/Tests/AppTests/MetricsTests.swift index 3d81d52f4..5f9e82cd2 100644 --- a/Tests/AppTests/MetricsTests.swift +++ b/Tests/AppTests/MetricsTests.swift @@ -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)) - ) let pkg = try await savePackage(on: app.db, "1") let new = [ try Version(package: pkg, reference: .branch("main")), @@ -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 ) } } @@ -164,7 +152,6 @@ extension AllTests.MetricsTests { // validation #expect((AppMetrics.buildTriggerDurationSeconds?.get()) ?? 0 > 0) - print(AppMetrics.buildTriggerDurationSeconds!.get()) } } }