diff --git a/.gitignore b/.gitignore index 228beed..4842982 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Packages *.xcodeproj Package.pins Package.resolved -.DS_Store \ No newline at end of file +.DS_Store +.swiftpm diff --git a/Package.swift b/Package.swift index ae3ece7..5a8a2a7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,21 @@ -// swift-tools-version:4.0 +// swift-tools-version:4.2 import PackageDescription let package = Package( name: "VaporMonitoring", products: [ - .library(name: "VaporMonitoring", targets: ["VaporMonitoring"]) + .library(name: "VaporMonitoring", targets: ["VaporMonitoring"]), + .executable(name: "MonitoringExample", targets: ["MonitoringExample"]) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.1.0"), - .package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.3.0"), - .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "0.2.0") + .package(url: "https://github.com/apple/swift-metrics.git", from: "1.2.0"), + .package(url: "https://github.com/Yasumoto/SwiftPrometheus.git", .branch("nio1")), + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0") ], targets: [ - .target(name: "VaporMonitoring", dependencies: ["Vapor", "SwiftMetrics", "SwiftPrometheus"]), - .target(name: "MonitoringExample", dependencies: ["VaporMonitoring"]) + .target(name: "VaporMonitoring", dependencies: ["Metrics", "SwiftPrometheus", "Vapor"]), + .target(name: "MonitoringExample", dependencies: ["VaporMonitoring"]), + .testTarget(name: "VaporMonitoringTests", dependencies: ["VaporMonitoring"]) ] ) diff --git a/README.md b/README.md index 59d5403..fc6d3c1 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,57 @@ # VaporMonitoring [![Vapor 3](https://img.shields.io/badge/vapor-3.0-blue.svg?style=flat)](https://vapor.codes) -[![Swift 4.1](https://img.shields.io/badge/swift-4.2-orange.svg?style=flat)](http://swift.org) +[![Swift 4.2](https://img.shields.io/badge/swift-4.2-orange.svg?style=flat)](http://swift.org) -## +## Introduction -`VaporMonitoring` is a Vapor 3 package for monitoring and providing metrics for your Vapor application. Built on top op [SwiftMetrics](https://github.com/RuntimeTools/SwiftMetrics) and [SwiftPrometheus](https://github.com/MrLotU/SwiftPrometheus). Vapor Monitoring provides the default SwiftMetrics metrics along with request specific metrics. Metrics are exposed using Prometheus. +`VaporMonitoring` is a Vapor 3 package for monitoring and providing metrics for your Vapor application. Built on top of [the `swift-metrics` package](https://github.com/apple/swift-metrics) it also provides a helper to bootstrap [SwiftPrometheus](https://github.com/MrLotU/SwiftPrometheus). `VaporMonitoring` provides middleware which will [track metrics using the `RED` method for your application](https://www.weave.works/blog/the-red-method-key-metrics-for-microservices-architecture/): + +1. Request Count +2. Error Count +3. Duration of each request + +It breaks these out by URL path, status code, and method for fine-grained insight. ## Installation + Vapor Monitoring can be installed using SPM + ```swift -.package(url: "https://github.com/vapor-community/VaporMonitoring.git", from: "2.0.0") +.package(url: "https://github.com/vapor-community/VaporMonitoring.git", from: "3.0.0") ``` ## Usage -Vapor Monitoring is easy to use, it requires only a few lines of code. -Vapor Monitoring requires a few things to work correclty, a `MonitoredRouter` and a `MonitoredResponder` are the most important ones. +### `MetricsMiddleware` + +Most folks will want easy integration with `swift-metrics`, in which case you should use `MetricsMiddleware`. + +Once you've brought the package into your project, you'll need to `import VaporMonitoring` in your `Configure.swift` file. Inside, you'll create a `MetricsMiddleware`: -To set up your monitoring, in your `Configure.swift` file, add the following: ```swift -let router = try VaporMonitoring.setupMonitoring(&config, &services) -services.register(router, as: Router.self) +services.register(MetricsMiddleware(), as: MetricsMiddleware.self) + +var middlewares = MiddlewareConfig() +middlewares.use(MetricsMiddleware.self) +// Add other middleware, such as the Vapor-provided +middlewares.use(ErrorMiddleware.self) +services.register(middlewares) ``` -What this does is load VaporMonitoring with the default configuration. This includes adding all required services to your apps services & setting some configuration prefferences to use the `MonitoredResponder` and `MonitoredRouter`. +This will place the monitoring inside your application, tracking incoming requests + outgoing responses, and calculating how long it takes for each to complete. + +*Note*: Place the `MetricsMiddleware` in your `MiddlewareConfig` as early as possible (preferably first) so you can track the entire duration. -By default, your prometheus metrics will be served at `host:port/metrics` and routes that don't have a routing closure, will be ignored to avoid exploding your prometheus logs. You can however customize this. +### Prometheus Integration + +If you'd like to take advantage of a Prometheus installation, you'll need to export the `/metrics` endpoint in your list of routes: -To customize your monitoring, add this to `Configure.swift` ```swift -let monitoringConfg = MonitoringConfig(prometheusRoute: "customRoute", onlyBuiltinRoutes: false) -let router = try VaporMonitoring.setupMonitoring(&config, &services, monitoringConfg) +let router = EngineRouter.default() +try routes(router) +let prometheusService = VaporPrometheus(router: router, route: "metrics") +services.register(prometheusService) services.register(router, as: Router.self) ``` -In this case, you'd have your prometheus metrics at `host:port/customRoute`. + +This will bootstrap `SwiftPrometheus` as your chosen backend, and also export metrics on `/metrics` (by default). diff --git a/Sources/MonitoringExample/main.swift b/Sources/MonitoringExample/main.swift index 6ac0a5b..cd1b16f 100644 --- a/Sources/MonitoringExample/main.swift +++ b/Sources/MonitoringExample/main.swift @@ -1,6 +1,5 @@ import Vapor import VaporMonitoring -import SwiftMetrics public func routes(_ router: Router) throws { // Basic "Hello, world!" example @@ -11,16 +10,16 @@ public func routes(_ router: Router) throws { /// Called before your application initializes. public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { - /// Register routes to the router - - let mConfig = MonitoringConfig(prometheusRoute: "metrics", onlyBuiltinRoutes: true) - let router = try VaporMonitoring.setupMonitoring(&config, &services, mConfig) - + services.register(MetricsMiddleware(), as: MetricsMiddleware.self) var middlewares = MiddlewareConfig() + middlewares.use(MetricsMiddleware.self) middlewares.use(ErrorMiddleware.self) services.register(middlewares) - + + let router = EngineRouter.default() try routes(router) + let prometheusService = VaporPrometheus(router: router, services: &services) + services.register(prometheusService) services.register(router, as: Router.self) } diff --git a/Sources/VaporMonitoring/Extensions.swift b/Sources/VaporMonitoring/Extensions.swift deleted file mode 100644 index 48d0ce0..0000000 --- a/Sources/VaporMonitoring/Extensions.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Extensions.swift -// VaporMonitoring -// -// Created by Jari Koopman on 01/06/2018. -// - -import Foundation -import SwiftMetrics -import Vapor - -// MARK: - Vapor extensions - -extension Request: Equatable { - public static func == (lhs: Request, rhs: Request) -> Bool { - return lhs.description == rhs.description && lhs.debugDescription == rhs.debugDescription - } -} - -/// Conforms SwiftMetrics to Service so we can register it with Vapor -extension SwiftMetrics: Service { } - -public typealias requestClosure = (RequestData) -> () - -// MARK: - SwiftMetrics extensions - -public extension SwiftMonitor.EventEmitter { - static var requestObservers: [requestClosure] = [] - - static func publish(data: RequestData) { - for process in requestObservers { - process(data) - } - } - - static func subscribe(callback: @escaping requestClosure) { - requestObservers.append(callback) - } -} - -public extension SwiftMonitor { - public func on(_ callback: @escaping requestClosure) { - EventEmitter.subscribe(callback: callback) - } - - func raiseEvent(data: RequestData) { - EventEmitter.publish(data: data) - } -} - -public extension SwiftMetrics { - public func emitData(_ data: RequestData) { - if let monitor = swiftMon { - monitor.raiseEvent(data: data) - } - } -} diff --git a/Sources/VaporMonitoring/MetricsMiddleware.swift b/Sources/VaporMonitoring/MetricsMiddleware.swift new file mode 100644 index 0000000..5709092 --- /dev/null +++ b/Sources/VaporMonitoring/MetricsMiddleware.swift @@ -0,0 +1,61 @@ +import Metrics +import Vapor + +/// Middleware to track in per-request metrics +/// +/// This middleware is "backend-agnostic" and can be used with any `swift-metrics`-compatible +/// implementation. It is based +/// [off the RED Method](https://www.weave.works/blog/the-red-method-key-metrics-for-microservices-architecture/) +public final class MetricsMiddleware { + let requestsCounterLabel = "http_requests_total" + let requestsTimerLabel = "http_requests_duration_seconds" + let requestErrorsLabel = "http_request_errors_total" + + public init() { } +} + +// We track the start time of each request, then when it comes "back out" and toward the client +// we can specify the total duration of the request. +extension MetricsMiddleware: Middleware { + public func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture { + let start = Date().timeIntervalSince1970 + let response: Future + do { + response = try next.respond(to: request) + } catch { + response = request.eventLoop.newFailedFuture(error: error) + } + + _ = response.map { response in + self.updateMetrics(for: request, responseCounterName: self.requestsCounterLabel, start: start, statusCode: response.http.status.code) + }.mapIfError { error in + self.updateMetrics(for: request, responseCounterName: self.requestErrorsLabel, start: start) + } + + return response + } + + private func updateMetrics(for request: Request, responseCounterName: String, start: Double, statusCode: UInt? = nil) { + let topLevel = String(request.http.url.path.split(separator: "/").first ?? "/") + var counterDimensions = [ + ("method", request.http.method.string), + ("path", topLevel)] + if let statusCode = statusCode { + counterDimensions.append(("status_code", "\(statusCode)")) + } + let timerDimensions = [ + ("method", request.http.method.string), + ("path", topLevel)] + let end = Date().timeIntervalSince1970 + let duration = end - start + + Metrics.Counter(label: responseCounterName, dimensions: counterDimensions).increment() + Metrics.Timer(label: self.requestsTimerLabel, dimensions: timerDimensions, preferredDisplayUnit: .seconds).recordSeconds(duration) + } +} + +extension MetricsMiddleware: ServiceType { + public static func makeService(for container: Container) throws -> MetricsMiddleware { + return MetricsMiddleware() + } +} diff --git a/Sources/VaporMonitoring/Responder+Monitoring.swift b/Sources/VaporMonitoring/Responder+Monitoring.swift deleted file mode 100644 index 18a9ba4..0000000 --- a/Sources/VaporMonitoring/Responder+Monitoring.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Responder+Monitoring.swift -// VaporMonitoring -// -// Created by Jari Koopman on 01/06/2018. -// - -import Foundation -import SwiftMetrics -import Vapor - -/// Responder subclass adding monitoring -/// Built on top of a responder of your choosing (defaults to the default ApplicationResponder) -public final class MonitoredResponder: Responder, ServiceType { - /// See `ServiceType` - public static var serviceSupports: [Any.Type] { return [Responder.self] } - - /// See `ServiceType` - public static func makeService(for worker: Container) throws -> MonitoredResponder { - let baseResponder = try worker.make(ApplicationResponder.self) - let metrics = try worker.make(SwiftMetrics.self) - return try .monitoring(responder: baseResponder, metrics: metrics) - } - - /// See `Responder.respond` - public func respond(to req: Request) throws -> EventLoopFuture { - // Logging - return try self.responder.respond(to: req).map(to: Response.self, { (res) in - queue.sync { - for (index, r) in requestsLog.enumerated() { - if req == r.request { - self.metrics.emitData(RequestData(timestamp: Int(r.timestamp), url: r.route, requestDuration: timeIntervalSince1970MilliSeconds - r.timestamp, statusCode: res.http.status.code, method: r.request.http.method)) - requestsLog.remove(at: index) - break - } - } - } - return res - }) - } - - /// Internal responder - private let responder: Responder - /// SwiftMetrics instance - private let metrics: SwiftMetrics - - /// Creates a new MonitoredResponder with provided Responder and Metrics - init(responder: Responder, metrics: SwiftMetrics) throws { - guard type(of: responder) != type(of: self) else { - throw VaporError(identifier: "responderType", reason: "Can't provide a `MonitoredResponder` to `MonitoredResponder`", suggestedFixes: ["Provide a different type of `Responder` to `MonitoredResponder`"]) - } - self.responder = responder - self.metrics = metrics - } - - /// Easy initalization of monitored resonder - public static func monitoring(responder: Responder, metrics: SwiftMetrics) throws -> MonitoredResponder { - return try MonitoredResponder(responder: responder, metrics: metrics) - } -} diff --git a/Sources/VaporMonitoring/Router+Monitoring.swift b/Sources/VaporMonitoring/Router+Monitoring.swift deleted file mode 100644 index 7c102a2..0000000 --- a/Sources/VaporMonitoring/Router+Monitoring.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Router+Monitoring.swift -// VaporMonitoring -// -// Created by Jari Koopman on 01/06/2018. -// - -import Foundation -import SwiftMetrics -import Vapor - -/// Router subclass adding monitoring -/// Built on top of a router of your choosing (defaults to the default EngineRouter) -public final class MonitoredRouter: Router { - /// See `Router.register` - public func register(route: Route) { - router.register(route: route) - } - - /// See `Router.routes` - public var routes: [Route] { - return router.routes - } - - /// See `Router.route` - /// Adds logging to routing a request - public func route(request: Request) -> Responder? { - - let result = router.route(request: request) - - // Logging - queue.sync { - var path: String = request.http.urlString - if onlyBuiltinRoutes { - guard let _ = result, let route = routes.first(where: { (route) -> Bool in - route.path.readable == "/\(request.http.method.string)\(request.http.url.path)" - }) else { return } - path = route.path.readable - } - - if requestsLog.count > 1000 { - requestsLog.removeFirst() - } - requestsLog.append(RequestLog(request: request, timestamp: timeIntervalSince1970MilliSeconds, route: path)) - } - - return result - } - - /// The internal router - private let router: Router - - /// Show only builtin routes in metrics - private let onlyBuiltinRoutes: Bool - - /// Initializes MonitoredRouter with internal with an internal router to use for actual routing - public init(router: Router = EngineRouter.default(), onlyBuiltinRoutes: Bool = true) throws { - guard type(of: router) != type(of: self) else { - throw VaporError(identifier: "routerType", reason: "Can't provide a `MonitoredRouter` to `MonitoredRouter`", suggestedFixes: ["Provide a different type of `Router` to `MonitoredRouter`"]) - } - self.router = router - self.onlyBuiltinRoutes = onlyBuiltinRoutes - } -} diff --git a/Sources/VaporMonitoring/VaporMetricsPrometheus.swift b/Sources/VaporMonitoring/VaporMetricsPrometheus.swift deleted file mode 100644 index cfc1cfb..0000000 --- a/Sources/VaporMonitoring/VaporMetricsPrometheus.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// VaporMetricsPrometheus.swift -// VaporMonitoring -// -// Created by Jari Koopman on 31/05/2018. -// - -import Vapor -import SwiftMetrics -import Prometheus - -let vaporMonitoringPromClient = PrometheusClient() - -let osCPUUsed = vaporMonitoringPromClient.createGauge(forType: Float.self, named: "os_cpu_used_ratio", helpText: "The ratio of the systems CPU that is currently used (values are 0-1)") -let processCPUUsed = vaporMonitoringPromClient.createGauge(forType: Float.self, named: "process_cpu_used_ratio", helpText: "The ratio of the process CPU that is currently used (values are 0-1)") - -let osResidentBytes = vaporMonitoringPromClient.createGauge(forType: Int.self, named: "os_resident_memory_bytes", helpText: "OS memory size in bytes.") -let processResidentBytes = vaporMonitoringPromClient.createGauge(forType: Int.self, named: "process_resident_memory_bytes", helpText: "Resident memory size in bytes.") -let processVirtualBytes = vaporMonitoringPromClient.createGauge(forType: Int.self, named: "process_virtual_memory_bytes", helpText: "Virtual memory size in bytes.") - -let requestsTotal = vaporMonitoringPromClient.createCounter(forType: Int.self, named: "http_requests_total", helpText: "Total number of HTTP requests made.", withLabelType: TotalRequestsLabels.self) - -let requestDuration = vaporMonitoringPromClient.createSummary(forType: Double.self, named: "http_request_duration_microseconds", helpText: "The HTTP request latencies in microseconds.", labels: RequestDurationLabels.self) - - -func cpuEvent(cpu: CPUData) { - osCPUUsed.set(cpu.percentUsedBySystem) - processCPUUsed.set(cpu.percentUsedByApplication) -} - -func memEvent(mem: MemData) { - osResidentBytes.set(mem.totalRAMUsed) - processResidentBytes.set(mem.applicationRAMUsed) - processVirtualBytes.set(mem.applicationAddressSpaceSize) -} - -func httpEvent(http: RequestData) { - requestsTotal.inc(1, TotalRequestsLabels(http.statusCode, http.url, http.method.string)) - requestDuration.observe(http.requestDuration * 1000.0, RequestDurationLabels(http.url)) -} - -struct TotalRequestsLabels: MetricLabels { - let code: UInt - let handler: String - let method: String - - init() { - self.code = 0 - self.handler = "*" - self.method = "*" - } - - init(_ c: UInt, _ h: String, _ m: String) { - self.code = c - self.handler = h - self.method = m - } -} - -struct RequestDurationLabels: SummaryLabels { - var quantile: String = "" - let handler: String - - init() { - self.handler = "*" - } - - init(_ h: String) { - self.handler = h - } -} - -/// Class providing Prometheus data -/// Powered by SwiftMetrics -public class VaporMetricsPrometheus: Service { - var monitor: SwiftMonitor - var metrics: SwiftMetrics - - let p_quantiles: [Double] = [0.5,0.9,0.99] - - public init(metrics: SwiftMetrics, router: Router, route: [String]) throws { - self.metrics = metrics - self.monitor = metrics.monitor() - - monitor.on(cpuEvent) - monitor.on(memEvent) - monitor.on(httpEvent) - - // TBH, I feel like this should be possible in a nicer way - // But route.convertToPathComponents() gives all kinds of errors :L - router.get(route.map { $0 }, use: self.getPrometheusData) - } - - func getPrometheusData(_ req: Request) throws -> Future { - let promise = req.eventLoop.newPromise(String.self) - vaporMonitoringPromClient.getMetrics { - promise.succeed(result: $0) - } - return promise.futureResult - } -} diff --git a/Sources/VaporMonitoring/VaporMonitoring.swift b/Sources/VaporMonitoring/VaporMonitoring.swift deleted file mode 100644 index a60eddf..0000000 --- a/Sources/VaporMonitoring/VaporMonitoring.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// VaporMonitoring.swift -// VaporMonitoring -// -// Created by Jari Koopman on 29/05/2018. -// - -import SwiftMetrics -import Vapor - -/// Provides configuration for VaporMonitoring -public struct MonitoringConfig { - /// At what route to host the Prometheus data - var prometheusRoute: [String] - - /// Only display response times for builtin routes - var onlyBuiltinRoutes: Bool - - public init(prometheusRoute: String..., onlyBuiltinRoutes: Bool) { - self.prometheusRoute = prometheusRoute - self.onlyBuiltinRoutes = onlyBuiltinRoutes - } - - public static func `default`() -> MonitoringConfig { - return .init(prometheusRoute: "metrics", onlyBuiltinRoutes: false) - } -} - -/// Vapor Monitoring class -/// Used to set up monitoring/metrics on your Vapor app -public final class VaporMonitoring { - /// Sets up config & services to monitor your Vapor app - public static func setupMonitoring(_ config: inout Config, _ services: inout Services, _ monitorConfig: MonitoringConfig = .default()) throws -> MonitoredRouter { - services.register(MonitoredResponder.self) - config.prefer(MonitoredResponder.self, for: Responder.self) - - let metrics = try SwiftMetrics() - services.register(metrics) - - let router = try MonitoredRouter(onlyBuiltinRoutes: monitorConfig.onlyBuiltinRoutes) - config.prefer(MonitoredRouter.self, for: Router.self) - - let prometheus = try VaporMetricsPrometheus(metrics: metrics, router: router, route: monitorConfig.prometheusRoute) - services.register(prometheus) - - return router - } -} - -/// Data collected from each request -public struct RequestData: SMData { - public let timestamp: Int - public let url: String - public let requestDuration: Double - public let statusCode: UInt - public let method: HTTPMethod -} - -/// Log of request -internal struct RequestLog { - var request: Request - var timestamp: Double - var route: String -} - -/// Log of requests -internal var requestsLog = [RequestLog]() - -/// Timestamp for refference -internal var timeIntervalSince1970MilliSeconds: Double { - return Date().timeIntervalSince1970 * 1000 -} - -internal var queue = DispatchQueue(label: "requestLogQueue") diff --git a/Sources/VaporMonitoring/VaporPrometheus.swift b/Sources/VaporMonitoring/VaporPrometheus.swift new file mode 100644 index 0000000..0f52ff0 --- /dev/null +++ b/Sources/VaporMonitoring/VaporPrometheus.swift @@ -0,0 +1,25 @@ +import Vapor +import Metrics +import Prometheus + +/// Class providing Prometheus data +/// +/// This class will automatically register its Prometheus client with `swift-metrics` for you. +public class VaporPrometheus: Service { + let prometheusClient = PrometheusClient() + + public init(router: Router, services: inout Services, route: String = "metrics") { + services.register(prometheusClient, as: PrometheusClient.self) + MetricsSystem.bootstrap(prometheusClient) + router.get(route, use: self.getPrometheusData) + } + + func getPrometheusData(_ req: Request) throws -> Future { + // The underlying API here should update to return a Future we can just return directly. + let promise = req.eventLoop.newPromise(String.self) + prometheusClient.collect(into: promise) + return promise.futureResult + } +} + +extension PrometheusClient: Service { } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..56dfbee --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,8 @@ +import XCTest + +import VaporMonitoringTests + +var tests = [XCTestCaseEntry]() +tests += VaporMonitoringTests.__allTests() + +XCTMain(tests) diff --git a/Tests/VaporMonitoringTests/VaporMonitoringTests.swift b/Tests/VaporMonitoringTests/VaporMonitoringTests.swift new file mode 100644 index 0000000..615d108 --- /dev/null +++ b/Tests/VaporMonitoringTests/VaporMonitoringTests.swift @@ -0,0 +1,81 @@ +import XCTest +@testable import VaporMonitoring + +import HTTP +import Metrics +import Prometheus +import Vapor + +final class VaporMonitoringTests: XCTestCase { + public struct TestError: Error { } + + let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + public func routes(_ router: Router) throws { + router.get("posts") { req in + return "Hello, world!" + } + } + + func makeRequest(_ url: String, middleware: Middleware, container: Container) throws { + let httpRequest = HTTPRequest(method: .GET, url: url, version: .init(major: 1, minor: 1), headers: HTTPHeaders(), body: HTTPBody()) + let request = Vapor.Request(http: httpRequest, using: container) + let responder = BasicResponder { request in + if !request.http.url.path.contains("9001") { + return self.elg.next().newSucceededFuture(result: Response(using: container)) + } else { + return self.elg.next().newFailedFuture(error: TestError()) + } + } + + // Trigger the request + let response = try middleware.respond(to: request, chainingTo: responder) + do { + _ = try response.wait() + } catch is TestError { /* expected for requests to ID 9001 */ } + } + + func testVaporMonitoring() throws { + // Setup Middleware + var services = Services.default() + let middleware = MetricsMiddleware() + services.register(MetricsMiddleware(), as: MetricsMiddleware.self) + var middlewares = MiddlewareConfig() + middlewares.use(MetricsMiddleware.self) + services.register(middlewares) + + // Create VaporPrometheus + let router = EngineRouter.default() + try routes(router) + let prometheusService = VaporPrometheus(router: router, services: &services) + services.register(prometheusService) + services.register(router, as: Router.self) + + // Prepare fake HTTP request + let testContainer = BasicContainer(config: Config(), environment: Environment.testing, services: Services.default(), on: elg) + + for _ in 1...5 { + try makeRequest("http://fake-blog.com", middleware: middleware, container: testContainer) + } + for postID in [1, 1, 1, 1, 2, 3, 4, 5, 9001, 9001] { + try makeRequest("http://fake-blog.com/posts/\(postID)", middleware: middleware, container: testContainer) + } + + // Verify we captured as expected + let prom = try MetricsSystem.prometheus() + let metricsPromise = elg.next().newPromise(of: String.self) + prom.collect(into: metricsPromise) + let metrics = try metricsPromise.futureResult.wait().split(separator: "\n").map { String($0) } + + let expectedResults = [ + #"http_requests_total{status_code="200", path="/", method="GET"} 5"#, + #"http_requests_total{status_code="200", path="posts", method="GET"} 8"#, + "http_requests_duration_seconds_count 15", + #"http_requests_duration_seconds_count{path="posts", method="GET"} 10"#, + #"http_request_errors_total{path="posts", method="GET"} 2"# + ] + for result in expectedResults { + XCTAssert(metrics.contains(result)) + } + } +} diff --git a/Tests/VaporMonitoringTests/XCTestManifests.swift b/Tests/VaporMonitoringTests/XCTestManifests.swift new file mode 100644 index 0000000..c55f72c --- /dev/null +++ b/Tests/VaporMonitoringTests/XCTestManifests.swift @@ -0,0 +1,18 @@ +#if !canImport(ObjectiveC) +import XCTest + +extension VaporMonitoringTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__VaporMonitoringTests = [ + ("testVaporMonitoring", testVaporMonitoring), + ] +} + +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(VaporMonitoringTests.__allTests__VaporMonitoringTests), + ] +} +#endif