diff --git a/.github/codecov.yml b/.github/codecov.yml index 50efcbdde1..961484dd88 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -107,6 +107,8 @@ flags: carryforward: true SqlServer: carryforward: true + SurrealDb: + carryforward: true System: carryforward: true UI: diff --git a/.github/labeler.yml b/.github/labeler.yml index fd15465173..40bb7f54f9 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -183,6 +183,9 @@ sqlserver: - changed-files: - any-glob-to-any-file: [src/HealthChecks.SqlServer/**/*] +surrealdb: + - src/HealthChecks.SurrealDb/**/* + system: - changed-files: - any-glob-to-any-file: [src/HealthChecks.System/**/*] diff --git a/.github/workflows/healthchecks_surrealdb_cd.yml b/.github/workflows/healthchecks_surrealdb_cd.yml new file mode 100644 index 0000000000..85f5bf3fa8 --- /dev/null +++ b/.github/workflows/healthchecks_surrealdb_cd.yml @@ -0,0 +1,16 @@ +name: HealthChecks SurrealDB CD + +on: + push: + tags: + - release-surrealdb-* + - release-all-* + +jobs: + build: + uses: ./.github/workflows/reusable_cd_workflow.yml + secrets: inherit + with: + BUILD_CONFIG: Release + PROJECT_PATH: ./src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.SurrealDb diff --git a/.github/workflows/healthchecks_surrealdb_cd_preview.yml b/.github/workflows/healthchecks_surrealdb_cd_preview.yml new file mode 100644 index 0000000000..d38e0f111a --- /dev/null +++ b/.github/workflows/healthchecks_surrealdb_cd_preview.yml @@ -0,0 +1,17 @@ +name: HealthChecks SurrealDB Preview CD + +on: + push: + tags: + - preview-surrealdb-* + - preview-all-* + +jobs: + build: + uses: ./.github/workflows/reusable_cd_preview_workflow.yml + secrets: inherit + with: + BUILD_CONFIG: Release + VERSION_SUFFIX_PREFIX: rc1 + PROJECT_PATH: ./src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.SurrealDb diff --git a/.github/workflows/healthchecks_surrealdb_ci.yml b/.github/workflows/healthchecks_surrealdb_ci.yml new file mode 100644 index 0000000000..d6ae7feec8 --- /dev/null +++ b/.github/workflows/healthchecks_surrealdb_ci.yml @@ -0,0 +1,76 @@ +name: HealthChecks SurrealDB CI + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - src/HealthChecks.SurrealDb/** + - test/HealthChecks.SurrealDb.Tests/** + - test/_SHARED/** + - .github/workflows/healthchecks_surrealdb_ci.yml + - Directory.Build.props + - Directory.Build.targets + - Directory.Packages.props + tags-ignore: + - release-* + - preview-* + + pull_request: + branches: [master] + paths: + - src/HealthChecks.SurrealDb/** + - test/HealthChecks.SurrealDb.Tests/** + - test/_SHARED/** + - .github/workflows/healthchecks_surrealdb_ci.yml + - Directory.Build.props + - Directory.Build.targets + - Directory.Packages.props + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Start SurrealDB + uses: surrealdb/setup-surreal@v2 + with: + surrealdb_version: latest + surrealdb_port: 8000 + surrealdb_username: root + surrealdb_password: root + surrealdb_auth: true + surrealdb_strict: + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Restore + run: | + dotnet restore ./src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj && + dotnet restore ./test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj + - name: Check formatting + run: | + dotnet format --no-restore --verify-no-changes --severity warn ./src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) && + dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) + - name: Build + run: | + dotnet build --no-restore ./src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj && + dotnet build --no-restore ./test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj + - name: Test + run: > + dotnet test + ./test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj + --no-restore + --no-build + --collect "XPlat Code Coverage" + --results-directory .coverage + -- + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + - name: Upload Coverage + uses: codecov/codecov-action@v3 + with: + flags: SurrealDb + directory: .coverage diff --git a/AspNetCore.Diagnostics.HealthChecks.sln b/AspNetCore.Diagnostics.HealthChecks.sln index 64d8fec892..6249bfaf2a 100644 --- a/AspNetCore.Diagnostics.HealthChecks.sln +++ b/AspNetCore.Diagnostics.HealthChecks.sln @@ -315,6 +315,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.Rabbitmq.v6", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.RabbitMQ.v6.Tests", "test\HealthChecks.RabbitMQ.v6.Tests\HealthChecks.RabbitMQ.v6.Tests.csproj", "{2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.SurrealDb", "src\HealthChecks.SurrealDb\HealthChecks.SurrealDb.csproj", "{97DAA09F-E0FA-4D5B-A72B-896161E570DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.SurrealDb.Tests", "test\HealthChecks.SurrealDb.Tests\HealthChecks.SurrealDb.Tests.csproj", "{44BB97EE-88DB-4C9B-8195-2C6D889AE391}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.ClickHouse", "src\HealthChecks.ClickHouse\HealthChecks.ClickHouse.csproj", "{96E2B0A3-02BD-456B-8888-4D96DABA99EB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.ClickHouse.Tests", "test\HealthChecks.ClickHouse.Tests\HealthChecks.ClickHouse.Tests.csproj", "{2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}" @@ -885,6 +889,14 @@ Global {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Debug|Any CPU.Build.0 = Debug|Any CPU {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Release|Any CPU.ActiveCfg = Release|Any CPU {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Release|Any CPU.Build.0 = Release|Any CPU + {97DAA09F-E0FA-4D5B-A72B-896161E570DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97DAA09F-E0FA-4D5B-A72B-896161E570DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97DAA09F-E0FA-4D5B-A72B-896161E570DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97DAA09F-E0FA-4D5B-A72B-896161E570DA}.Release|Any CPU.Build.0 = Release|Any CPU + {44BB97EE-88DB-4C9B-8195-2C6D889AE391}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44BB97EE-88DB-4C9B-8195-2C6D889AE391}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44BB97EE-88DB-4C9B-8195-2C6D889AE391}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44BB97EE-88DB-4C9B-8195-2C6D889AE391}.Release|Any CPU.Build.0 = Release|Any CPU {96E2B0A3-02BD-456B-8888-4D96DABA99EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {96E2B0A3-02BD-456B-8888-4D96DABA99EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {96E2B0A3-02BD-456B-8888-4D96DABA99EB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1038,6 +1050,8 @@ Global {D49CF52C-9D21-4D98-8A15-A2B259E9C003} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} {C76D7349-A3D2-7277-93C6-EE92E8E447A5} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} + {97DAA09F-E0FA-4D5B-A72B-896161E570DA} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} + {44BB97EE-88DB-4C9B-8195-2C6D889AE391} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} {96E2B0A3-02BD-456B-8888-4D96DABA99EB} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} EndGlobalSection diff --git a/Directory.Packages.props b/Directory.Packages.props index af73e3b085..14504ae867 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -95,6 +95,7 @@ + diff --git a/build/versions.props b/build/versions.props index 1acaa55c81..3f14c77254 100644 --- a/build/versions.props +++ b/build/versions.props @@ -58,6 +58,7 @@ 9.0.0 9.0.0 9.0.0 + 9.0.0 9.0.0 9.0.0 9.0.0 diff --git a/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj b/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj index 124a2eb6e7..7036eb3f29 100644 --- a/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj +++ b/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj @@ -5,6 +5,7 @@ $(PackageTags);Beat;ClickHouse HealthChecks.ClickHouse is a health check for ClickHouse. $(HealthCheckClickHouse) + false diff --git a/src/HealthChecks.SurrealDb/DependencyInjection/SurrealDbHealthCheckBuilderExtensions.cs b/src/HealthChecks.SurrealDb/DependencyInjection/SurrealDbHealthCheckBuilderExtensions.cs new file mode 100644 index 0000000000..12107ecb1d --- /dev/null +++ b/src/HealthChecks.SurrealDb/DependencyInjection/SurrealDbHealthCheckBuilderExtensions.cs @@ -0,0 +1,53 @@ +using HealthChecks.SurrealDb; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using SurrealDb.Net; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure . +/// +public static class SurrealDbHealthCheckBuilderExtensions +{ + private const string NAME = "surrealdb"; + + /// + /// Add a health check for SurrealDB. + /// + /// The . + /// + /// An optional factory to obtain instance. + /// When not provided, is simply resolved from . + /// + /// The health check name. Optional. If null the type name 'surrealdb' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddSurreal( + this IHealthChecksBuilder builder, + Func? factory = null, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => Factory(sp, factory), + failureStatus, + tags, + timeout)); + + static SurrealDbHealthCheck Factory(IServiceProvider sp, Func? factory) + { + // The user might have registered a factory for SurrealDbClient type, but not for the abstraction (ISurrealDbClient). + // That is why we try to resolve ISurrealDbClient first. + ISurrealDbClient client = factory?.Invoke(sp) ?? sp.GetService() ?? sp.GetRequiredService(); + return new(client); + } + } +} diff --git a/src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj b/src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj new file mode 100644 index 0000000000..54b83b1c23 --- /dev/null +++ b/src/HealthChecks.SurrealDb/HealthChecks.SurrealDb.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.1;$(DefaultNetCoreApp) + $(PackageTags);SurrealDb + HealthChecks.SurrealDb is the health check package for SurrealDB. + $(HealthCheckSurrealDb) + false + + + + + + + diff --git a/src/HealthChecks.SurrealDb/README.md b/src/HealthChecks.SurrealDb/README.md new file mode 100644 index 0000000000..dc747278f9 --- /dev/null +++ b/src/HealthChecks.SurrealDb/README.md @@ -0,0 +1,15 @@ +## SurrealDB Health Check + +This health check verifies the ability to communicate with [SurrealDb](https://surrealdb.com). It uses the provided [ISurrealDbClient](https://surrealdb.com/docs/sdk/dotnet). + +### Defaults + +By default, the `ISurrealDbClient` instance is resolved from service provider. + +```csharp +void Configure(IHealthChecksBuilder builder) +{ + builder.Services.AddSurreal("Server=http://localhost:8000;Namespace=test;Database=test"); + builder.AddHealthChecks().AddSurreal(); +} +``` diff --git a/src/HealthChecks.SurrealDb/SurrealDbHealthCheck.cs b/src/HealthChecks.SurrealDb/SurrealDbHealthCheck.cs new file mode 100644 index 0000000000..c4248d8a93 --- /dev/null +++ b/src/HealthChecks.SurrealDb/SurrealDbHealthCheck.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using SurrealDb.Net; + +namespace HealthChecks.SurrealDb; + +/// +/// A health check for SurrealDb services. +/// +public class SurrealDbHealthCheck : IHealthCheck +{ + private readonly ISurrealDbClient _client; + + public SurrealDbHealthCheck(ISurrealDbClient client) + { + _client = Guard.ThrowIfNull(client); + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + await _client.Connect(cancellationToken).ConfigureAwait(false); + + return await _client.Health(cancellationToken).ConfigureAwait(false) + ? HealthCheckResult.Healthy() + : HealthCheckResult.Unhealthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } +} diff --git a/test/HealthChecks.SurrealDb.Tests/DependencyInjection/RegistrationTests.cs b/test/HealthChecks.SurrealDb.Tests/DependencyInjection/RegistrationTests.cs new file mode 100644 index 0000000000..290072f18e --- /dev/null +++ b/test/HealthChecks.SurrealDb.Tests/DependencyInjection/RegistrationTests.cs @@ -0,0 +1,70 @@ +using SurrealDb.Net; + +namespace HealthChecks.SurrealDb.Tests.DependencyInjection; + +public class surrealdb_registration_should +{ + private const string ConnectionString = "Server=http://localhost:8000;Namespace=test;Database=test;Username=root;Password=root"; + + [Fact] + public void add_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + services.AddSurreal(ConnectionString); + services + .AddHealthChecks() + .AddSurreal(); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe("surrealdb"); + check.ShouldBeOfType(); + } + + [Fact] + public void add_named_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + services.AddSurreal(ConnectionString); + services + .AddHealthChecks() + .AddSurreal(name: "my-surrealdb-1"); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe("my-surrealdb-1"); + check.ShouldBeOfType(); + } + + [Fact] + public void add_health_check_with_connection_string_factory_when_properly_configured() + { + var services = new ServiceCollection(); + services.AddSurreal(ConnectionString); + bool factoryCalled = false; + services.AddHealthChecks() + .AddSurreal(sp => + { + factoryCalled = true; + return sp.GetRequiredService(); + }); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe("surrealdb"); + check.ShouldBeOfType(); + factoryCalled.ShouldBeTrue(); + } +} diff --git a/test/HealthChecks.SurrealDb.Tests/Functional/SurrealDbHealthCheckTests.cs b/test/HealthChecks.SurrealDb.Tests/Functional/SurrealDbHealthCheckTests.cs new file mode 100644 index 0000000000..cdf6c21a76 --- /dev/null +++ b/test/HealthChecks.SurrealDb.Tests/Functional/SurrealDbHealthCheckTests.cs @@ -0,0 +1,64 @@ +using System.Net; + +namespace HealthChecks.SurrealDb.Tests.Functional; + +public class surrealdb_healthcheck_should +{ + [Fact] + public async Task be_healthy_if_surrealdb_is_available() + { + const string connectionString = "Server=http://localhost:8000;Namespace=test;Database=test;Username=root;Password=root"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddSurreal(connectionString); + services + .AddHealthChecks() + .AddSurreal(tags: ["surrealdb"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("surrealdb") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_unhealthy_if_surrealdb_is_not_available() + { + const string connectionString = "Server=http://localhost:1234;Namespace=test;Database=test;Username=root;Password=root"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddSurreal(connectionString); + services + .AddHealthChecks() + .AddSurreal(tags: ["surrealdb"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("surrealdb") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } +} diff --git a/test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj b/test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj new file mode 100644 index 0000000000..1d0531a26b --- /dev/null +++ b/test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.approved.txt b/test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.approved.txt new file mode 100644 index 0000000000..8fb3ecf195 --- /dev/null +++ b/test/HealthChecks.SurrealDb.Tests/HealthChecks.SurrealDb.approved.txt @@ -0,0 +1,15 @@ +namespace HealthChecks.SurrealDb +{ + public class SurrealDbHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck + { + public SurrealDbHealthCheck(SurrealDb.Net.ISurrealDbClient client) { } + public System.Threading.Tasks.Task CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { } + } +} +namespace Microsoft.Extensions.DependencyInjection +{ + public static class SurrealDbHealthCheckBuilderExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddSurreal(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func? factory = null, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } + } +} \ No newline at end of file