Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Add health checks #377

Merged
merged 11 commits into from
Nov 26, 2023
6 changes: 1 addition & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="xunit" Version="2.6.1" />
<PackageVersion Include="Scrutor" Version="4.2.2" />
<PackageVersion Include="AspNetCore.HealthChecks.CosmosDb" Version="7.1.0" />
</ItemGroup>
<!-- .NET 7.0. bits -->
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageVersion Include="AspNetCore.HealthChecks.CosmosDb" Version="7.1.0" />
<PackageVersion Include="IEvangelist.Azure.CosmosRepository" Version="7.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.14" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
Expand All @@ -28,7 +28,6 @@
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
Expand All @@ -47,7 +46,6 @@
</ItemGroup>
<!-- .NET 8.0. bits -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="AspNetCore.HealthChecks.CosmosDb" Version="7.1.0" />
<PackageVersion Include="IEvangelist.Azure.CosmosRepository" Version="7.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
Expand All @@ -56,7 +54,6 @@
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
Expand All @@ -82,7 +79,6 @@
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
Expand Down
28 changes: 28 additions & 0 deletions docs/cosmos-repo/content/6-miscellaneous/healthchecks/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: "Health Checks"
weight: 2
---

The `IEvangelist.Azure.CosmosRepository.AspNetCore` package adds support for [AspNet Core Health Checks](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks) using the [HealthChecks.CosmosDb](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/blob/master/src/HealthChecks.CosmosDb/README.md) package.

## Setup

To configure Cosmos DB health checks:

```csharp
services.AddHealthChecks().AddCosmosRepository();
```

By default, this will scan all of the assemblies in your solution to locate the container names for each of your `IItem` types. To refine this, and potentially reduce startup times, pass in the Assemblies containing your `IItem` types:

```csharp
services.AddHealthChecks().AddCosmosRepository(assemblies: typeof(ExampleItem).Assembly);
```

The Cosmos Repository Health package supports all of the existing functionality of Health Checks, such as failureStatus and tags, see the [Microsoft Documentation](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks) for configuration details.

Don't forget to map the health endpoint with:

