Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HealthChecks.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<Project Path="src/NetEvolve.HealthChecks.Azure.Queues/NetEvolve.HealthChecks.Azure.Queues.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Azure.ServiceBus/NetEvolve.HealthChecks.Azure.ServiceBus.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Azure.Tables/NetEvolve.HealthChecks.Azure.Tables.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Azure.CosmosDB/NetEvolve.HealthChecks.Azure.CosmosDB.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj" />
<Project Path="src/NetEvolve.HealthChecks.ClickHouse/NetEvolve.HealthChecks.ClickHouse.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Dapr/NetEvolve.HealthChecks.Dapr.csproj" />
Expand Down
60 changes: 60 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.CosmosDB/ClientCreation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace NetEvolve.HealthChecks.Azure.CosmosDB;

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using Azure.Core;
using Azure.Identity;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.DependencyInjection;

internal class ClientCreation
{
private ConcurrentDictionary<string, CosmosClient>? _cosmosClients;

internal CosmosClient GetCosmosClient<TOptions>(
string name,
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : class, ICosmosDbOptions
{
_cosmosClients ??= new ConcurrentDictionary<string, CosmosClient>();

return _cosmosClients.GetOrAdd(name, _ => CreateCosmosClient(options, serviceProvider));
}

internal static CosmosClient CreateCosmosClient<TOptions>(
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : class, ICosmosDbOptions
{
CosmosClientOptions? clientOptions = null;
if (options.ConfigureClientOptions is not null)
{
clientOptions = new CosmosClientOptions();
options.ConfigureClientOptions(clientOptions);
}

var mode = options.Mode ?? CosmosDbClientCreationMode.ConnectionString;

#pragma warning disable IDE0010 // Add missing cases
switch (mode)
{
case CosmosDbClientCreationMode.DefaultAzureCredentials:
var tokenCredential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
return new CosmosClient(options.ServiceEndpoint, tokenCredential, clientOptions);
case CosmosDbClientCreationMode.ConnectionString:
return new CosmosClient(options.ConnectionString, clientOptions);
case CosmosDbClientCreationMode.AccountKey:
return new CosmosClient(options.ServiceEndpoint, options.AccountKey, clientOptions);
case CosmosDbClientCreationMode.ServicePrincipal:
var servicePrincipalCredential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
return new CosmosClient(options.ServiceEndpoint, servicePrincipalCredential, clientOptions);
default:
throw new UnreachableException($"Invalid client creation mode `{mode}`.");
}
#pragma warning restore IDE0010 // Add missing cases
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace NetEvolve.HealthChecks.Azure.CosmosDB;

/// <summary>
/// Specifies the mode for creating a CosmosDB client.
/// </summary>
public enum CosmosDbClientCreationMode
{
/// <summary>
/// Use connection string to create the client.
/// </summary>
ConnectionString,

/// <summary>
/// Use service endpoint with default Azure credentials.
/// </summary>
DefaultAzureCredentials,

/// <summary>
/// Use service endpoint with account key authentication.
/// </summary>
AccountKey,

/// <summary>
/// Use service endpoint with Azure Active Directory (token credential).
/// </summary>
ServicePrincipal
}
115 changes: 115 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbConfigure.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
namespace NetEvolve.HealthChecks.Azure.CosmosDB;

using System;
using System.Threading;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using static Microsoft.Extensions.Options.ValidateOptionsResult;

internal sealed class CosmosDbConfigure
: IConfigureNamedOptions<CosmosDbOptions>,
IValidateOptions<CosmosDbOptions>
{
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;

public CosmosDbConfigure(IConfiguration configuration, IServiceProvider serviceProvider)
{
_configuration = configuration;
_serviceProvider = serviceProvider;
}

public void Configure(string? name, CosmosDbOptions options)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
_configuration.Bind($"HealthChecks:CosmosDb:{name}", options);
}

public void Configure(CosmosDbOptions options) => Configure(Options.DefaultName, options);

public ValidateOptionsResult Validate(string? name, CosmosDbOptions options)
{
if (string.IsNullOrWhiteSpace(name))
{
return Fail("The name cannot be null or whitespace.");
}

if (options is null)
{
return Fail("The option cannot be null.");
}

if (options.Timeout < Timeout.Infinite)
{
return Fail("The timeout value must be a positive number in milliseconds or -1 for an infinite timeout.");
}

var mode = options.Mode ?? CosmosDbClientCreationMode.ConnectionString;

return mode switch
{
CosmosDbClientCreationMode.ConnectionString => ValidateModeConnectionString(options),
CosmosDbClientCreationMode.DefaultAzureCredentials => ValidateModeDefaultAzureCredentials(options),
CosmosDbClientCreationMode.AccountKey => ValidateModeAccountKey(options),
CosmosDbClientCreationMode.ServicePrincipal => ValidateModeServicePrincipal(options),
_ => Fail($"The mode `{mode}` is not supported."),
};
}

private static ValidateOptionsResult ValidateModeConnectionString(CosmosDbOptions options)
{
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
return Fail(
$"The connection string cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.ConnectionString)}` mode."
);
}

return Success;
}

private static ValidateOptionsResult ValidateModeDefaultAzureCredentials(CosmosDbOptions options)
{
if (string.IsNullOrWhiteSpace(options.ServiceEndpoint))
{
return Fail(
$"The service endpoint cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.DefaultAzureCredentials)}` mode."
);
}

return Success;
}

private static ValidateOptionsResult ValidateModeAccountKey(CosmosDbOptions options)
{
if (string.IsNullOrWhiteSpace(options.ServiceEndpoint))
{
return Fail(
$"The service endpoint cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.AccountKey)}` mode."
);
}

if (string.IsNullOrWhiteSpace(options.AccountKey))
{
return Fail(
$"The account key cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.AccountKey)}` mode."
);
}

return Success;
}

private static ValidateOptionsResult ValidateModeServicePrincipal(CosmosDbOptions options)
{
if (string.IsNullOrWhiteSpace(options.ServiceEndpoint))
{
return Fail(
$"The service endpoint cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.ServicePrincipal)}` mode."
);
}

return Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace NetEvolve.HealthChecks.Azure.CosmosDB;

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using NetEvolve.Extensions.Tasks;
using NetEvolve.HealthChecks.Abstractions;

internal sealed class CosmosDbHealthCheck : ConfigurableHealthCheckBase<CosmosDbOptions>
{
private readonly IServiceProvider _serviceProvider;

public CosmosDbHealthCheck(
IServiceProvider serviceProvider,
IOptionsMonitor<CosmosDbOptions> optionsMonitor
)
: base(optionsMonitor) => _serviceProvider = serviceProvider;

protected override async ValueTask<HealthCheckResult> ExecuteHealthCheckAsync(
string name,
HealthStatus failureStatus,
CosmosDbOptions options,
CancellationToken cancellationToken
)
{
var clientCreation = _serviceProvider.GetRequiredService<ClientCreation>();
var cosmosClient = clientCreation.GetCosmosClient(name, options, _serviceProvider);

// Check if the CosmosDB service is available by reading the account properties
var (serviceAvailable, _) = await cosmosClient
.ReadAccountAsync()
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

if (!serviceAvailable)
{
return HealthCheckState(false, name);
}

// If database name is specified, check database availability
if (!string.IsNullOrWhiteSpace(options.DatabaseName))
{
var database = cosmosClient.GetDatabase(options.DatabaseName);
var (databaseAvailable, _) = await database
.ReadAsync(cancellationToken: cancellationToken)
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

if (!databaseAvailable)
{
return HealthCheckResult.Unhealthy($"{name}: Database `{options.DatabaseName}` is not available.");
}

// If container name is also specified, check container availability
if (!string.IsNullOrWhiteSpace(options.ContainerName))
{
var container = database.GetContainer(options.ContainerName);
var (containerAvailable, _) = await container
.ReadContainerAsync(cancellationToken: cancellationToken)
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

if (!containerAvailable)
{
return HealthCheckResult.Unhealthy($"{name}: Container `{options.ContainerName}` is not available.");
}
}
}

return HealthCheckState(true, name);
}
}
50 changes: 50 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace NetEvolve.HealthChecks.Azure.CosmosDB;

using System;
using Microsoft.Azure.Cosmos;

/// <summary>
/// Options for the <see cref="CosmosDbHealthCheck"/>.
/// </summary>
public sealed record CosmosDbOptions : ICosmosDbOptions
{
/// <summary>
/// Gets or sets the connection string.
/// </summary>
public string? ConnectionString { get; set; }

/// <summary>
/// Gets or sets the mode to create the client.
/// </summary>
public CosmosDbClientCreationMode? Mode { get; set; }

/// <summary>
/// Gets or sets the service endpoint.
/// </summary>
public string? ServiceEndpoint { get; set; }

/// <summary>
/// Gets or sets the account key.
/// </summary>
public string? AccountKey { get; set; }

/// <summary>
/// Gets or sets the lambda to configure the <see cref="CosmosClientOptions"/>.
/// </summary>
public Action<CosmosClientOptions>? ConfigureClientOptions { get; set; }

/// <summary>
/// The timeout to use when connecting and executing tasks against the database.
/// </summary>
public int Timeout { get; set; } = 100;

/// <summary>
/// Gets or sets the database name to check.
/// </summary>
public string? DatabaseName { get; set; }

/// <summary>
/// Gets or sets the container name to check.
/// </summary>
public string? ContainerName { get; set; }
}
Loading
Loading