diff --git a/.env b/.env index 21333af5dc..8b65cb2a26 100644 --- a/.env +++ b/.env @@ -20,4 +20,7 @@ FTP_PORT=21 FTP_USER=bob FTP_PASS=12345 RAVENDB_PORT=9030 -SOLR_PORT=8983 \ No newline at end of file +SOLR_PORT=8983 +CLICKHOUSE_USER=default +CLICKHOUSE_PASSWORD=Password12! +CLICKHOUSE_PORT=8123 diff --git a/.github/codecov.yml b/.github/codecov.yml index f1fb5bbdbf..50efcbdde1 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -31,6 +31,8 @@ flags: carryforward: true AzureStorage: carryforward: true + ClickHouse: + carryforward: true Consul: carryforward: true CosmosDb: diff --git a/.github/labeler.yml b/.github/labeler.yml index cba517c918..fd15465173 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -36,6 +36,9 @@ azure: - changed-files: - any-glob-to-any-file: [src/HealthChecks.Azure*/**/*] +clickhouse: + - src/HealthChecks.ClickHouse/**/* + consul: - changed-files: - any-glob-to-any-file: [src/HealthChecks.Consul/**/*] diff --git a/.github/workflows/healthchecks_clickhouse_cd.yml b/.github/workflows/healthchecks_clickhouse_cd.yml new file mode 100644 index 0000000000..defdbc1377 --- /dev/null +++ b/.github/workflows/healthchecks_clickhouse_cd.yml @@ -0,0 +1,16 @@ +name: HealthChecks ClickHouse DB CD + +on: + push: + tags: + - release-clickhouse-* + - release-all-* + +jobs: + build: + uses: ./.github/workflows/reusable_cd_workflow.yml + secrets: inherit + with: + BUILD_CONFIG: Release + PROJECT_PATH: ./src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.ClickHouse diff --git a/.github/workflows/healthchecks_clickhouse_cd_preview.yml b/.github/workflows/healthchecks_clickhouse_cd_preview.yml new file mode 100644 index 0000000000..c7c1f9eb9c --- /dev/null +++ b/.github/workflows/healthchecks_clickhouse_cd_preview.yml @@ -0,0 +1,17 @@ +name: HealthChecks ClickHouse DB Preview CD + +on: + push: + tags: + - preview-clickhouse-* + - 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.ClickHouse/HealthChecks.ClickHouse.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.ClickHouse diff --git a/.github/workflows/healthchecks_clickhouse_ci.yml b/.github/workflows/healthchecks_clickhouse_ci.yml new file mode 100644 index 0000000000..eb7368a3f6 --- /dev/null +++ b/.github/workflows/healthchecks_clickhouse_ci.yml @@ -0,0 +1,77 @@ +name: HealthChecks ClickHouse DB CI + +on: + workflow_dispatch: + push: + branches: [ master ] + paths: + - src/HealthChecks.ClickHouse/** + - test/HealthChecks.ClickHouse.Tests/** + - test/_SHARED/** + - .github/workflows/healthchecks_clickhouse_ci.yml + - Directory.Build.props + - Directory.Build.targets + - Directory.Packages.props + tags-ignore: + - release-* + - preview-* + + pull_request: + branches: [ master ] + paths: + - src/HealthChecks.ClickHouse/** + - test/HealthChecks.ClickHouse.Tests/** + - test/_SHARED/** + - .github/workflows/healthchecks_clickhouse_ci.yml + - Directory.Build.props + - Directory.Build.targets + - Directory.Packages.props + +jobs: + build: + runs-on: ubuntu-latest + services: + clickhouse: + image: clickhouse/clickhouse-server:24-alpine + ports: + - 8123:8123 + env: + CLICKHOUSE_DB: default + CLICKHOUSE_USER: default + CLICKHOUSE_PASSWORD: "Password12!" + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Restore + run: | + dotnet restore ./src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj && + dotnet restore ./test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.Tests.csproj + - name: Check formatting + run: | + dotnet format --no-restore --verify-no-changes --severity warn ./src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) && + dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) + - name: Build + run: | + dotnet build --no-restore ./src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj && + dotnet build --no-restore ./test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.Tests.csproj + - name: Test + run: > + dotnet test + ./test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.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: ClickHouse + directory: .coverage diff --git a/AspNetCore.Diagnostics.HealthChecks.sln b/AspNetCore.Diagnostics.HealthChecks.sln index 5cfda92fe4..64d8fec892 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("{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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -881,6 +885,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 + {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 + {96E2B0A3-02BD-456B-8888-4D96DABA99EB}.Release|Any CPU.Build.0 = Release|Any CPU + {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1026,6 +1038,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} + {96E2B0A3-02BD-456B-8888-4D96DABA99EB} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} + {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2B8C62A1-11B6-469F-874C-A02443256568} diff --git a/Directory.Packages.props b/Directory.Packages.props index 1cdc346bdd..af73e3b085 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/README.md b/README.md index 682850cec3..de28f98f82 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ HealthChecks packages include health checks for: | Azure Key Vault | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.AzureKeyVault)](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureKeyVault) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.AzureKeyVault)](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureKeyVault) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/azure)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/azure) | Azure Search | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.AzureSearch)](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureSearch) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.AzureSearch)](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureSearch) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/azure)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/azure) | Azure Service Bus | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.AzureServiceBus)](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureServiceBus) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.AzureServiceBus)](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureServiceBus) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/azure)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/azure) | Queue and Topics | +| ClickHouse | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.ClickHouse)](https://www.nuget.org/packages/AspNetCore.HealthChecks.ClickHouse) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.ClickHouse)](https://www.nuget.org/packages/AspNetCore.HealthChecks.ClickHouse) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/clickhouse)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/clickhouse) | Consul | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.Consul)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Consul) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.Consul)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Consul) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/consul)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/consul) | CosmosDb | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.CosmosDb)](https://www.nuget.org/packages/AspNetCore.HealthChecks.CosmosDb) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.CosmosDb)](https://www.nuget.org/packages/AspNetCore.HealthChecks.CosmosDb) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/cosmosdb)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/cosmosdb) | CosmosDb and Azure Table | Dapr | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.Dapr)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Dapr) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.Dapr)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Dapr) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/dapr)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/dapr) @@ -143,6 +144,7 @@ Install-Package AspNetCore.HealthChecks.AzureKeyVault Install-Package AspNetCore.HealthChecks.AzureSearch Install-Package AspNetCore.HealthChecks.AzureServiceBus Install-Package AspNetCore.HealthChecks.AzureStorage +Install-Package AspNetCore.HealthChecks.ClickHouse Install-Package AspNetCore.HealthChecks.Consul Install-Package AspNetCore.HealthChecks.CosmosDb Install-Package AspNetCore.HealthChecks.Dapr @@ -703,4 +705,4 @@ answering [questions](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthCh 2. Follow the code guidelines and conventions. 3. New features are not only code, tests and documentation are also mandatory. 4. PRs with [`Ups for grabs`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/Ups%20for%20grabs) -and [help wanted](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/help%20wanted) tags are good candidates to contribute. +and [help wanted](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/help%20wanted) tags are good candidates to contribute. diff --git a/build/versions.props b/build/versions.props index a6b9ea3acd..1acaa55c81 100644 --- a/build/versions.props +++ b/build/versions.props @@ -18,6 +18,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/docker-compose.yml b/docker-compose.yml index ee3a4c86ee..62ea24a5d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,15 @@ services: volumes: - ./build/docker-services/solrcore:/var/solr/data/solrcore - ./build/docker-services/solrcoredown:/var/solr/data/solrcoredown + clickhouse: + image: clickhouse/clickhouse-server:24-alpine + environment: + - CLICKHOUSE_DB=${CLICKHOUSE_USER} + - CLICKHOUSE_USER=${CLICKHOUSE_USER} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} + - CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT="1" + ports: + - ${CLICKHOUSE_PORT}:8123 postgres: image: postgres environment: @@ -165,7 +174,7 @@ services: image: postgres ports: - "8010:5432" - environment: + environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=Password12! nats: @@ -180,7 +189,7 @@ services: ports: - "8086:8086" environment: - DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_MODE: setup DOCKER_INFLUXDB_INIT_USERNAME: ci_user DOCKER_INFLUXDB_INIT_PASSWORD: password DOCKER_INFLUXDB_INIT_ORG: influxdata diff --git a/src/HealthChecks.ClickHouse/ClickHouseHealthCheck.cs b/src/HealthChecks.ClickHouse/ClickHouseHealthCheck.cs new file mode 100644 index 0000000000..5dba494c66 --- /dev/null +++ b/src/HealthChecks.ClickHouse/ClickHouseHealthCheck.cs @@ -0,0 +1,41 @@ +using ClickHouse.Client.ADO; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecks.ClickHouse; + +/// +/// A health check for ClickHouse databases. +/// +public class ClickHouseHealthCheck : IHealthCheck +{ + internal const string HEALTH_QUERY = "SELECT 1;"; + + private readonly ClickHouseConnection _connection; + private readonly string _command; + + public ClickHouseHealthCheck(ClickHouseConnection connection, string command) + { + _connection = connection; + _command = command ?? HEALTH_QUERY; + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + using var command = _connection.CreateCommand(); + command.CommandText = _command; + + await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, description: ex.Message, exception: ex); + } + } +} diff --git a/src/HealthChecks.ClickHouse/DependencyInjection/ClickHouseHealthCheckBuilderExtensions.cs b/src/HealthChecks.ClickHouse/DependencyInjection/ClickHouseHealthCheckBuilderExtensions.cs new file mode 100644 index 0000000000..8090c4e0e9 --- /dev/null +++ b/src/HealthChecks.ClickHouse/DependencyInjection/ClickHouseHealthCheckBuilderExtensions.cs @@ -0,0 +1,47 @@ +using ClickHouse.Client.ADO; +using HealthChecks.ClickHouse; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure . +/// +public static class ClickHouseHealthCheckBuilderExtensions +{ + private const string NAME = "ClickHouse"; + + /// + /// Add a health check for ClickHouse databases. + /// + /// The . + /// A factory to build the ClickHouse connection to use. + /// The query to be used in check. + /// The health check name. Optional. If null the type name 'ClickHouse' 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 AddClickHouse( + this IHealthChecksBuilder builder, + Func connectionFactory, + string healthQuery = ClickHouseHealthCheck.HEALTH_QUERY, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + Guard.ThrowIfNull(connectionFactory); + Guard.ThrowIfNull(healthQuery); + + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => new ClickHouseHealthCheck(connectionFactory(sp), healthQuery), + failureStatus, + tags, + timeout)); + } +} diff --git a/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj b/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj new file mode 100644 index 0000000000..124a2eb6e7 --- /dev/null +++ b/src/HealthChecks.ClickHouse/HealthChecks.ClickHouse.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.1;$(DefaultNetCoreApp) + $(PackageTags);Beat;ClickHouse + HealthChecks.ClickHouse is a health check for ClickHouse. + $(HealthCheckClickHouse) + + + + + + + diff --git a/src/HealthChecks.ClickHouse/README.md b/src/HealthChecks.ClickHouse/README.md new file mode 100644 index 0000000000..743c8fccfe --- /dev/null +++ b/src/HealthChecks.ClickHouse/README.md @@ -0,0 +1,18 @@ +## ClickHouse Health Check + +This health check verifies the ability to communicate with [ClickHouse](https://www.clickhouse.com/). It uses the [ClickHouse.Client](https://www.nuget.org/packages/ClickHouse.Client) library. + +## Recommended approach + +When registering the ClickHouse health check, it is [recommended](https://github.com/DarkWanderer/ClickHouse.Client/wiki/Connection-lifetime-&-pooling#recommendations) to use `IHttpClientFactory` or a static instance of `HttpClient` to manage connections. + +```csharp +void Configure(IServiceCollection services) +{ + services.AddHttpClient("ClickHouseClient"); + services.AddHealthChecks().AddClickHouse(static sp => { + var httpClientFactory = sp.GetRequiredService(); + return new ClickHouseConnection("Host=ch;Username=default;Password=test;Database=default", httpClientFactory, "ClickHouseClient"); + }); +} +``` diff --git a/test/HealthChecks.ClickHouse.Tests/DependencyInjection/RegistrationTests.cs b/test/HealthChecks.ClickHouse.Tests/DependencyInjection/RegistrationTests.cs new file mode 100644 index 0000000000..53eb515969 --- /dev/null +++ b/test/HealthChecks.ClickHouse.Tests/DependencyInjection/RegistrationTests.cs @@ -0,0 +1,91 @@ +using ClickHouse.Client.ADO; + +namespace HealthChecks.ClickHouse.Tests.DependencyInjection; + +public class clickhouse_registration_should +{ + [Fact] + public void add_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + services.AddHealthChecks() + .AddClickHouse(static _ => new ClickHouseConnection("Host=localhost")); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe("ClickHouse"); + check.ShouldBeOfType(); + } + + [Fact] + public void add_named_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + services.AddHealthChecks() + .AddClickHouse(static _ => new ClickHouseConnection("Host=localhost"), name: "my-ch-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-ch-1"); + check.ShouldBeOfType(); + } + + [Fact] + public void add_health_check_with_connection_string_factory_when_properly_configured() + { + var services = new ServiceCollection(); + var factoryCalled = false; + services.AddHealthChecks() + .AddClickHouse(_ => + { + factoryCalled = true; + return new ClickHouseConnection("Host=localhost"); + }, name: "my-ch-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-ch-1"); + check.ShouldBeOfType(); + factoryCalled.ShouldBeTrue(); + } + + [Fact] + public void factory_is_called_everytime_healthcheck_is_created() + { + ServiceCollection services = new(); + int factoryCalls = 0; + services.AddHealthChecks() + .AddClickHouse(_ => + { + Interlocked.Increment(ref factoryCalls); + return new ClickHouseConnection("Host=localhost"); + }, name: "my-ch-1"); + + using var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.Single(); + + for (int i = 0; i < 10; i++) + { + _ = registration.Factory(serviceProvider); + } + + // ClickHouseConnection is not thread safe, so we assume that that we get a new instance every time + factoryCalls.ShouldBe(10); + } +} diff --git a/test/HealthChecks.ClickHouse.Tests/Functional/ClickHouseHealthCheckTests.cs b/test/HealthChecks.ClickHouse.Tests/Functional/ClickHouseHealthCheckTests.cs new file mode 100644 index 0000000000..9d0a35420c --- /dev/null +++ b/test/HealthChecks.ClickHouse.Tests/Functional/ClickHouseHealthCheckTests.cs @@ -0,0 +1,183 @@ +using System.Net; +using ClickHouse.Client.ADO; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace HealthChecks.ClickHouse.Tests.Functional; + +public class DBConfigSetting +{ + public string ConnectionString { get; set; } = null!; +} + +public class ClickHouse_healthcheck_should +{ + private const string ConnectionString = "Host=127.0.0.1;Port=8123;Database=default;Username=default;Password=Password12!;"; + + [Fact] + public async Task be_healthy_if_ClickHouse_is_available() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(static services => + { + services.AddHealthChecks() + .AddClickHouse(static _ => new ClickHouseConnection(ConnectionString), tags: new string[] { "ClickHouse" }); + }) + .Configure(static app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = static r => r.Tags.Contains("ClickHouse") + }); + }); + + 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_sql_query_is_not_valid() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(static services => + { + services.AddHealthChecks() + .AddClickHouse(static _ => new ClickHouseConnection(ConnectionString), "SELECT 1 FROM InvalidDB", tags: new string[] { "ClickHouse" }); + }) + .Configure(static app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = static r => r.Tags.Contains("ClickHouse") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task be_unhealthy_if_ClickHouse_is_not_available() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddClickHouse(static _ => new ClickHouseConnection("Host=200.0.0.1;Port=8123;Database=default;Username=default;Password=Password12!;"), tags: new string[] { "ClickHouse" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("ClickHouse") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task be_healthy_if_ClickHouse_is_available_by_iServiceProvider_registered() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(new DBConfigSetting + { + ConnectionString = ConnectionString + }); + + services.AddHealthChecks() + .AddClickHouse(static sp => new ClickHouseConnection(sp.GetRequiredService().ConnectionString), tags: new string[] { "ClickHouse" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("ClickHouse") + }); + }); + + 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_ClickHouse_is_not_available_registered() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(new DBConfigSetting + { + ConnectionString = "Server=200.0.0.1;Port=8010;User ID=postgres;Password=Password12!;database=postgres" + }); + + services.AddHealthChecks() + .AddClickHouse(static sp => new ClickHouseConnection(sp.GetRequiredService().ConnectionString), tags: new string[] { "ClickHouse" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("ClickHouse") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task unhealthy_check_log_detailed_messages() + { + const string connectionString = "Server=127.0.0.1;Port=8010;User ID=postgres;Password=Password12!;database=postgres"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddLogging(b => + b.ClearProviders() + .Services.TryAddEnumerable(ServiceDescriptor.Singleton()) + ) + .AddHealthChecks() + .AddClickHouse(static _ => new ClickHouseConnection(connectionString), "SELECT 1 FROM InvalidDB", tags: new string[] { "ClickHouse" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + Predicate = r => r.Tags.Contains("ClickHouse") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + var testLoggerProvider = (TestLoggerProvider)server.Services.GetRequiredService(); + + testLoggerProvider.ShouldNotBeNull(); + var logger = testLoggerProvider.GetLogger("Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService"); + + logger.ShouldNotBeNull(); + logger?.EventLog[0].Item2.ShouldNotContain("with message '(null)'"); + } +} diff --git a/test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.Tests.csproj b/test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.Tests.csproj new file mode 100644 index 0000000000..1ba4988d75 --- /dev/null +++ b/test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.approved.txt b/test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.approved.txt new file mode 100644 index 0000000000..6863047df1 --- /dev/null +++ b/test/HealthChecks.ClickHouse.Tests/HealthChecks.ClickHouse.approved.txt @@ -0,0 +1,15 @@ +namespace HealthChecks.ClickHouse +{ + public class ClickHouseHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck + { + public ClickHouseHealthCheck(ClickHouse.Client.ADO.ClickHouseConnection connection, string command) { } + public System.Threading.Tasks.Task CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { } + } +} +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ClickHouseHealthCheckBuilderExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddClickHouse(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func connectionFactory, string healthQuery = "SELECT 1;", 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