diff --git a/Package.resolved b/Package.resolved
index 0289bc7..e705437 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -81,6 +81,24 @@
"version" : "1.20.0"
}
},
+ {
+ "identity" : "redis",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/redis.git",
+ "state" : {
+ "revision" : "2a8d3e4639b90b39b74309b54216bdfd9cb52b41",
+ "version" : "5.0.0-alpha.2.2"
+ }
+ },
+ {
+ "identity" : "redistack",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-server/RediStack.git",
+ "state" : {
+ "revision" : "622ce440f90d79b58e45f3a3efdd64c51d1dfd17",
+ "version" : "1.6.2"
+ }
+ },
{
"identity" : "routing-kit",
"kind" : "remoteSourceControl",
diff --git a/Package.swift b/Package.swift
index 060fad5..3433675 100644
--- a/Package.swift
+++ b/Package.swift
@@ -20,7 +20,9 @@ let package = Package(
// 🖋 Swift ORM (queries, models, and relations) for NoSQL and SQL databases.
.package(url: "https://github.com/vapor/fluent.git", from: "4.1.0"),
// 🐘 Swift ORM (queries, models, relations, etc) built on PostgreSQL.
- .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.1.1")
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.1.1"),
+ // Vapor provider for RedisKit + RedisNIO
+ .package(url: "https://github.com/vapor/redis.git", from: "5.0.0-alpha.2.2"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
@@ -30,7 +32,8 @@ let package = Package(
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
- .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver")
+ .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
+ .product(name: "Redis", package: "redis"),
]
),
.testTarget(
diff --git a/Sources/HealthChecks/Extensions/Application+Extensions.swift b/Sources/HealthChecks/Extensions/Application+Extensions.swift
index 02a3379..78ba26f 100644
--- a/Sources/HealthChecks/Extensions/Application+Extensions.swift
+++ b/Sources/HealthChecks/Extensions/Application+Extensions.swift
@@ -73,6 +73,18 @@ extension Application {
get { storage[LaunchTimeKey.self] ?? Date().timeIntervalSinceReferenceDate }
set { storage[LaunchTimeKey.self] = newValue }
}
+
+ /// A `redisIdKey` conform to StorageKey protocol
+ private struct RedisIdKey: StorageKey {
+ /// Less verbose typealias for `String`.
+ typealias Value = String
+ }
+
+ /// Setup `redisIdKey` in application storage
+ public var redisId: String? {
+ get { storage[RedisIdKey.self] }
+ set { storage[RedisIdKey.self] = newValue }
+ }
}
extension Application {
@@ -125,6 +137,18 @@ extension Application {
get { storage[ApplicationHealthChecksKey.self] }
set { storage[ApplicationHealthChecksKey.self] = newValue }
}
+
+ /// A `RedisHealthChecksKey` conform to StorageKey protocol
+ public struct RedisHealthChecksKey: StorageKey {
+ /// Less verbose typealias for `RedisHealthChecksProtocol`.
+ public typealias Value = RedisHealthChecksProtocol
+ }
+
+ /// Setup `redisHealthChecks` in application storage
+ public var redisHealthChecks: RedisHealthChecksProtocol? {
+ get { storage[RedisHealthChecksKey.self] }
+ set { storage[RedisHealthChecksKey.self] = newValue }
+ }
}
extension Application {
diff --git a/Sources/HealthChecks/RedisHealthChecks/RedisChecksProtocol.swift b/Sources/HealthChecks/RedisHealthChecks/RedisChecksProtocol.swift
new file mode 100644
index 0000000..6fd24d0
--- /dev/null
+++ b/Sources/HealthChecks/RedisHealthChecks/RedisChecksProtocol.swift
@@ -0,0 +1,40 @@
+// FS App Health Checks
+// Copyright (C) 2024 FREEDOM SPACE, LLC
+
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//
+// RedisChecksProtocol.swift
+//
+//
+// Created by Mykola Buhaiov on 21.02.2024.
+//
+
+import Vapor
+
+/// Groups func for get redis health check
+public protocol RedisChecksProtocol {
+ /// Get redis connection
+ /// - Returns: `HealthCheckItem`
+ func connection() async -> HealthCheckItem
+
+ /// Get response time from redis
+ /// - Returns: `HealthCheckItem`
+ func getResponseTime() async -> HealthCheckItem
+
+ /// Get pong from redis
+ /// - Returns: `String`
+ func pong() async -> String
+}
diff --git a/Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift b/Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift
new file mode 100644
index 0000000..9d67f2c
--- /dev/null
+++ b/Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift
@@ -0,0 +1,101 @@
+// FS App Health Checks
+// Copyright (C) 2024 FREEDOM SPACE, LLC
+
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//
+// RedisHealthChecks.swift
+//
+//
+// Created by Mykola Buhaiov on 21.02.2024.
+//
+
+import Vapor
+import Fluent
+import Redis
+
+/// Service that provides redis health check functionality
+public struct RedisHealthChecks: RedisHealthChecksProtocol {
+ /// Instance of app as `Application`
+ public let app: Application
+
+ /// Get redis connection
+ /// - Returns: `HealthCheckItem`
+ public func connection() async -> HealthCheckItem {
+ let dateNow = Date().timeIntervalSinceReferenceDate
+ let response = await pong()
+ let result = HealthCheckItem(
+ componentId: app.redisId,
+ componentType: .datastore,
+ observedValue: Date().timeIntervalSinceReferenceDate - dateNow,
+ observedUnit: "s",
+ status: response.lowercased().contains("pong") ? .pass : .fail,
+ time: app.dateTimeISOFormat.string(from: Date()),
+ output: !response.lowercased().contains("pong") ? response : nil,
+ links: nil,
+ node: nil
+ )
+ return result
+ }
+
+ /// Get response time from redis
+ /// - Returns: `HealthCheckItem`
+ public func getResponseTime() async -> HealthCheckItem {
+ let dateNow = Date().timeIntervalSinceReferenceDate
+ let response = await pong()
+ let result = HealthCheckItem(
+ componentId: app.redisId,
+ componentType: .datastore,
+ observedValue: Date().timeIntervalSinceReferenceDate - dateNow,
+ observedUnit: "s",
+ status: response.lowercased().contains("pong") ? .pass : .fail,
+ time: app.dateTimeISOFormat.string(from: Date()),
+ output: !response.lowercased().contains("pong") ? response : nil,
+ links: nil,
+ node: nil
+ )
+ return result
+ }
+
+ /// Get pong from redis
+ /// - Returns: `String`
+ public func pong() async -> String {
+ let result = try? await app.redis.ping().get()
+ var connectionDescription = "ERROR: No connect to Redis database"
+ if let result, result.lowercased().contains("pong") {
+ connectionDescription = result
+ }
+ return connectionDescription
+ }
+
+ /// Check with setup options
+ /// - Parameter options: array of `MeasurementType`
+ /// - Returns: dictionary `[String: HealthCheckItem]`
+ public func checkHealth(for options: [MeasurementType]) async -> [String: HealthCheckItem] {
+ var result = ["": HealthCheckItem()]
+ let measurementTypes = Array(Set(options))
+ for type in measurementTypes {
+ switch type {
+ case .responseTime:
+ result["\(ComponentName.redis):\(MeasurementType.responseTime)"] = await getResponseTime()
+ case .connections:
+ result["\(ComponentName.redis):\(MeasurementType.connections)"] = await connection()
+ default:
+ break
+ }
+ }
+ return result
+ }
+}
diff --git a/Sources/HealthChecks/RedisHealthChecks/RedisHealthChecksProtocol.swift b/Sources/HealthChecks/RedisHealthChecks/RedisHealthChecksProtocol.swift
new file mode 100644
index 0000000..3e7d0e1
--- /dev/null
+++ b/Sources/HealthChecks/RedisHealthChecks/RedisHealthChecksProtocol.swift
@@ -0,0 +1,28 @@
+// FS App Health Checks
+// Copyright (C) 2024 FREEDOM SPACE, LLC
+
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//
+// RedisHealthChecksProtocol.swift
+//
+//
+// Created by Mykola Buhaiov on 21.02.2024.
+//
+
+import Vapor
+
+/// Groups func for get redis health check
+public protocol RedisHealthChecksProtocol: RedisChecksProtocol, ChecksProtocol {}
diff --git a/Tests/HealthChecksTests/Mocks/RedisHealthChecksMock.swift b/Tests/HealthChecksTests/Mocks/RedisHealthChecksMock.swift
new file mode 100644
index 0000000..0c79e84
--- /dev/null
+++ b/Tests/HealthChecksTests/Mocks/RedisHealthChecksMock.swift
@@ -0,0 +1,62 @@
+// FS App Health Checks
+// Copyright (C) 2024 FREEDOM SPACE, LLC
+
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//
+// RedisHealthChecksMock.swift
+//
+//
+// Created by Mykola Buhaiov on 21.02.2024.
+//
+
+import Vapor
+@testable import HealthChecks
+
+public struct RedisHealthChecksMock: RedisHealthChecksProtocol {
+ static let redisId = "adca7c3d-55f4-4ab3-a842-18b35f50cb0f"
+ static let healthCheckItem = HealthCheckItem(
+ componentId: redisId,
+ componentType: .datastore,
+ observedValue: 1,
+ observedUnit: "s",
+ status: .pass,
+ affectedEndpoints: nil,
+ time: "2024-02-01T11:11:59.364",
+ output: "Ok",
+ links: nil,
+ node: nil
+ )
+
+ public func connection() async -> HealthCheckItem {
+ RedisHealthChecksMock.healthCheckItem
+ }
+
+ public func getResponseTime() async -> HealthCheckItem {
+ RedisHealthChecksMock.healthCheckItem
+ }
+
+ public func pong() async -> String {
+ "PONG"
+ }
+
+ public func checkHealth(for options: [MeasurementType]) async -> [String: HealthCheckItem] {
+ let result = [
+ "\(ComponentName.redis):\(MeasurementType.responseTime)": RedisHealthChecksMock.healthCheckItem,
+ "\(ComponentName.redis):\(MeasurementType.connections)": RedisHealthChecksMock.healthCheckItem
+ ]
+ return result
+ }
+}
diff --git a/Tests/HealthChecksTests/RedisHealthChecksTests.swift b/Tests/HealthChecksTests/RedisHealthChecksTests.swift
new file mode 100644
index 0000000..1c50b90
--- /dev/null
+++ b/Tests/HealthChecksTests/RedisHealthChecksTests.swift
@@ -0,0 +1,68 @@
+// FS App Health Checks
+// Copyright (C) 2024 FREEDOM SPACE, LLC
+
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//
+// RedisHealthChecksTests.swift
+//
+//
+// Created by Mykola Buhaiov on 21.02.2024.
+//
+
+import Vapor
+import XCTest
+@testable import HealthChecks
+
+final class RedisHealthChecksTests: XCTestCase {
+ func testConnection() async throws {
+ let app = Application(.testing)
+ defer { app.shutdown() }
+ app.redisId = UUID().uuidString
+ app.redisHealthChecks = RedisHealthChecksMock()
+ let result = await app.redisHealthChecks?.connection()
+ XCTAssertEqual(result, RedisHealthChecksMock.healthCheckItem)
+ }
+
+ func testGetResponseTime() async throws {
+ let app = Application(.testing)
+ defer { app.shutdown() }
+ let redisId = UUID().uuidString
+ app.redisId = redisId
+ app.redisHealthChecks = RedisHealthChecksMock()
+ let result = await app.redisHealthChecks?.getResponseTime()
+ XCTAssertEqual(result, RedisHealthChecksMock.healthCheckItem)
+ XCTAssertEqual(app.redisId, redisId)
+ }
+
+ func testCheckHealth() async throws {
+ let app = Application(.testing)
+ defer { app.shutdown() }
+ app.redisHealthChecks = RedisHealthChecksMock()
+ let result = await app.redisHealthChecks?.checkHealth(for: [MeasurementType.responseTime, MeasurementType.connections])
+ let redisConnections = result?["\(ComponentName.redis):\(MeasurementType.connections)"]
+ XCTAssertEqual(redisConnections, RedisHealthChecksMock.healthCheckItem)
+ let redisResponseTimes = result?["\(ComponentName.redis):\(MeasurementType.responseTime)"]
+ XCTAssertEqual(redisResponseTimes, RedisHealthChecksMock.healthCheckItem)
+ }
+
+ func testGetPong() async throws {
+ let app = Application(.testing)
+ defer { app.shutdown() }
+ app.redisHealthChecks = RedisHealthChecksMock()
+ let result = await app.redisHealthChecks?.pong()
+ XCTAssertEqual(result, "PONG")
+ }
+}