```csharp
app.MapHealthChecks("/healthz");
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.CosmosDb" />
<PackageReference Include="CorrelationId" />
<PackageReference Include="Mumby0168.CleanArchitecture.Exceptions" />
<PackageReference Include="Mumby0168.CleanArchitecture.Exceptions.AspNetCore" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@
using CorrelationId;
using CorrelationId.Abstractions;
using CorrelationId.DependencyInjection;
using Microsoft.Azure.CosmosRepository.Extensions;
using Microsoft.Azure.CosmosEventSourcing.Extensions;
using Microsoft.Azure.CosmosEventSourcing.Stores;
using Microsoft.Azure.CosmosRepository.AspNetCore.Extensions;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.CosmosRepository.Extensions;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
IServiceCollection services = builder.Services;

services.AddHealthChecks().AddAzureCosmosDB(provider =>
{
var connectionString = builder.Configuration.GetCosmosRepositoryConnectionString();
return new CosmosClient(connectionString);
});

services.AddHealthChecks().AddCosmosRepository();
services.AddCleanArchitectureExceptionsHandler(options => options.ApplicationName = "EventSourcingShipSample");
services.AddSwaggerGen();
services.AddEndpointsApiExplorer();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HealthChecks.CosmosDb;
using Microsoft.Azure.CosmosRepository.Options;
using Microsoft.Azure.CosmosRepository.Providers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.CosmosRepository.AspNetCore.Extensions;

public static class HealthChecksBuilderExtensions
{
/// <summary>
/// Add a health check for Azure Cosmos DB by registering <see cref="AzureCosmosDbHealthCheck"/> for given <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add <see cref="HealthCheckRegistration"/> to.</param>
/// <param name="healthCheckName">The health check name. Optional. If <c>null</c> the name 'azure_cosmosdb' will be used.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <param name="assemblies">The assemblies to scan for <see cref="IItem"/> types. Optional. If <c>null</c> types are discovered autimatically. Providing a assemblies to scan may reduce start up time.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddCosmosRepository(this IHealthChecksBuilder builder,
string? healthCheckName = "azure_cosmosdb",
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default,
params Assembly[]? assemblies)
{
builder.AddAzureCosmosDB(
clientFactory: provider => provider.GetRequiredService<ICosmosClientProvider>().CosmosClient,
optionsFactory: sp =>
{
IOptions<RepositoryOptions> options = sp.GetRequiredService<IOptions<RepositoryOptions>>();
ICosmosItemConfigurationProvider itemConfigProvider =
sp.GetRequiredService<ICosmosItemConfigurationProvider>();
IEnumerable<string> containers = itemConfigProvider.GetAllItemConfigurations(assemblies)
.Select(i => i.ContainerName).Distinct();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.Select(i => i.ContainerName).Distinct();
.DistinctBy(i => i.ContainerName);


return new AzureCosmosDbHealthCheckOptions
{
DatabaseId = options.Value.DatabaseId,
ContainerIds = containers
};
}, healthCheckName, failureStatus, tags, timeout);

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Company>Microsoft Corporation</Company>
<Product>IEvangelist Azure Cosmos Repository</Product>
<TargetFrameworks>$(DefaultTargetFrameworks);$(CompatibilityTargetFrameworks)</TargetFrameworks>
<TargetFrameworks>$(DefaultTargetFrameworks)</TargetFrameworks>
<Description>This client library enables client applications to connect to Azure Cosmos via a repository pattern around the official Azure Cosmos .NET SDK. Azure Cosmos is a globally distributed, multi-model database service. For more information, refer to http://azure.microsoft.com/services/cosmos-db/. </Description>
<Copyright>Copyright © IEvangelist. All rights reserved. Licensed under the MIT License.</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
Expand Down Expand Up @@ -37,31 +36,30 @@
<RepositoryUrl>https://github.com/IEvangelist/azure-cosmos-dotnet-repository</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryType>git</RepositoryType>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="MinVer">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Scrutor" />
<PackageReference Include="AspNetCore.HealthChecks.CosmosDb" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Azure.CosmosRepository\Microsoft.Azure.CosmosRepository.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Azure.CosmosRepository.AspNetCoreTests" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\LICENSE">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ public static class ServiceCollectionExtensions
/// </summary>
/// <param name="services">The service collection to add services to.</param>
/// <param name="setupAction">An action to configure the repository options</param>
/// <param name="additionSetupAction">An action to configure the <see cref="CosmosClientOptions"/></param>
/// <param name="additionalSetupAction">An action to configure the <see cref="CosmosClientOptions"/></param>
/// <returns>The same service collection that was provided, with the required cosmos services.</returns>
public static IServiceCollection AddCosmosRepository(
this IServiceCollection services,
Action<RepositoryOptions>? setupAction = default,
Action<CosmosClientOptions>? additionSetupAction = default)
Action<CosmosClientOptions>? additionalSetupAction = default)
{
if (services is null)
{
Expand All @@ -34,7 +34,7 @@ public static IServiceCollection AddCosmosRepository(

services.AddLogging()
.AddHttpClient()
.AddSingleton(new CosmosClientOptionsManipulator(additionSetupAction))
.AddSingleton(new CosmosClientOptionsManipulator(additionalSetupAction))
.AddSingleton<ICosmosClientOptionsProvider, DefaultCosmosClientOptionsProvider>()
.AddSingleton<ICosmosClientProvider, DefaultCosmosClientProvider>()
.AddSingleton(typeof(ICosmosContainerProvider<>), typeof(DefaultCosmosContainerProvider<>))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class DefaultCosmosClientProvider : ICosmosClientProvider, IDisposable
readonly CosmosClientOptions _cosmosClientOptions;
readonly RepositoryOptions _options;

public CosmosClient CosmosClient => _lazyCosmosClient.Value;

private DefaultCosmosClientProvider(
CosmosClientOptions cosmosClientOptions,
IOptions<RepositoryOptions> options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ public ItemConfiguration GetItemConfiguration<TItem>() where TItem : IItem =>
public ItemConfiguration GetItemConfiguration(Type itemType) =>
_itemOptionsMap.GetOrAdd(itemType, AddOptions(itemType));

public List<ItemConfiguration> GetAllItemConfigurations(params Assembly[]? assemblies)
{
IEnumerable<Type> itemTypes = (assemblies ?? AppDomain.CurrentDomain.GetAssemblies())
.SelectMany(s => s.GetTypes())
.Where(p => typeof(IItem).IsAssignableFrom(p) && p is {IsInterface: false, IsAbstract: false});

foreach (Type itemType in itemTypes)
{
_itemOptionsMap.GetOrAdd(itemType, AddOptions(itemType));
}

return _itemOptionsMap.Select(i => i.Value).ToList();
}

private ItemConfiguration AddOptions(Type itemType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ namespace Microsoft.Azure.CosmosRepository.Providers;
/// an instance to the configured <see cref="CosmosClient"/> object,
/// which is shared.
/// </summary>
interface ICosmosClientProvider
internal interface ICosmosClientProvider
{
Task<T> UseClientAsync<T>(Func<CosmosClient, Task<T>> consume);

CosmosClient CosmosClient { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ namespace Microsoft.Azure.CosmosRepository.Providers;
/// <summary>
/// Holds all of the configuration information for an item.
/// </summary>
interface ICosmosItemConfigurationProvider
internal interface ICosmosItemConfigurationProvider
{
ItemConfiguration GetItemConfiguration<TItem>() where TItem : IItem;

ItemConfiguration GetItemConfiguration(Type itemType);

List<ItemConfiguration> GetAllItemConfigurations(params Assembly[]? assemblies);
}
3 changes: 2 additions & 1 deletion src/Microsoft.Azure.CosmosRepository/Visibility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
// Licensed under the MIT License.


[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Microsoft.Azure.CosmosRepository.AspNetCore")]
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void GetOptionsAlwaysGetOptionsForItem()
UniqueKeyPolicy uniqueKeyPolicy = new();
var throughputProperties = ThroughputProperties.CreateAutoscaleThroughput(400);

_containerNameProvider.Setup(o => o.GetContainerName(typeof(Item1))).Returns("a");
_containerNameProvider.Setup(o => o.GetContainerName(typeof(Item1))).Returns<Type>(t => t.FullName!);
_partitionKeyPathProvider.Setup(o => o.GetPartitionKeyPath(typeof(Item1))).Returns("/id");
_uniqueKeyPolicyProvider.Setup(o => o.GetUniqueKeyPolicy(typeof(Item1))).Returns(uniqueKeyPolicy);
_defaultTimeToLiveProvider.Setup(o => o.GetDefaultTimeToLive(typeof(Item1))).Returns(10);
Expand All @@ -37,16 +37,45 @@ public void GetOptionsAlwaysGetOptionsForItem()

ItemConfiguration configuration = provider.GetItemConfiguration<Item1>();

Assert.Equal("a", configuration.ContainerName);
Assert.Equal(typeof(Item1).FullName, configuration.ContainerName);
Assert.Equal("/id", configuration.PartitionKeyPath);
Assert.Equal(uniqueKeyPolicy, configuration.UniqueKeyPolicy);
Assert.Equal(10, configuration.DefaultTimeToLive);
Assert.True(configuration.SyncContainerProperties);
Assert.Equal(throughputProperties, configuration.ThroughputProperties);
}

[Fact]
public void GetAllItemConfigurationsAlwaysGetsAllOptionsForItems()
{
ICosmosItemConfigurationProvider provider = new DefaultCosmosItemConfigurationProvider(
_containerNameProvider.Object,
_partitionKeyPathProvider.Object,
_uniqueKeyPolicyProvider.Object,
_defaultTimeToLiveProvider.Object,
_syncContainerPropertiesProvider.Object,
_throughputProvider.Object,
_strictTypeCheckingProvider.Object);

_containerNameProvider.Setup(o => o.GetContainerName(It.IsAny<Type>())).Returns<Type>(t => t.FullName!);

IEnumerable<string> expectedContainerNames = new[] {typeof(Item1).Assembly}
.SelectMany(s => s.GetTypes())
.Where(p => typeof(IItem).IsAssignableFrom(p) && p is {IsInterface: false, IsAbstract: false})
.Select(t => t.FullName!).OrderBy(name => name);

IEnumerable<string> containerNames = provider.GetAllItemConfigurations(typeof(Item1).Assembly).Select(c => c.ContainerName).OrderBy(name => name);

containerNames.Should().BeEquivalentTo(expectedContainerNames);
}

class Item1 : Item
{

}

class Item2 : Item
{

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public TestCosmosClientProvider(CosmosClient cosmosClient) =>

public Task<T> UseClientAsync<T>(Func<CosmosClient, Task<T>> consume)
=> consume(_cosmosClient) ?? throw new ArgumentNullException(nameof(consume));

public CosmosClient CosmosClient => _cosmosClient;
}