Skip to content

Commit

Permalink
feat: add redis health check (#13)
Browse files Browse the repository at this point in the history
* feat: add redis health check

* style: rename function

* test: refactor test
  • Loading branch information
gulivero1773 authored Feb 22, 2024
1 parent ea74280 commit 377bcf0
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 2 deletions.
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand Down
24 changes: 24 additions & 0 deletions Sources/HealthChecks/Extensions/Application+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions Sources/HealthChecks/RedisHealthChecks/RedisChecksProtocol.swift
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

//
// 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
}
101 changes: 101 additions & 0 deletions Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

//
// 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
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

//
// RedisHealthChecksProtocol.swift
//
//
// Created by Mykola Buhaiov on 21.02.2024.
//

import Vapor

/// Groups func for get redis health check
public protocol RedisHealthChecksProtocol: RedisChecksProtocol, ChecksProtocol {}
62 changes: 62 additions & 0 deletions Tests/HealthChecksTests/Mocks/RedisHealthChecksMock.swift
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

//
// 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
}
}
68 changes: 68 additions & 0 deletions Tests/HealthChecksTests/RedisHealthChecksTests.swift
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

//
// 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")
}
}

0 comments on commit 377bcf0

Please sign in to comment.