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 | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureKeyVault) | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureKeyVault) | [](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/azure)
| Azure Search | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureSearch) | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureSearch) | [](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/azure)
| Azure Service Bus | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureServiceBus) | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.AzureServiceBus) | [](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/azure) | Queue and Topics |
+| ClickHouse | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.ClickHouse) | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.ClickHouse) | [](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/clickhouse)
| Consul | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.Consul) | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.Consul) | [](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/consul)
| CosmosDb | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.CosmosDb) | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.CosmosDb) | [](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/cosmosdb) | CosmosDb and Azure Table
| Dapr | [](https://www.nuget.org/packages/AspNetCore.HealthChecks.Dapr) | [](https://www.nuget.org/packages/AspNetCore.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