From 5c0d5701c5d3a5fbd7acf23dc0d410769da5cef2 Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Tue, 16 Jul 2019 22:59:38 -0700 Subject: [PATCH 1/7] Use the latest & greatest SwiftPrometheus, and utilize Middleware instead of Responder+Router. --- .gitignore | 3 +- Package.swift | 11 +- README.md | 51 ++++++--- Sources/MonitoringExample/main.swift | 13 ++- Sources/VaporMonitoring/Extensions.swift | 57 ---------- .../VaporMonitoring/MetricsMiddleware.swift | 48 +++++++++ .../Responder+Monitoring.swift | 60 ----------- .../VaporMonitoring/Router+Monitoring.swift | 64 ----------- .../VaporMetricsPrometheus.swift | 101 ------------------ Sources/VaporMonitoring/VaporMonitoring.swift | 74 ------------- Sources/VaporMonitoring/VaporPrometheus.swift | 29 +++++ 11 files changed, 127 insertions(+), 384 deletions(-) delete mode 100644 Sources/VaporMonitoring/Extensions.swift create mode 100644 Sources/VaporMonitoring/MetricsMiddleware.swift delete mode 100644 Sources/VaporMonitoring/Responder+Monitoring.swift delete mode 100644 Sources/VaporMonitoring/Router+Monitoring.swift delete mode 100644 Sources/VaporMonitoring/VaporMetricsPrometheus.swift delete mode 100644 Sources/VaporMonitoring/VaporMonitoring.swift create mode 100644 Sources/VaporMonitoring/VaporPrometheus.swift 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..30ed704 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,20 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.0 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/apple/swift-metrics.git", from: "1.0.0"), + .package(url: "https://github.com/Yasumoto/SwiftPrometheus.git", .branch("yasumoto-summary-seconds")), .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") ], targets: [ - .target(name: "VaporMonitoring", dependencies: ["Vapor", "SwiftMetrics", "SwiftPrometheus"]), + .target(name: "VaporMonitoring", dependencies: ["Metrics", "SwiftPrometheus", "Vapor"]), .target(name: "MonitoringExample", 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..9267e21 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, route: "metrics") + 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..ad1f951 --- /dev/null +++ b/Sources/VaporMonitoring/MetricsMiddleware.swift @@ -0,0 +1,48 @@ +// +// MetricsMiddleware.swift +// VaporMonitoring +// +// Created by Joe Smith on 07/15/2019. +// + +import Metrics +import Vapor + +/// Middleware to track in per-request metrics +/// +/// Based [off the RED Method](https://www.weave.works/blog/the-red-method-key-metrics-for-microservices-architecture/) +public final class MetricsMiddleware { + public let requestsCounterLabel = "http_requests_total" + public let requestsTimerLabel = "http_requests_duration_seconds" + // private let requestErrorsCounter = Metrics.Counter(label: "http_request_errors_total", dimensions: [(String, String)]()) NEED TO ADD ERRORS + + public init() { } +} + +extension MetricsMiddleware: Middleware { + public func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture { + let start = Date() + let response: Future + do { + response = try next.respond(to: request) + } catch { + response = request.eventLoop.newFailedFuture(error: error) + } + return response.map { response in + let dimensions = [ + ("method", request.http.method.string), + ("path", request.http.url.path), + ("status_code", "\(response.http.status.code)")] + Metrics.Counter(label: self.requestsCounterLabel, dimensions: dimensions).increment() + let duration = start.timeIntervalSinceNow * -1 + Metrics.Timer(label: self.requestsTimerLabel, dimensions: dimensions).record(duration) + return response + } // should we also handle the failed future too? + } +} + +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..1052e87 --- /dev/null +++ b/Sources/VaporMonitoring/VaporPrometheus.swift @@ -0,0 +1,29 @@ +// +// VaporPrometheus.swift +// VaporMonitoring +// +// Created by Joe Smith on 07/16/2019. +// + +import Vapor +import Metrics +import Prometheus + +/// Class providing Prometheus data +public class VaporPrometheus: Service { + let prometheusClient = PrometheusClient() + + let p_quantiles: [Double] = [0.5,0.9,0.99] + + public init(router: Router, route: String) { + Metrics.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) + promise.succeed(result: prometheusClient.collect()) + return promise.futureResult + } +} From 5bc203005e7dee5869989189f48537d70600f09f Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Tue, 27 Aug 2019 15:19:40 -0700 Subject: [PATCH 2/7] Use new NIO1 alpha --- Package.swift | 2 +- Sources/VaporMonitoring/VaporPrometheus.swift | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 30ed704..4ccd937 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"), - .package(url: "https://github.com/Yasumoto/SwiftPrometheus.git", .branch("yasumoto-summary-seconds")), + .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "0.0.0-alpha.1"), .package(url: "https://github.com/vapor/vapor.git", from: "3.1.0"), ], targets: [ diff --git a/Sources/VaporMonitoring/VaporPrometheus.swift b/Sources/VaporMonitoring/VaporPrometheus.swift index 1052e87..2154b16 100644 --- a/Sources/VaporMonitoring/VaporPrometheus.swift +++ b/Sources/VaporMonitoring/VaporPrometheus.swift @@ -12,10 +12,8 @@ import Prometheus /// Class providing Prometheus data public class VaporPrometheus: Service { let prometheusClient = PrometheusClient() - - let p_quantiles: [Double] = [0.5,0.9,0.99] - public init(router: Router, route: String) { + public init(router: Router, route: String = "metrics") { Metrics.MetricsSystem.bootstrap(prometheusClient) router.get(route, use: self.getPrometheusData) } From 7b8fc7880bcfde526b1901867e6dfc711357dd34 Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Tue, 27 Aug 2019 15:25:03 -0700 Subject: [PATCH 3/7] Register PrometheusClient --- Sources/MonitoringExample/main.swift | 2 +- Sources/VaporMonitoring/VaporPrometheus.swift | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/MonitoringExample/main.swift b/Sources/MonitoringExample/main.swift index 9267e21..cd1b16f 100644 --- a/Sources/MonitoringExample/main.swift +++ b/Sources/MonitoringExample/main.swift @@ -18,7 +18,7 @@ public func configure(_ config: inout Config, _ env: inout Environment, _ servic let router = EngineRouter.default() try routes(router) - let prometheusService = VaporPrometheus(router: router, route: "metrics") + let prometheusService = VaporPrometheus(router: router, services: &services) services.register(prometheusService) services.register(router, as: Router.self) } diff --git a/Sources/VaporMonitoring/VaporPrometheus.swift b/Sources/VaporMonitoring/VaporPrometheus.swift index 2154b16..5e8831e 100644 --- a/Sources/VaporMonitoring/VaporPrometheus.swift +++ b/Sources/VaporMonitoring/VaporPrometheus.swift @@ -12,8 +12,9 @@ import Prometheus /// Class providing Prometheus data public class VaporPrometheus: Service { let prometheusClient = PrometheusClient() - - public init(router: Router, route: String = "metrics") { + + public init(router: Router, services: inout Services, route: String = "metrics") { + services.register(prometheusClient, as: PrometheusClient.self) Metrics.MetricsSystem.bootstrap(prometheusClient) router.get(route, use: self.getPrometheusData) } @@ -25,3 +26,5 @@ public class VaporPrometheus: Service { return promise.futureResult } } + +extension PrometheusClient: Service { } From c1fca277a395e624d55b87111da8428180bf5856 Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Tue, 27 Aug 2019 15:36:31 -0700 Subject: [PATCH 4/7] Pin to 0.0.0 version so we don't get old 0.3.0 --- Package.swift | 2 +- Sources/VaporMonitoring/VaporPrometheus.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 4ccd937..1867bf0 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"), - .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "0.0.0-alpha.1"), + .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", .exact("0.0.0-alpha.1")), .package(url: "https://github.com/vapor/vapor.git", from: "3.1.0"), ], targets: [ diff --git a/Sources/VaporMonitoring/VaporPrometheus.swift b/Sources/VaporMonitoring/VaporPrometheus.swift index 5e8831e..bbfe01a 100644 --- a/Sources/VaporMonitoring/VaporPrometheus.swift +++ b/Sources/VaporMonitoring/VaporPrometheus.swift @@ -22,7 +22,7 @@ public class VaporPrometheus: Service { 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) - promise.succeed(result: prometheusClient.collect()) + prometheusClient.collect(into: promise) return promise.futureResult } } From a4725a5938c1ea0041838c3a6e8b3b9475f02360 Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Sat, 31 Aug 2019 10:18:26 -0700 Subject: [PATCH 5/7] Use 0.4.0 for SwiftPrometheus Details at https://github.com/MrLotU/SwiftPrometheus/issues/14 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 1867bf0..8592b51 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"), - .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", .exact("0.0.0-alpha.1")), + .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "0.4.0-alpha.1"), .package(url: "https://github.com/vapor/vapor.git", from: "3.1.0"), ], targets: [ From 5cb8c9f8464dd5d6db84c7985098eaeeecc8033b Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Sun, 13 Oct 2019 20:42:07 -0700 Subject: [PATCH 6/7] Only track the top-level path, correct duration, add a test --- Package.swift | 11 +-- .../VaporMonitoring/MetricsMiddleware.swift | 44 ++++++---- Tests/LinuxMain.swift | 8 ++ .../VaporMonitoringTests.swift | 82 +++++++++++++++++++ 4 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/VaporMonitoringTests/VaporMonitoringTests.swift diff --git a/Package.swift b/Package.swift index 8592b51..5a8a2a7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:4.2 import PackageDescription @@ -9,12 +9,13 @@ let package = Package( .executable(name: "MonitoringExample", targets: ["MonitoringExample"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"), - .package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "0.4.0-alpha.1"), - .package(url: "https://github.com/vapor/vapor.git", from: "3.1.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: ["Metrics", "SwiftPrometheus", "Vapor"]), - .target(name: "MonitoringExample", dependencies: ["VaporMonitoring"]) + .target(name: "MonitoringExample", dependencies: ["VaporMonitoring"]), + .testTarget(name: "VaporMonitoringTests", dependencies: ["VaporMonitoring"]) ] ) diff --git a/Sources/VaporMonitoring/MetricsMiddleware.swift b/Sources/VaporMonitoring/MetricsMiddleware.swift index ad1f951..0c6a6d2 100644 --- a/Sources/VaporMonitoring/MetricsMiddleware.swift +++ b/Sources/VaporMonitoring/MetricsMiddleware.swift @@ -12,32 +12,48 @@ import Vapor /// /// Based [off the RED Method](https://www.weave.works/blog/the-red-method-key-metrics-for-microservices-architecture/) public final class MetricsMiddleware { - public let requestsCounterLabel = "http_requests_total" - public let requestsTimerLabel = "http_requests_duration_seconds" - // private let requestErrorsCounter = Metrics.Counter(label: "http_request_errors_total", dimensions: [(String, String)]()) NEED TO ADD ERRORS + let requestsCounterLabel = "http_requests_total" + let requestsTimerLabel = "http_requests_duration_seconds" + let requestErrorsLabel = "http_request_errors_total" public init() { } } extension MetricsMiddleware: Middleware { public func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture { - let start = Date() + let start = Date().timeIntervalSince1970 let response: Future do { response = try next.respond(to: request) } catch { response = request.eventLoop.newFailedFuture(error: error) } - return response.map { response in - let dimensions = [ - ("method", request.http.method.string), - ("path", request.http.url.path), - ("status_code", "\(response.http.status.code)")] - Metrics.Counter(label: self.requestsCounterLabel, dimensions: dimensions).increment() - let duration = start.timeIntervalSinceNow * -1 - Metrics.Timer(label: self.requestsTimerLabel, dimensions: dimensions).record(duration) - return response - } // should we also handle the failed future too? + + _ = 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) } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..2bbc3e0 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,8 @@ +import XCTest + +import VaporMonitoringTests + +var tests = [XCTestCaseEntry]() +tests += VaporMonitoringsTests.__allTests() + +XCTMain(tests) diff --git a/Tests/VaporMonitoringTests/VaporMonitoringTests.swift b/Tests/VaporMonitoringTests/VaporMonitoringTests.swift new file mode 100644 index 0000000..b03ee5d --- /dev/null +++ b/Tests/VaporMonitoringTests/VaporMonitoringTests.swift @@ -0,0 +1,82 @@ +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 */ } + } + + 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)) + } + } +} From d8ee3bcdc54df36eb0e65b4e54d34aafa4574b98 Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Sun, 13 Oct 2019 21:16:26 -0700 Subject: [PATCH 7/7] Review feedback, more comments, and tidying headers --- .../VaporMonitoring/MetricsMiddleware.swift | 13 +++++-------- Sources/VaporMonitoring/VaporPrometheus.swift | 11 +++-------- Tests/LinuxMain.swift | 2 +- .../VaporMonitoringTests.swift | 3 +-- .../VaporMonitoringTests/XCTestManifests.swift | 18 ++++++++++++++++++ 5 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 Tests/VaporMonitoringTests/XCTestManifests.swift diff --git a/Sources/VaporMonitoring/MetricsMiddleware.swift b/Sources/VaporMonitoring/MetricsMiddleware.swift index 0c6a6d2..5709092 100644 --- a/Sources/VaporMonitoring/MetricsMiddleware.swift +++ b/Sources/VaporMonitoring/MetricsMiddleware.swift @@ -1,16 +1,11 @@ -// -// MetricsMiddleware.swift -// VaporMonitoring -// -// Created by Joe Smith on 07/15/2019. -// - import Metrics import Vapor /// Middleware to track in per-request metrics /// -/// Based [off the RED Method](https://www.weave.works/blog/the-red-method-key-metrics-for-microservices-architecture/) +/// 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" @@ -19,6 +14,8 @@ public final class MetricsMiddleware { 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 diff --git a/Sources/VaporMonitoring/VaporPrometheus.swift b/Sources/VaporMonitoring/VaporPrometheus.swift index bbfe01a..0f52ff0 100644 --- a/Sources/VaporMonitoring/VaporPrometheus.swift +++ b/Sources/VaporMonitoring/VaporPrometheus.swift @@ -1,21 +1,16 @@ -// -// VaporPrometheus.swift -// VaporMonitoring -// -// Created by Joe Smith on 07/16/2019. -// - 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) - Metrics.MetricsSystem.bootstrap(prometheusClient) + MetricsSystem.bootstrap(prometheusClient) router.get(route, use: self.getPrometheusData) } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 2bbc3e0..56dfbee 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -3,6 +3,6 @@ import XCTest import VaporMonitoringTests var tests = [XCTestCaseEntry]() -tests += VaporMonitoringsTests.__allTests() +tests += VaporMonitoringTests.__allTests() XCTMain(tests) diff --git a/Tests/VaporMonitoringTests/VaporMonitoringTests.swift b/Tests/VaporMonitoringTests/VaporMonitoringTests.swift index b03ee5d..615d108 100644 --- a/Tests/VaporMonitoringTests/VaporMonitoringTests.swift +++ b/Tests/VaporMonitoringTests/VaporMonitoringTests.swift @@ -32,7 +32,7 @@ final class VaporMonitoringTests: XCTestCase { let response = try middleware.respond(to: request, chainingTo: responder) do { _ = try response.wait() - } catch is TestError { /* expected */ } + } catch is TestError { /* expected for requests to ID 9001 */ } } func testVaporMonitoring() throws { @@ -57,7 +57,6 @@ final class VaporMonitoringTests: XCTestCase { 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) } 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