diff --git a/src/HealthChecks.Azure.Messaging.EventGrid/AzureEventGridHealthCheck.cs b/src/HealthChecks.Azure.Messaging.EventGrid/AzureEventGridHealthCheck.cs new file mode 100644 index 0000000000..00d16bc502 --- /dev/null +++ b/src/HealthChecks.Azure.Messaging.EventGrid/AzureEventGridHealthCheck.cs @@ -0,0 +1,33 @@ +namespace HealthChecks.Azure.Messaging.EventGrid; + +public sealed class AzureEventGridHealthCheck : IHealthCheck +{ + private readonly EventGridPublisherClient _client; + + public AzureEventGridHealthCheck(EventGridPublisherClient client) + { + _client = Guard.ThrowIfNull(client); + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + // Send a ping event to verify connectivity + var healthCheckEvent = new EventGridEvent( + "HealthCheck", + "HealthCheck.Ping", + "1.0", + new { Timestamp = DateTimeOffset.UtcNow }); + + await _client.SendEventAsync(healthCheckEvent, cancellationToken).ConfigureAwait(false); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } +} \ No newline at end of file diff --git a/src/HealthChecks.Azure.Messaging.EventGrid/DependencyInjection/AzureEventGridHealthChecksBuilderExtensions.cs b/src/HealthChecks.Azure.Messaging.EventGrid/DependencyInjection/AzureEventGridHealthChecksBuilderExtensions.cs new file mode 100644 index 0000000000..8b697d006d --- /dev/null +++ b/src/HealthChecks.Azure.Messaging.EventGrid/DependencyInjection/AzureEventGridHealthChecksBuilderExtensions.cs @@ -0,0 +1,41 @@ +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure . +/// +public static class AzureEventGridHealthChecksBuilderExtensions +{ + private const string NAME = "azure_event_grid"; + + /// + /// Add a health check for Azure Event Grid by registering for given . + /// + /// The to add to. + /// + /// An optional factory to obtain instance. + /// When not provided, is simply resolved from . + /// + /// The health check name. Optional. If null the name 'azure_event_grid' will be used. + /// + /// 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 AddAzureEventGrid( + this IHealthChecksBuilder builder, + Func? clientFactory = default, + string? name = NAME, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => new AzureEventGridHealthCheck(clientFactory?.Invoke(sp) ?? sp.GetRequiredService()), + failureStatus, + tags, + timeout)); + } +} \ No newline at end of file diff --git a/src/HealthChecks.Azure.Messaging.EventGrid/GlobalUsings.cs b/src/HealthChecks.Azure.Messaging.EventGrid/GlobalUsings.cs new file mode 100644 index 0000000000..ae825a6edd --- /dev/null +++ b/src/HealthChecks.Azure.Messaging.EventGrid/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using System; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Runtime.CompilerServices; + +global using Azure.Messaging.EventGrid; + +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; \ No newline at end of file diff --git a/src/HealthChecks.Azure.Messaging.EventGrid/HealthChecks.Azure.Messaging.EventGrid.csproj b/src/HealthChecks.Azure.Messaging.EventGrid/HealthChecks.Azure.Messaging.EventGrid.csproj new file mode 100644 index 0000000000..7894d00b26 --- /dev/null +++ b/src/HealthChecks.Azure.Messaging.EventGrid/HealthChecks.Azure.Messaging.EventGrid.csproj @@ -0,0 +1,16 @@ + + + + net6.0;net7.0;net8.0 + $(PackageTags);Azure;EventGrid + HealthChecks.Azure.Messaging.EventGrid is the health check package for Azure Event Grid. + enable + enable + + + + + + + +]]> \ No newline at end of file diff --git a/src/HealthChecks.Azure.Messaging.EventGrid/README.md b/src/HealthChecks.Azure.Messaging.EventGrid/README.md new file mode 100644 index 0000000000..1d4cf64877 --- /dev/null +++ b/src/HealthChecks.Azure.Messaging.EventGrid/README.md @@ -0,0 +1,51 @@ +# Azure Event Grid Health Check + +This health check verifies the ability to communicate with [Azure Event Grid](https://azure.microsoft.com/services/event-grid/). It uses the provided [EventGridPublisherClient](https://learn.microsoft.com/dotnet/api/azure.messaging.eventgrid.eventgridpublisherclient) to send a ping event to verify connectivity. + +## Implementation + +The health check makes an actual call to Event Grid by sending a simple ping event. This verifies that: +1. The client is properly configured +2. The connection to Event Grid service is working +3. The topic exists and is accessible +4. The credentials are valid and not expired + +## Setup + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + // Register the EventGridPublisherClient + services.AddSingleton(sp => + { + return new EventGridPublisherClient( + new Uri("https://"), + new AzureKeyCredential("")); + }); + + services + .AddHealthChecks() + .AddAzureEventGrid(); +} +``` + +You can also register the health check by providing a factory method for the client: + +```csharp +services + .AddHealthChecks() + .AddAzureEventGrid(sp => + { + return new EventGridPublisherClient( + new Uri("https://"), + new AzureKeyCredential("")); + }); +``` + +## Parameters + +- `clientFactory`: An optional factory to obtain the EventGridPublisherClient instance. When not provided, EventGridPublisherClient is resolved from IServiceProvider. +- `name`: The health check name. Optional. If null the name 'azure_event_grid' will be used. +- `failureStatus`: The HealthStatus that should be reported when the health check fails. Optional. If null then the default status of HealthStatus.Unhealthy will be reported. +- `tags`: A list of tags that can be used to filter sets of health checks. +- `timeout`: An optional TimeSpan representing the timeout of the check. \ No newline at end of file diff --git a/src/HealthChecks.Azure.Messaging.EventGrid/Utils/Guard.cs b/src/HealthChecks.Azure.Messaging.EventGrid/Utils/Guard.cs new file mode 100644 index 0000000000..2e300abc4d --- /dev/null +++ b/src/HealthChecks.Azure.Messaging.EventGrid/Utils/Guard.cs @@ -0,0 +1,15 @@ +namespace HealthChecks.Azure.Messaging.EventGrid; + +internal static class Guard +{ + public static T ThrowIfNull(T value, [CallerArgumentExpression(nameof(value))] string? parameterName = null) + where T : class + { + if (value is null) + { + throw new ArgumentNullException(parameterName); + } + + return value; + } +} \ No newline at end of file diff --git a/test/HealthChecks.Azure.Messaging.EventGrid.Tests/EventGridConformanceTests.cs b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/EventGridConformanceTests.cs new file mode 100644 index 0000000000..ebb78b1d72 --- /dev/null +++ b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/EventGridConformanceTests.cs @@ -0,0 +1,24 @@ +using Azure.Identity; +using Azure.Messaging.EventGrid; + +namespace HealthChecks.Azure.Messaging.EventGrid.Tests; + +public class EventGridConformanceTests : ConformanceTests +{ + protected override IHealthChecksBuilder AddHealthCheck(IHealthChecksBuilder builder, Func? clientFactory = null, Func? optionsFactory = null, string? healthCheckName = null, HealthStatus? failureStatus = null, IEnumerable? tags = null, TimeSpan? timeout = null) + => builder.AddAzureEventGrid(clientFactory, healthCheckName, failureStatus, tags, timeout); + + protected override EventGridPublisherClient CreateClientForNonExistingEndpoint() + => new(new Uri("https://non-existing-topic.region.eventgrid.azure.net"), new AzureCliCredential()); + + protected override AzureEventGridHealthCheck CreateHealthCheck(EventGridPublisherClient client, UnusedOptions? options) + => new(client); + + protected override UnusedOptions CreateHealthCheckOptions() + => new(); +} + +// AzureEventGridHealthCheck does not use any options, the type exists only to meet ConformanceTests<,,> criteria +public sealed class UnusedOptions +{ +} \ No newline at end of file diff --git a/test/HealthChecks.Azure.Messaging.EventGrid.Tests/GlobalUsings.cs b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/GlobalUsings.cs new file mode 100644 index 0000000000..7bbea44071 --- /dev/null +++ b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using System; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Collections.Generic; + +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; + +global using HealthChecks.Tests.Common; \ No newline at end of file diff --git a/test/HealthChecks.Azure.Messaging.EventGrid.Tests/HealthChecks.Azure.Messaging.EventGrid.Tests.csproj b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/HealthChecks.Azure.Messaging.EventGrid.Tests.csproj new file mode 100644 index 0000000000..2bf300e582 --- /dev/null +++ b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/HealthChecks.Azure.Messaging.EventGrid.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + false + enable + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + +]]> \ No newline at end of file diff --git a/test/HealthChecks.Azure.Messaging.EventGrid.Tests/HealthChecks.Azure.Messaging.EventGrid.approved.txt b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/HealthChecks.Azure.Messaging.EventGrid.approved.txt new file mode 100644 index 0000000000..1ef74225b2 --- /dev/null +++ b/test/HealthChecks.Azure.Messaging.EventGrid.Tests/HealthChecks.Azure.Messaging.EventGrid.approved.txt @@ -0,0 +1,15 @@ +namespace HealthChecks.Azure.Messaging.EventGrid +{ + public sealed class AzureEventGridHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck + { + public AzureEventGridHealthCheck(Azure.Messaging.EventGrid.EventGridPublisherClient 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 AzureEventGridHealthChecksBuilderExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAzureEventGrid(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func? clientFactory = null, string? name = "azure_event_grid", Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } + } +} \ No newline at end of file