diff --git a/.env b/.env index 21333af5dc..3157b789f2 100644 --- a/.env +++ b/.env @@ -12,6 +12,7 @@ MYSQL_PORT=3306 MYSQL_PASSWORD=Password12! ZOOKEEPER_PORT=3000 KAFKA_PORT=9092 +PULSAR_PORT=6650 RABBITMQ_PORT=5672 IDSVR_PORT=8888 ORACLE_PORT=1521 @@ -20,4 +21,4 @@ FTP_PORT=21 FTP_USER=bob FTP_PASS=12345 RAVENDB_PORT=9030 -SOLR_PORT=8983 \ No newline at end of file +SOLR_PORT=8983 diff --git a/.github/codecov.yml b/.github/codecov.yml index cac878e857..bc143991a2 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -87,6 +87,8 @@ flags: carryforward: true Publisher.Seq: carryforward: true + Pulsar: + carryforward: true RabbitMQ: carryforward: true RavenDb: diff --git a/.github/labeler.yml b/.github/labeler.yml index fdc5433489..58ae543ae0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -99,6 +99,9 @@ prometheus: - src/HealthChecks.Prometheus.Metrics/**/* - src/HealthChecks.Publisher.Prometheus/**/* +pulsar: + - src/HealthChecks.Pulsar/**/* + applicationinsights: - src/HealthChecks.Publisher.ApplicationInsights/**/* diff --git a/.github/workflows/healthchecks_pulsar_cd.yml b/.github/workflows/healthchecks_pulsar_cd.yml new file mode 100644 index 0000000000..096c44f262 --- /dev/null +++ b/.github/workflows/healthchecks_pulsar_cd.yml @@ -0,0 +1,16 @@ +name: HealthChecks Pulsar CD + +on: + push: + tags: + - release-pulsar-* + - release-all-* + +jobs: + build: + uses: ./.github/workflows/reusable_cd_workflow.yml + secrets: inherit + with: + BUILD_CONFIG: Release + PROJECT_PATH: ./src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.Pulsar diff --git a/.github/workflows/healthchecks_pulsar_cd_preview.yml b/.github/workflows/healthchecks_pulsar_cd_preview.yml new file mode 100644 index 0000000000..277705629e --- /dev/null +++ b/.github/workflows/healthchecks_pulsar_cd_preview.yml @@ -0,0 +1,17 @@ +name: HealthChecks Pulsar Preview CD + +on: + push: + tags: + - preview-pulsar-* + - 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.Pulsar/HealthChecks.Pulsar.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.Pulsar diff --git a/.github/workflows/healthchecks_pulsar_ci.yml b/.github/workflows/healthchecks_pulsar_ci.yml new file mode 100644 index 0000000000..7109a4611e --- /dev/null +++ b/.github/workflows/healthchecks_pulsar_ci.yml @@ -0,0 +1,75 @@ +name: HealthChecks Pulsar CI + +on: + workflow_dispatch: + push: + branches: [ master ] + paths: + - src/HealthChecks.Pulsar/** + - test/HealthChecks.Pulsar.Tests/** + - test/_SHARED/** + - .github/workflows/healthchecks_pulsar_ci.yml + - Directory.Build.props + - Directory.Build.targets + tags-ignore: + - release-* + - preview-* + + pull_request: + branches: [ master ] + paths: + - src/HealthChecks.Pulsar/** + - test/HealthChecks.Pulsar.Tests/** + - test/_SHARED/** + - .github/workflows/healthchecks_pulsar_ci.yml + - Directory.Build.props + - Directory.Build.targets + +jobs: + build: + runs-on: ubuntu-latest + services: + pulsar: + image: apachepulsar/pulsar:latest + env: + PULSAR_MEM: " -Xms1g -Xmx1g -XX:MaxDirectMemorySize=2g" + ports: + - 6650:6650 + options: >- + --entrypoint '/bin/bash -c "bin/apply-config-from-env.py conf/standalone.conf && bin/pulsar standalone --no-functions-worker"' + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Restore + run: | + dotnet restore ./src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj && + dotnet restore ./test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.Tests.csproj + - name: Check formatting + run: | + dotnet format --no-restore --verify-no-changes --severity warn ./src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) && + dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) + - name: Build + run: | + dotnet build --no-restore ./src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj && + dotnet build --no-restore ./test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.Tests.csproj + - name: Test + run: > + dotnet test + ./test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.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: Pulsar + directory: .coverage diff --git a/AspNetCore.Diagnostics.HealthChecks.sln b/AspNetCore.Diagnostics.HealthChecks.sln index 56d827accc..9af82db086 100644 --- a/AspNetCore.Diagnostics.HealthChecks.sln +++ b/AspNetCore.Diagnostics.HealthChecks.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -310,6 +310,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.Milvus", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.Milvus.Tests", "test\HealthChecks.Milvus.Tests\HealthChecks.Milvus.Tests.csproj", "{D49CF52C-9D21-4D98-8A15-A2B259E9C003}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.Pulsar", "src\HealthChecks.Pulsar\HealthChecks.Pulsar.csproj", "{15E844A6-9411-42DF-9862-EBC6A9147D2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.Pulsar.Tests", "test\HealthChecks.Pulsar.Tests\HealthChecks.Pulsar.Tests.csproj", "{72D9B21B-97A9-4F29-AEBE-A0FC15CC69B4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -868,6 +872,14 @@ Global {D49CF52C-9D21-4D98-8A15-A2B259E9C003}.Debug|Any CPU.Build.0 = Debug|Any CPU {D49CF52C-9D21-4D98-8A15-A2B259E9C003}.Release|Any CPU.ActiveCfg = Release|Any CPU {D49CF52C-9D21-4D98-8A15-A2B259E9C003}.Release|Any CPU.Build.0 = Release|Any CPU + {15E844A6-9411-42DF-9862-EBC6A9147D2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15E844A6-9411-42DF-9862-EBC6A9147D2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15E844A6-9411-42DF-9862-EBC6A9147D2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15E844A6-9411-42DF-9862-EBC6A9147D2C}.Release|Any CPU.Build.0 = Release|Any CPU + {72D9B21B-97A9-4F29-AEBE-A0FC15CC69B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72D9B21B-97A9-4F29-AEBE-A0FC15CC69B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72D9B21B-97A9-4F29-AEBE-A0FC15CC69B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72D9B21B-97A9-4F29-AEBE-A0FC15CC69B4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1011,6 +1023,8 @@ Global {3B812989-2C4E-4FCE-B3A0-EF9C00A9B3A5} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} {17913EAF-3B12-495B-80EA-9EB975FBE6BA} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} {D49CF52C-9D21-4D98-8A15-A2B259E9C003} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} + {15E844A6-9411-42DF-9862-EBC6A9147D2C} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} + {72D9B21B-97A9-4F29-AEBE-A0FC15CC69B4} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2B8C62A1-11B6-469F-874C-A02443256568} diff --git a/README.md b/README.md index 95d18c6eb3..a0e6b920cf 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ HealthChecks packages include health checks for: | Nats | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.Nats)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Nats) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.Nats)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Nats) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/nats)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/nats) | NATS, messaging, message-bus, pubsub | | Network | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.Network)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Network) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.Network)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Network) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/network)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/network) | Ftp, SFtp, Dns, Tcp port, Smtp, Imap, Ssl | | Postgres | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.NpgSql)](https://www.nuget.org/packages/AspNetCore.HealthChecks.NpgSql) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.NpgSql)](https://www.nuget.org/packages/AspNetCore.HealthChecks.NpgSql) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/npgsql)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/npgsql) +| Pulsar | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.Pulsar)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Pulsar) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.Pulsar)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Pulsar) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/Pulsar)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/pulsar) | Identity Server | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.OpenIdConnectServer)](https://www.nuget.org/packages/AspNetCore.HealthChecks.OpenIdConnectServer) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.OpenIdConnectServer)](https://www.nuget.org/packages/AspNetCore.HealthChecks.OpenIdConnectServer) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/openidconnect)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/openidconnect) | Oracle | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.Oracle)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Oracle) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.Oracle)](https://www.nuget.org/packages/AspNetCore.HealthChecks.Oracle) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/oracle)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/oracle) | RabbitMQ | [![Nuget](https://img.shields.io/nuget/dt/AspNetCore.HealthChecks.RabbitMQ)](https://www.nuget.org/packages/AspNetCore.HealthChecks.RabbitMQ) | [![Nuget](https://img.shields.io/nuget/v/AspNetCore.HealthChecks.RabbitMQ)](https://www.nuget.org/packages/AspNetCore.HealthChecks.RabbitMQ) | [![view](https://img.shields.io/github/issues/Xabaril/AspNetCore.Diagnostics.HealthChecks/rabbitmq)](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/labels/rabbitmq) @@ -161,6 +162,7 @@ Install-Package AspNetCore.HealthChecks.MySql Install-Package AspNetCore.HealthChecks.Nats Install-Package AspNetCore.HealthChecks.Network Install-Package AspNetCore.HealthChecks.Npgsql +Install-Package AspNetCore.HealthChecks.Pulsar Install-Package AspNetCore.HealthChecks.OpenIdConnectServer Install-Package AspNetCore.HealthChecks.Oracle Install-Package AspNetCore.HealthChecks.RabbitMQ @@ -699,4 +701,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 e6d83284d6..1eda4ed831 100644 --- a/build/versions.props +++ b/build/versions.props @@ -49,6 +49,7 @@ 8.0.1 8.0.1 8.0.1 + 8.0.1 8.0.2 8.0.1 8.0.1 diff --git a/docker-compose.yml b/docker-compose.yml index ee3a4c86ee..c5ebb28d4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,6 +68,14 @@ services: - ${KAFKA_PORT}:9092 links: - zookeeper + pulsar: + image: apachepulsar/pulsar:latest + environment: + PULSAR_MEM: " -Xms1g -Xmx1g -XX:MaxDirectMemorySize=2g" + ports: + - ${PULSAR_PORT}:6650 + command: | + /bin/bash -c "bin/apply-config-from-env.py conf/standalone.conf && bin/pulsar standalone --no-functions-worker" rabbitmq: image: rabbitmq ports: @@ -165,7 +173,7 @@ services: image: postgres ports: - "8010:5432" - environment: + environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=Password12! nats: @@ -180,7 +188,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.Pulsar/DependencyInjection/PulsarHealthCheckBuilderExtensions.cs b/src/HealthChecks.Pulsar/DependencyInjection/PulsarHealthCheckBuilderExtensions.cs new file mode 100644 index 0000000000..af9b0ece7d --- /dev/null +++ b/src/HealthChecks.Pulsar/DependencyInjection/PulsarHealthCheckBuilderExtensions.cs @@ -0,0 +1,147 @@ +using DotPulsar.Abstractions; +using HealthChecks.Pulsar; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure . +/// +public static class PulsarHealthCheckBuilderExtensions +{ + private const string NAME = "pulsar"; + internal const string DEFAULT_TOPIC = "non-persistent://public/default/healthchecks-topic"; + + private static readonly TimeSpan DEFAULT_TIMEOUT = TimeSpan.FromSeconds(15); + + /// + /// Add a health check for Pulsar cluster. + /// + /// The . + /// The topic name to produce Pulsar messages on. + /// The health check name. Optional. If null the type name 'kafka' 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 AddPulsar( + this IHealthChecksBuilder builder, + string topic = DEFAULT_TOPIC, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default + ) => AddPulsar( + builder, + sp => sp.GetRequiredService(), + topic: topic, + name: name, + failureStatus: failureStatus, + tags: tags, + timeout: timeout ?? DEFAULT_TIMEOUT); + + /// + /// Add a health check for Pulsar cluster. + /// + /// The . + /// The Pulsar client factory. + /// The topic name to produce Pulsar messages on. + /// The health check name. Optional. If null the type name 'kafka' 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 AddPulsar( + this IHealthChecksBuilder builder, + Func clientFactory, + string topic = DEFAULT_TOPIC, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default + ) + { + builder.Services.TryAddSingleton(sp => new PulsarHealthCheck(clientFactory(sp), new PulsarHealthCheckOptions + { + Topic = topic + })); + + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => sp.GetRequiredService(), + failureStatus, + tags, + timeout ?? DEFAULT_TIMEOUT)); + } + + /// + /// Add a health check for Pulsar cluster. + /// + /// The . + /// Options to configure Pulsar health check. + /// The health check name. Optional. If null the type name 'kafka' 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 AddPulsar( + this IHealthChecksBuilder builder, + PulsarHealthCheckOptions options, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default + ) => AddPulsar( + builder, + options, + sp => sp.GetRequiredService(), + name: name, + failureStatus: failureStatus, + tags: tags, + timeout: timeout ?? DEFAULT_TIMEOUT); + + /// + /// Add a health check for Pulsar cluster. + /// + /// The . + /// Options to configure Pulsar health check. + /// The Pulsar client factory. + /// The health check name. Optional. If null the type name 'kafka' 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 AddPulsar( + this IHealthChecksBuilder builder, + PulsarHealthCheckOptions options, + Func clientFactory, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default + ) + { + builder.Services.TryAddSingleton(sp => new PulsarHealthCheck(clientFactory(sp), options)); + + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => sp.GetRequiredService(), + failureStatus, + tags, + timeout ?? DEFAULT_TIMEOUT)); + } +} diff --git a/src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj b/src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj new file mode 100644 index 0000000000..600288b3fe --- /dev/null +++ b/src/HealthChecks.Pulsar/HealthChecks.Pulsar.csproj @@ -0,0 +1,15 @@ + + + + $(DefaultLibraryTargetFrameworks) + $(PackageTags);Pulsar + HealthChecks.Pulsar is the health check package for Pulsar. + $(HealthCheckPulsar) + + + + + + + + diff --git a/src/HealthChecks.Pulsar/PulsarHealthCheck.cs b/src/HealthChecks.Pulsar/PulsarHealthCheck.cs new file mode 100644 index 0000000000..dfdaa867e5 --- /dev/null +++ b/src/HealthChecks.Pulsar/PulsarHealthCheck.cs @@ -0,0 +1,66 @@ +using System.Text; +using DotPulsar.Abstractions; +using DotPulsar.Extensions; +using DotPulsar.Schemas; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecks.Pulsar; + +/// +/// A health check for Pulsar cluster. +/// +public class PulsarHealthCheck : IHealthCheck, IAsyncDisposable +{ + private readonly IPulsarClient client; + private readonly PulsarHealthCheckOptions options; + private IProducer? producer; + + public PulsarHealthCheck(IPulsarClient client, PulsarHealthCheckOptions? options) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.options = options ?? new PulsarHealthCheckOptions(); + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + if (producer != null && producer.IsFinalState()) + { + await producer.DisposeAsync().ConfigureAwait(false); + producer = null; + } + + if (producer == null) + { + var builder = client.NewProducer(new StringSchema(Encoding.UTF8)); + builder.Topic(options.Topic); + + options.Configure?.Invoke(builder); + producer ??= builder.Create(); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + if (context.Registration.Timeout != Timeout.InfiniteTimeSpan) + { + cts.CancelAfter(context.Registration.Timeout); + } + + var message = producer.NewMessage(); + await options.MessageSender(options, message, cts.Token).ConfigureAwait(false); + return HealthCheckResult.Healthy(); + } + catch (OperationCanceledException) + { + return new HealthCheckResult(context.Registration.FailureStatus); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } + + /// + public ValueTask DisposeAsync() => producer?.DisposeAsync() ?? new ValueTask(); +} diff --git a/src/HealthChecks.Pulsar/PulsarHealthCheckOptions.cs b/src/HealthChecks.Pulsar/PulsarHealthCheckOptions.cs new file mode 100644 index 0000000000..238b28380e --- /dev/null +++ b/src/HealthChecks.Pulsar/PulsarHealthCheckOptions.cs @@ -0,0 +1,33 @@ +using DotPulsar; +using DotPulsar.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecks.Pulsar; + +/// +/// Options for . +/// +public class PulsarHealthCheckOptions +{ + /// + /// The topic name to produce Pulsar messages on. + /// + public string Topic { get; set; } = PulsarHealthCheckBuilderExtensions.DEFAULT_TOPIC; + + /// + /// Optional delegate to configure Pulsar producer. + /// + public Action>? Configure { get; set; } + + /// + /// Delegate to build a message being send to Pulsar. + /// + public Func MessageBuilder { get; set; } = _ => $"Check Pulsar healthy on {DateTime.UtcNow}"; + + /// + /// Delegate to build a message being send to Pulsar. + /// + public Func, CancellationToken, ValueTask> MessageSender { get; set; } = (o, builder, ct) => builder + .Key("healthcheck-key") + .Send(o.MessageBuilder(o), ct); +} diff --git a/test/FunctionalTests/FunctionalTests.csproj b/test/FunctionalTests/FunctionalTests.csproj index 92e268445a..f8e18812ca 100644 --- a/test/FunctionalTests/FunctionalTests.csproj +++ b/test/FunctionalTests/FunctionalTests.csproj @@ -36,6 +36,7 @@ + diff --git a/test/HealthChecks.Pulsar.Tests/DependencyInjection/RegistrationTests.cs b/test/HealthChecks.Pulsar.Tests/DependencyInjection/RegistrationTests.cs new file mode 100644 index 0000000000..36b286a776 --- /dev/null +++ b/test/HealthChecks.Pulsar.Tests/DependencyInjection/RegistrationTests.cs @@ -0,0 +1,48 @@ +using System.Net; +using DotPulsar; + +namespace HealthChecks.Pulsar.Tests.DependencyInjection; + +public class pulsar_registration_should +{ + [Fact] + public async Task add_health_check_when_properly_configured() + { + await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri("pulsar://localhost:1234")) + .Build(); + + var services = new ServiceCollection(); + services.AddHealthChecks() + .AddPulsar(_ => client); + + await using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe("pulsar"); + check.ShouldBeOfType(); + } + [Fact] + public async Task add_named_health_check_when_properly_configured() + { + await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri("pulsar://localhost:1234")) + .Build(); + + var services = new ServiceCollection(); + services.AddHealthChecks() + .AddPulsar(_ => client, name: "my-pulsar-group"); + + await using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe("my-pulsar-group"); + check.ShouldBeOfType(); + } +} diff --git a/test/HealthChecks.Pulsar.Tests/Functional/PulsarHealthCheckTests.cs b/test/HealthChecks.Pulsar.Tests/Functional/PulsarHealthCheckTests.cs new file mode 100644 index 0000000000..44957fee96 --- /dev/null +++ b/test/HealthChecks.Pulsar.Tests/Functional/PulsarHealthCheckTests.cs @@ -0,0 +1,63 @@ +using System.Net; +using DotPulsar; + +namespace HealthChecks.Pulsar.Tests.Functional; + +public class pulsar_healthcheck_should +{ + [Fact] + public async Task be_unhealthy_if_pulsar_is_unavailable() + { + await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri("pulsar://localhost:1234")) + .Build(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddPulsar(_ => client, tags: new string[] { "pulsar" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("pulsar") + }); + }); + + 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_pulsar_is_available() + { + await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri("pulsar://localhost:6650")) + .Build(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddPulsar(_ => client, tags: new string[] { "pulsar" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("pulsar") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } +} diff --git a/test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.Tests.csproj b/test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.Tests.csproj new file mode 100644 index 0000000000..78c91a32b0 --- /dev/null +++ b/test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.approved.txt b/test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.approved.txt new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/test/HealthChecks.Pulsar.Tests/HealthChecks.Pulsar.approved.txt @@ -0,0 +1 @@ + \ No newline at end of file