-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
Closed: #41
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
namespace NetEvolve.HealthChecks.Azure.ServiceBus; | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Diagnostics; | ||
using global::Azure.Core; | ||
using global::Azure.Identity; | ||
using global::Azure.Messaging.ServiceBus; | ||
using global::Azure.Messaging.ServiceBus.Administration; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
internal static class ClientCreation | ||
{ | ||
private static ConcurrentDictionary<string, ServiceBusClient>? _serviceBusClients; | ||
private static ConcurrentDictionary< | ||
string, | ||
ServiceBusAdministrationClient | ||
>? _serviceBusAdministrationClients; | ||
|
||
internal static ServiceBusClient GetClient<TOptions>( | ||
string name, | ||
TOptions options, | ||
IServiceProvider serviceProvider | ||
) | ||
where TOptions : ServiceBusOptionsBase | ||
{ | ||
if (options.Mode == ClientCreationMode.ServiceProvider) | ||
{ | ||
return serviceProvider.GetRequiredService<ServiceBusClient>(); | ||
} | ||
|
||
if (_serviceBusClients is null) | ||
{ | ||
_serviceBusClients = new ConcurrentDictionary<string, ServiceBusClient>( | ||
StringComparer.OrdinalIgnoreCase | ||
); | ||
} | ||
|
||
return _serviceBusClients.GetOrAdd(name, _ => CreateClient(options, serviceProvider)); | ||
} | ||
|
||
internal static ServiceBusAdministrationClient GetAdministrationClient<TOptions>( | ||
string name, | ||
TOptions options, | ||
IServiceProvider serviceProvider | ||
) | ||
where TOptions : ServiceBusOptionsBase | ||
{ | ||
if (options.Mode == ClientCreationMode.ServiceProvider) | ||
{ | ||
return serviceProvider.GetRequiredService<ServiceBusAdministrationClient>(); | ||
} | ||
|
||
if (_serviceBusAdministrationClients is null) | ||
{ | ||
_serviceBusAdministrationClients = new ConcurrentDictionary< | ||
string, | ||
ServiceBusAdministrationClient | ||
>(StringComparer.OrdinalIgnoreCase); | ||
} | ||
|
||
return _serviceBusAdministrationClients.GetOrAdd( | ||
name, | ||
_ => CreateAdministrationClient(options, serviceProvider) | ||
); | ||
} | ||
|
||
private static ServiceBusClient CreateClient<TOptions>( | ||
TOptions options, | ||
IServiceProvider serviceProvider | ||
) | ||
where TOptions : ServiceBusOptionsBase => | ||
options.Mode switch | ||
{ | ||
ClientCreationMode.ServiceProvider => | ||
serviceProvider.GetRequiredService<ServiceBusClient>(), | ||
ClientCreationMode.DefaultAzureCredentials => new ServiceBusClient( | ||
options.FullyQualifiedNamespace, | ||
serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential() | ||
), | ||
ClientCreationMode.ConnectionString => new ServiceBusClient(options.ConnectionString), | ||
_ => throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."), | ||
}; | ||
|
||
private static ServiceBusAdministrationClient CreateAdministrationClient<TOptions>( | ||
TOptions options, | ||
IServiceProvider serviceProvider | ||
) | ||
where TOptions : ServiceBusOptionsBase => | ||
options.Mode switch | ||
{ | ||
ClientCreationMode.ServiceProvider => | ||
serviceProvider.GetRequiredService<ServiceBusAdministrationClient>(), | ||
ClientCreationMode.DefaultAzureCredentials => new ServiceBusAdministrationClient( | ||
options.FullyQualifiedNamespace, | ||
serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential() | ||
), | ||
ClientCreationMode.ConnectionString => new ServiceBusAdministrationClient( | ||
options.ConnectionString | ||
), | ||
_ => throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."), | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace NetEvolve.HealthChecks.Azure.ServiceBus; | ||
|
||
using System; | ||
using global::Azure.Messaging.ServiceBus; | ||
|
||
/// <summary> | ||
/// Describes the mode to create or retrieve a <see cref="ServiceBusClient"/>. | ||
/// </summary> | ||
public enum ClientCreationMode | ||
{ | ||
/// <summary> | ||
/// The default mode. The <see cref="ServiceBusClient"/> is loading the preregistered instance from the <see cref="IServiceProvider"/>. | ||
/// </summary> | ||
ServiceProvider = 0, | ||
|
||
DefaultAzureCredentials, | ||
Check warning on line 16 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs GitHub Actions / Build & Tests / CodeQL / Run CodeQL
|
||
ConnectionString, | ||
Check warning on line 17 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs GitHub Actions / Build & Tests / CodeQL / Run CodeQL
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
namespace NetEvolve.HealthChecks.Azure.ServiceBus; | ||
|
||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Diagnostics.HealthChecks; | ||
using NetEvolve.Arguments; | ||
using NetEvolve.HealthChecks.Abstractions; | ||
|
||
/// <summary> | ||
/// Extensions methods for <see cref="IHealthChecksBuilder"/> with custom Health Checks. | ||
/// </summary> | ||
public static class DependencyInjectionExtensions | ||
{ | ||
private static readonly string[] _defaultTags = ["storage", "azure", "servicebus"]; | ||
|
||
/// <summary> | ||
/// Adds a health check for an Azure Service Bus Queue. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param> | ||
/// <param name="name">The name of the <see cref="ServiceBusQueueHealthCheck"/>.</param> | ||
/// <param name="options">An optional action to configure.</param> | ||
/// <param name="tags">A list of additional tags that can be used to filter sets of health checks. Optional.</param> | ||
/// <exception cref="ArgumentNullException">The <paramref name="builder"/> is <see langword="null" />.</exception> | ||
/// <exception cref="ArgumentNullException">The <paramref name="name"/> is <see langword="null" />.</exception> | ||
/// <exception cref="ArgumentException">The <paramref name="name"/> is <see langword="null" /> or <c>whitespace</c>.</exception> | ||
/// <exception cref="ArgumentException">The <paramref name="name"/> is already in use.</exception> | ||
/// <exception cref="ArgumentNullException">The <paramref name="tags"/> is <see langword="null" />.</exception> | ||
public static IHealthChecksBuilder AddServiceBusQueueHealthCheck( | ||
[NotNull] this IHealthChecksBuilder builder, | ||
[NotNull] string name, | ||
Action<ServiceBusQueueOptions>? options = null, | ||
params string[] tags | ||
) | ||
{ | ||
ArgumentNullException.ThrowIfNull(builder); | ||
Argument.ThrowIfNullOrEmpty(name); | ||
ArgumentNullException.ThrowIfNull(tags); | ||
|
||
if (!builder.IsServiceTypeRegistered<ServiceBusQueueMarker>()) | ||
{ | ||
_ = builder | ||
.Services.AddSingleton<ServiceBusQueueMarker>() | ||
.AddSingleton<ServiceBusQueueHealthCheck>() | ||
.ConfigureOptions<ServiceBusQueueOptionsConfigure>(); | ||
} | ||
|
||
if (builder.IsNameAlreadyUsed<ServiceBusQueueHealthCheck>(name)) | ||
{ | ||
throw new ArgumentException($"Name `{name}` already in use.", nameof(name), null); | ||
} | ||
|
||
if (options is not null) | ||
{ | ||
_ = builder.Services.Configure(name, options); | ||
} | ||
|
||
return builder.AddCheck<ServiceBusQueueHealthCheck>( | ||
name, | ||
HealthStatus.Unhealthy, | ||
_defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase) | ||
); | ||
} | ||
|
||
private sealed partial class ServiceBusQueueMarker { } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>$(_ProjectTargetFrameworks)</TargetFrameworks> | ||
|
||
<Description>Contains HealthChecks for Azure Service Bus.</Description> | ||
<PackageTags>$(PackageTags);azure;servicebus;</PackageTags> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Azure.Identity" /> | ||
<PackageReference Include="Azure.Messaging.ServiceBus" /> | ||
<PackageReference Include="NetEvolve.Arguments" /> | ||
<PackageReference Include="NetEvolve.Extensions.Tasks" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\NetEvolve.HealthChecks.Abstractions\NetEvolve.HealthChecks.Abstractions.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
namespace NetEvolve.HealthChecks.Azure.ServiceBus; | ||
|
||
public abstract class ServiceBusOptionsBase | ||
Check warning on line 3 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs GitHub Actions / Build & Tests / CodeQL / Run CodeQL
|
||
{ | ||
/// <summary> | ||
/// Gets or sets the client creation mode, default is <see cref="ClientCreationMode.ServiceProvider"/>. | ||
/// </summary> | ||
public ClientCreationMode Mode { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the azure service bus connection string. | ||
/// </summary> | ||
public string? ConnectionString { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the fully qualified namespace. | ||
/// </summary> | ||
public string FullyQualifiedNamespace { get; set; } | ||
Check warning on line 18 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs GitHub Actions / Build & Tests / CodeQL / Run CodeQL
Check warning on line 18 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs GitHub Actions / Build & Tests / Tests / Testing .NET solution
|
||
|
||
/// <summary> | ||
/// The timeout to use when connecting and executing tasks against database. | ||
/// Default is 100 milliseconds. | ||
/// </summary> | ||
public int Timeout { get; set; } = 100; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
namespace NetEvolve.HealthChecks.Azure.ServiceBus; | ||
|
||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Diagnostics.HealthChecks; | ||
using Microsoft.Extensions.Options; | ||
using NetEvolve.Extensions.Tasks; | ||
using NetEvolve.HealthChecks.Abstractions; | ||
|
||
internal sealed class ServiceBusQueueHealthCheck | ||
: ConfigurableHealthCheckBase<ServiceBusQueueOptions> | ||
{ | ||
private readonly IServiceProvider _serviceProvider; | ||
|
||
public ServiceBusQueueHealthCheck( | ||
IServiceProvider serviceProvider, | ||
IOptionsMonitor<ServiceBusQueueOptions> optionsMonitor | ||
) | ||
: base(optionsMonitor) => _serviceProvider = serviceProvider; | ||
|
||
protected override ValueTask<HealthCheckResult> ExecuteHealthCheckAsync( | ||
string name, | ||
HealthStatus failureStatus, | ||
ServiceBusQueueOptions options, | ||
CancellationToken cancellationToken | ||
) => | ||
options.EnablePeekMode | ||
? ExecutePeekHealthCheckAsync(name, options, cancellationToken) | ||
: ExecuteHealthCheckAsync(name, options, cancellationToken); | ||
|
||
private async ValueTask<HealthCheckResult> ExecuteHealthCheckAsync( | ||
string name, | ||
ServiceBusQueueOptions options, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
var client = ClientCreation.GetAdministrationClient(name, options, _serviceProvider); | ||
|
||
var (isValid, queue) = await client | ||
.GetQueueAsync(options.QueueName, cancellationToken: cancellationToken) | ||
.WithTimeoutAsync(options.Timeout, cancellationToken) | ||
.ConfigureAwait(false); | ||
|
||
return HealthCheckState(isValid && queue is not null, name); | ||
} | ||
|
||
private async ValueTask<HealthCheckResult> ExecutePeekHealthCheckAsync( | ||
string name, | ||
ServiceBusQueueOptions options, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
var client = ClientCreation.GetClient(name, options, _serviceProvider); | ||
|
||
var receiver = client.CreateReceiver(options.QueueName); | ||
|
||
var (isValid, _) = await receiver | ||
.ReceiveMessageAsync(cancellationToken: cancellationToken) | ||
.WithTimeoutAsync(options.Timeout, cancellationToken) | ||
.ConfigureAwait(false); | ||
|
||
return HealthCheckState(isValid, name); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
namespace NetEvolve.HealthChecks.Azure.ServiceBus; | ||
|
||
using Microsoft.Extensions.Options; | ||
|
||
public class ServiceBusQueueOptions : ServiceBusOptionsBase | ||
Check warning on line 5 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs GitHub Actions / Build & Tests / CodeQL / Run CodeQL
|
||
{ | ||
/// <summary> | ||
/// Gets or sets a value indicating whether to enable peek mode, default is <c>false</c>. | ||
/// </summary> | ||
/// <remarks> | ||
/// To enable the peek mode, the executing user requires Listen claim to work. | ||
/// </remarks> | ||
/// <seealso href="https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender"/> | ||
public bool EnablePeekMode { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the queue name, which is checked if it exists. | ||
/// </summary> | ||
public string? QueueName { get; set; } | ||
} |