Skip to content

Commit 1bc7ff9

Browse files
Copilotsamtrion
andcommitted
Implement Azure Synapse health check package with basic structure and tests
Co-authored-by: samtrion <[email protected]>
1 parent ad3679b commit 1bc7ff9

16 files changed

+562
-0
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.19.0" />
2626
<PackageVersion Include="Azure.Storage.Blobs" Version="12.24.0" />
2727
<PackageVersion Include="Azure.Storage.Queues" Version="12.22.0" />
28+
<PackageVersion Include="Azure.Analytics.Synapse.Artifacts" Version="1.0.0-preview.19" />
2829
<PackageVersion Include="ClickHouse.Client" Version="7.14.0" />
2930
<PackageVersion Include="CliWrap" Version="3.8.2" />
3031
<PackageVersion Include="Confluent.Kafka" Version="2.10.0" />

HealthChecks.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<Project Path="src/NetEvolve.HealthChecks.Azure.Blobs/NetEvolve.HealthChecks.Azure.Blobs.csproj" />
3737
<Project Path="src/NetEvolve.HealthChecks.Azure.Queues/NetEvolve.HealthChecks.Azure.Queues.csproj" />
3838
<Project Path="src/NetEvolve.HealthChecks.Azure.ServiceBus/NetEvolve.HealthChecks.Azure.ServiceBus.csproj" />
39+
<Project Path="src/NetEvolve.HealthChecks.Azure.Synapse/NetEvolve.HealthChecks.Azure.Synapse.csproj" />
3940
<Project Path="src/NetEvolve.HealthChecks.Azure.Tables/NetEvolve.HealthChecks.Azure.Tables.csproj" />
4041
<Project Path="src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj" />
4142
<Project Path="src/NetEvolve.HealthChecks.ClickHouse/NetEvolve.HealthChecks.ClickHouse.csproj" />
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace NetEvolve.HealthChecks.Azure.Synapse;
2+
3+
using System;
4+
using System.Collections.Concurrent;
5+
using System.Diagnostics;
6+
using global::Azure.Analytics.Synapse.Artifacts;
7+
using global::Azure.Identity;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
internal class ClientCreation
11+
{
12+
private ConcurrentDictionary<string, ArtifactsClient>? _artifactsClients;
13+
14+
internal ArtifactsClient GetArtifactsClient<TOptions>(
15+
string name,
16+
TOptions options,
17+
IServiceProvider serviceProvider
18+
)
19+
where TOptions : class, ISynapseOptions
20+
{
21+
if (options.Mode == SynapseClientCreationMode.ServiceProvider)
22+
{
23+
return serviceProvider.GetRequiredService<ArtifactsClient>();
24+
}
25+
26+
_artifactsClients ??= new ConcurrentDictionary<string, ArtifactsClient>(StringComparer.OrdinalIgnoreCase);
27+
28+
return _artifactsClients.GetOrAdd(name, _ => CreateArtifactsClient(options, serviceProvider));
29+
}
30+
31+
internal static ArtifactsClient CreateArtifactsClient<TOptions>(
32+
TOptions options,
33+
IServiceProvider serviceProvider
34+
)
35+
where TOptions : class, ISynapseOptions
36+
{
37+
switch (options.Mode)
38+
{
39+
case SynapseClientCreationMode.DefaultAzureCredentials:
40+
var tokenCredential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
41+
return new ArtifactsClient(options.WorkspaceUri, tokenCredential);
42+
case SynapseClientCreationMode.ConnectionString:
43+
// Note: For connection string mode, we'll need to parse the connection string to extract workspace URI
44+
// and use DefaultAzureCredential for authentication
45+
var credential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
46+
return new ArtifactsClient(options.WorkspaceUri, credential);
47+
default:
48+
throw new UnreachableException($"Invalid client creation mode `{options.Mode}`.");
49+
}
50+
}
51+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace NetEvolve.HealthChecks.Azure.Synapse;
2+
3+
using System;
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Linq;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Extensions.Diagnostics.HealthChecks;
9+
using NetEvolve.HealthChecks.Abstractions;
10+
11+
/// <summary>
12+
/// Extensions methods for <see cref="IHealthChecksBuilder"/> with custom Health Checks.
13+
/// </summary>
14+
public static class DependencyInjectionExtensions
15+
{
16+
private static readonly string[] _defaultTags = ["azure", "synapse", "analytics"];
17+
18+
/// <summary>
19+
/// Adds a health check for the Azure Synapse Analytics, to check the availability of a workspace.
20+
/// </summary>
21+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
22+
/// <param name="name">The name of the <see cref="SynapseWorkspaceAvailableHealthCheck"/>.</param>
23+
/// <param name="options">An optional action to configure.</param>
24+
/// <param name="tags">A list of additional tags that can be used to filter sets of health checks. Optional.</param>
25+
/// <exception cref="ArgumentNullException">The <paramref name="builder"/> is <see langword="null" />.</exception>
26+
/// <exception cref="ArgumentNullException">The <paramref name="name"/> is <see langword="null" />.</exception>
27+
/// <exception cref="ArgumentException">The <paramref name="name"/> is <see langword="null" /> or <c>whitespace</c>.</exception>
28+
/// <exception cref="ArgumentException">The <paramref name="name"/> is already in use.</exception>
29+
/// <exception cref="ArgumentNullException">The <paramref name="tags"/> is <see langword="null" />.</exception>
30+
public static IHealthChecksBuilder AddSynapseWorkspaceAvailability(
31+
[NotNull] this IHealthChecksBuilder builder,
32+
[NotNull] string name,
33+
Action<SynapseWorkspaceAvailableOptions>? options = null,
34+
params string[] tags
35+
)
36+
{
37+
ArgumentNullException.ThrowIfNull(builder);
38+
ArgumentException.ThrowIfNullOrEmpty(name);
39+
ArgumentNullException.ThrowIfNull(tags);
40+
41+
if (!builder.IsServiceTypeRegistered<AzureSynapseWorkspaceCheckMarker>())
42+
{
43+
_ = builder
44+
.Services.AddSingleton<AzureSynapseWorkspaceCheckMarker>()
45+
.AddSingleton<SynapseWorkspaceAvailableHealthCheck>()
46+
.ConfigureOptions<SynapseWorkspaceAvailableConfigure>();
47+
48+
builder.Services.TryAddSingleton<ClientCreation>();
49+
}
50+
51+
builder.ThrowIfNameIsAlreadyUsed<SynapseWorkspaceAvailableHealthCheck>(name);
52+
53+
if (options is not null)
54+
{
55+
_ = builder.Services.Configure(name, options);
56+
}
57+
58+
return builder.AddCheck<SynapseWorkspaceAvailableHealthCheck>(
59+
name,
60+
HealthStatus.Unhealthy,
61+
_defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase)
62+
);
63+
}
64+
65+
private sealed partial class AzureSynapseWorkspaceCheckMarker;
66+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace NetEvolve.HealthChecks.Azure.Synapse;
2+
3+
using System;
4+
5+
internal interface ISynapseOptions
6+
{
7+
Uri? WorkspaceUri { get; }
8+
9+
string? ConnectionString { get; }
10+
11+
SynapseClientCreationMode? Mode { get; }
12+
13+
int Timeout { get; }
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>$(_ProjectTargetFrameworks)</TargetFrameworks>
4+
<Description>Contains HealthChecks for Azure Synapse Analytics.</Description>
5+
<PackageTags>$(PackageTags);azure;synapse;analytics</PackageTags>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageReference Include="Azure.Analytics.Synapse.Artifacts" />
9+
<PackageReference Include="Azure.Identity" />
10+
<PackageReference Include="NetEvolve.Extensions.Tasks" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<ProjectReference Include="..\NetEvolve.HealthChecks.Abstractions\NetEvolve.HealthChecks.Abstractions.csproj" />
14+
</ItemGroup>
15+
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# NetEvolve.HealthChecks.Azure.Synapse
2+
3+
[![NuGet](https://img.shields.io/nuget/v/NetEvolve.HealthChecks.Azure.Synapse?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/)
4+
[![NuGet](https://img.shields.io/nuget/dt/NetEvolve.HealthChecks.Azure.Synapse?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/)
5+
6+
This package provides a health check for Azure Synapse Analytics, based on the [Azure.Analytics.Synapse.Artifacts](https://www.nuget.org/packages/Azure.Analytics.Synapse.Artifacts/) package. The main purpose is to check that the Azure Synapse workspace is reachable and that the client can connect to it.
7+
8+
:bulb: This package is available for .NET 8.0 and later.
9+
10+
## Installation
11+
To use this package, you need to add the package to your project. You can do this by using the NuGet package manager or by using the dotnet CLI.
12+
```powershell
13+
dotnet add package NetEvolve.HealthChecks.Azure.Synapse
14+
```
15+
16+
## Health Check - Azure Synapse Workspace Availability
17+
The health check is a liveness check. It will check that the Azure Synapse Analytics workspace is reachable and that the client can connect to it. If the service needs longer than the configured timeout to respond, the health check will return `Degraded`. If the service is not reachable, the health check will return `Unhealthy`.
18+
19+
### Usage
20+
After adding the package, yo need to import the namespace `NetEvolve.HealthChecks.Azure.Synapse` and add the health check to the service collection.
21+
```csharp
22+
using NetEvolve.HealthChecks.Azure.Synapse;
23+
```
24+
Therefore, you can use two different approaches. In both approaches you have to provide a name for the health check.
25+
26+
### Parameters
27+
- `name`: The name of the health check. The name is used to identify the configuration object. It is required and must be unique within the application.
28+
- `options`: The configuration options for the health check. If you don't provide any options, the health check will use the configuration based approach.
29+
- `tags`: The tags for the health check. The tags `azure`, `synapse` and `analytics` are always used as default and combined with the user input. You can provide additional tags to group or filter the health checks.
30+
31+
### Variant 1: Configuration based
32+
The first one is to use the configuration based approach. Therefore, you have to add the configuration section `HealthChecks:AzureSynapse` to your `appsettings.json` file.
33+
```csharp
34+
var builder = services.AddHealthChecks();
35+
36+
builder.AddSynapseWorkspaceAvailability("<name>");
37+
```
38+
39+
The configuration looks like this:
40+
```json
41+
{
42+
..., // other configuration
43+
"HealthChecks": {
44+
"AzureSynapse": {
45+
"<name>": {
46+
"ConnectionString": "<connection-string>", // required for ConnectionString mode
47+
"WorkspaceUri": "<workspace-uri>", // required for DefaultAzureCredentials mode
48+
"Mode": "<mode>", // optional, default is ServiceProvider
49+
"Timeout": "<timeout>" // optional, default is 100 milliseconds
50+
}
51+
}
52+
}
53+
}
54+
```
55+
56+
### Variant 2: Options based
57+
The second one is to use the options based approach. Therefore, you have to create an instance of `SynapseWorkspaceAvailableOptions` and provide the configuration.
58+
```csharp
59+
var builder = services.AddHealthChecks();
60+
61+
builder.AddSynapseWorkspaceAvailability("<name>", options =>
62+
{
63+
options.ConnectionString = "<connection-string>";
64+
options.WorkspaceUri = new Uri("<workspace-uri>");
65+
options.Mode = SynapseClientCreationMode.DefaultAzureCredentials;
66+
options.Timeout = "<timeout>";
67+
});
68+
```
69+
70+
### :bulb: You can always provide tags to all health checks, for grouping or filtering.
71+
72+
```csharp
73+
var builder = services.AddHealthChecks();
74+
75+
builder.AddSynapseWorkspaceAvailability("<name>", options => ..., "azure");
76+
```
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace NetEvolve.HealthChecks.Azure.Synapse;
2+
3+
using System;
4+
using global::Azure.Analytics.Synapse.Artifacts;
5+
using global::Azure.Identity;
6+
7+
/// <summary>
8+
/// Describes the mode used to create the <see cref="ArtifactsClient"/>.
9+
/// </summary>
10+
public enum SynapseClientCreationMode
11+
{
12+
/// <summary>
13+
/// The default mode. The <see cref="ArtifactsClient"/> is loading the preregistered instance from the <see cref="IServiceProvider"/>.
14+
/// </summary>
15+
ServiceProvider = 0,
16+
17+
/// <summary>
18+
/// The <see cref="ArtifactsClient"/> is created using the <see cref="DefaultAzureCredential"/>.
19+
/// </summary>
20+
DefaultAzureCredentials = 1,
21+
22+
/// <summary>
23+
/// The <see cref="ArtifactsClient"/> is created using the connection string.
24+
/// </summary>
25+
ConnectionString = 2,
26+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace NetEvolve.HealthChecks.Azure.Synapse;
2+
3+
using System;
4+
using System.Threading;
5+
using global::Azure.Analytics.Synapse.Artifacts;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Options;
9+
using static Microsoft.Extensions.Options.ValidateOptionsResult;
10+
11+
internal sealed class SynapseWorkspaceAvailableConfigure
12+
: IConfigureNamedOptions<SynapseWorkspaceAvailableOptions>,
13+
IValidateOptions<SynapseWorkspaceAvailableOptions>
14+
{
15+
private readonly IConfiguration _configuration;
16+
private readonly IServiceProvider _serviceProvider;
17+
18+
public SynapseWorkspaceAvailableConfigure(IConfiguration configuration, IServiceProvider serviceProvider)
19+
{
20+
_configuration = configuration;
21+
_serviceProvider = serviceProvider;
22+
}
23+
24+
public void Configure(string? name, SynapseWorkspaceAvailableOptions options)
25+
{
26+
ArgumentException.ThrowIfNullOrWhiteSpace(name);
27+
_configuration.Bind($"HealthChecks:AzureSynapse:{name}", options);
28+
}
29+
30+
public void Configure(SynapseWorkspaceAvailableOptions options) => Configure(Options.DefaultName, options);
31+
32+
public ValidateOptionsResult Validate(string? name, SynapseWorkspaceAvailableOptions options)
33+
{
34+
if (string.IsNullOrWhiteSpace(name))
35+
{
36+
return Fail("The name cannot be null or whitespace.");
37+
}
38+
39+
if (options is null)
40+
{
41+
return Fail("The option cannot be null.");
42+
}
43+
44+
if (options.Timeout < Timeout.Infinite)
45+
{
46+
return Fail("The timeout value must be a positive number in milliseconds or -1 for an infinite timeout.");
47+
}
48+
49+
var mode = options.Mode;
50+
51+
return options.Mode switch
52+
{
53+
SynapseClientCreationMode.ServiceProvider => ValidateModeServiceProvider(),
54+
SynapseClientCreationMode.ConnectionString => ValidateModeConnectionString(options),
55+
SynapseClientCreationMode.DefaultAzureCredentials => ValidateModeDefaultAzureCredentials(options),
56+
_ => Fail($"The mode `{mode}` is not supported."),
57+
};
58+
}
59+
60+
private static ValidateOptionsResult ValidateModeDefaultAzureCredentials(SynapseWorkspaceAvailableOptions options)
61+
{
62+
if (options.WorkspaceUri is null)
63+
{
64+
return Fail(
65+
$"The workspace uri cannot be null when using `{nameof(SynapseClientCreationMode.DefaultAzureCredentials)}` mode."
66+
);
67+
}
68+
69+
if (!options.WorkspaceUri.IsAbsoluteUri)
70+
{
71+
return Fail(
72+
$"The workspace uri must be an absolute uri when using `{nameof(SynapseClientCreationMode.DefaultAzureCredentials)}` mode."
73+
);
74+
}
75+
76+
return Success;
77+
}
78+
79+
private static ValidateOptionsResult ValidateModeConnectionString(SynapseWorkspaceAvailableOptions options)
80+
{
81+
if (string.IsNullOrWhiteSpace(options.ConnectionString))
82+
{
83+
return Fail(
84+
$"The connection string cannot be null or whitespace when using `{nameof(SynapseClientCreationMode.ConnectionString)}` mode."
85+
);
86+
}
87+
88+
return Success;
89+
}
90+
91+
private ValidateOptionsResult ValidateModeServiceProvider()
92+
{
93+
if (_serviceProvider.GetService<ArtifactsClient>() is null)
94+
{
95+
return Fail(
96+
$"No service of type `{nameof(ArtifactsClient)}` registered. Please execute `builder.AddAzureClients()`."
97+
);
98+
}
99+
100+
return Success;
101+
}
102+
}

0 commit comments

Comments
 (0)