Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add redis health check #13

Merged
merged 3 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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] }

Check warning on line 85 in Sources/HealthChecks/Extensions/Application+Extensions.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HealthChecks/Extensions/Application+Extensions.swift#L85

Added line #L85 was not covered by tests
set { storage[RedisIdKey.self] = newValue }
}
}

extension Application {
Expand Down Expand Up @@ -125,6 +137,18 @@
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 getPong() 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 getPong()
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
}

Check warning on line 51 in Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift#L36-L51

Added lines #L36 - L51 were not covered by tests

/// Get response time from redis
/// - Returns: `HealthCheckItem`
public func getResponseTime() async -> HealthCheckItem {
let dateNow = Date().timeIntervalSinceReferenceDate
let response = await getPong()
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
}

Check warning on line 70 in Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift#L55-L70

Added lines #L55 - L70 were not covered by tests

/// Get pong from redis
/// - Returns: `String`
public func getPong() async -> String {
gulivero1773 marked this conversation as resolved.
Show resolved Hide resolved
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 warning on line 81 in Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift#L74-L81

Added lines #L74 - L81 were not covered by tests

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

Check warning on line 100 in Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift

View check run for this annotation

Codecov / codecov/patch

Sources/HealthChecks/RedisHealthChecks/RedisHealthChecks.swift#L86-L100

Added lines #L86 - L100 were not covered by tests
}
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 getPong() async -> String {
gulivero1773 marked this conversation as resolved.
Show resolved Hide resolved
"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
}
}
65 changes: 65 additions & 0 deletions Tests/HealthChecksTests/RedisHealthChecksTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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() }
app.redisHealthChecks = RedisHealthChecksMock()
let result = await app.redisHealthChecks?.getResponseTime()
XCTAssertEqual(result, RedisHealthChecksMock.healthCheckItem)
}

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?.getPong()
XCTAssertEqual(result, "PONG")
}
}
Loading