swift-server/swift-prometheus

Error "metrics system can only be initialized once per process..." during unit-tests execution

PanArtemG opened this issue · 6 comments

Hey! I understand that PrometheusClient () should be initialized only once, but during the execution of unit tests, the tests are run asynchronously and a new Application instance is created in each test, after which I get an error. How can I solve this?
Thanks!!!

Example test
`
func testForceDeleteRelatedEntity() throws {
let app = try self.createTestApp()
let port = app.http.server.configuration.port
let adminToken = try self.createAdminJWT(app)
let entityID = self.entityID
let adminHeaders = HTTPHeaders([("Authorization", "Bearer (adminToken)")])
defer { app.shutdown() }

    try app.testable(method: .running(port: port)).test(.DELETE, "v1/related-entities/\(entityID)/force-delete", headers: adminHeaders, afterResponse: { (res) in
        XCTAssertEqual(res.status, .noContent)
    })
    try app.autoRevert().wait()
}

`

Thanks for opening this issue!

From your code I'm going to assume that the metrics are being initialized in self.createTestApp()? If so, you can fix the issue you're running into by replacing MetricsSystem.bootstrap with MetricsSystem.bootstrapInternal (requires @testable import Metrics. This helper is specifically designed for bootstrapping metrics in (unit) tests and allows to bootstrap multiple times.

Hope this helps!

Thank you for responding so quickly. Your assumption is not entirely correct.

file - routes.swift
`
//MARK: - Initialize Prometheus exporting & metrics.
let prometheusClient = PrometheusClient()
MetricsSystem.bootstrap(prometheusClient)
let metricsCollector = try MetricsSystem.prometheus()

//MARK: -  Create gauge
let activeUserCount = metricsCollector.createGauge(forType: Int.self, named: "recently_active_users")

//MARK: - Route `Get metrics`
app.get("metrics") { req -> EventLoopFuture<String> in
    activeUserCount.set(app.sessions.memory.storage.sessions.count)
    let metricResponse = req.eventLoop.makePromise(of: String.self)
    metricsCollector.collect(into: metricResponse)
    return metricResponse.futureResult
}

`

To be honest, I didn't understand very well how I did all this and used the first working version I found on the Internet

Снимок экрана 2021-01-29 в 18 30 56

MetricsSystem.bootstrapInternal - I do not have that)

Everything worked out and works. This is my mistake, I was wrong with this function.
Thank you for not leaving me in trouble))))

//MARK: - Route Get metrics
private func getMetrics(_ req: Request) -> EventLoopFuture {
let promise = req.eventLoop.makePromise(of: String.self)
DispatchQueue.global().async {
do {
try MetricsSystem.prometheus().collect(into: promise)
} catch {
promise.fail(error)
}
}
return promise.futureResult
}

Thanks for opening this issue!

From your code I'm going to assume that the metrics are being initialized in self.createTestApp()? If so, you can fix the issue you're running into by replacing MetricsSystem.bootstrap with MetricsSystem.bootstrapInternal (requires @testable import Metrics. This helper is specifically designed for bootstrapping metrics in (unit) tests and allows to bootstrap multiple times.

Hope this helps!

Hi, @MrLotU ! I read the your discussion. But I still did't understand how to set up SwiftPrometneus to work with tests. In my case it is required to check that metrics are returned along the route.
I initialize the application:
override func setUpWithError() throws { app = Application(.testing) try configure(app) }
Next, I call the route and at least wait for an answer - Ok. But the server error comes.
func testAppMetrics() throws { try app.test(.GET, "v1/metrics", afterResponse: { (res) in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .plainText) XCTAssertNotNil(res.content) XCTAssertNotNil(res.body) }) }
I use
@testable import App @testable import Metrics import XCTVapor
And in main.swift - MetricsSystem.bootstrap(PrometheusClient())
I would be grateful if you could explain why .internalServerError returns? But when I call the same route through the API client everything works.