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 Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.19.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.24.0" />
<PackageVersion Include="Azure.Storage.Queues" Version="12.22.0" />
<PackageVersion Include="Azure.Analytics.Synapse.Artifacts" Version="1.0.0-preview.19" />
<PackageVersion Include="ClickHouse.Client" Version="7.14.0" />
<PackageVersion Include="CliWrap" Version="3.8.2" />
<PackageVersion Include="Confluent.Kafka" Version="2.10.0" />
Expand Down
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.Blobs/NetEvolve.HealthChecks.Azure.Blobs.csproj" />
<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.Synapse/NetEvolve.HealthChecks.Azure.Synapse.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Azure.Tables/NetEvolve.HealthChecks.Azure.Tables.csproj" />
<Project Path="src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj" />
<Project Path="src/NetEvolve.HealthChecks.ClickHouse/NetEvolve.HealthChecks.ClickHouse.csproj" />
Expand Down
74 changes: 74 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace NetEvolve.HealthChecks.Azure.Synapse;

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using global::Azure.Analytics.Synapse.Artifacts;
using global::Azure.Identity;
using Microsoft.Extensions.DependencyInjection;

internal class ClientCreation
{
private ConcurrentDictionary<string, ArtifactsClient>? _artifactsClients;

internal ArtifactsClient GetArtifactsClient<TOptions>(
string name,
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : class, ISynapseOptions
{
if (options.Mode == SynapseClientCreationMode.ServiceProvider)
{
return serviceProvider.GetRequiredService<ArtifactsClient>();
}

_artifactsClients ??= new ConcurrentDictionary<string, ArtifactsClient>(StringComparer.OrdinalIgnoreCase);

return _artifactsClients.GetOrAdd(name, _ => CreateArtifactsClient(options, serviceProvider));
}

internal static ArtifactsClient CreateArtifactsClient<TOptions>(
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : class, ISynapseOptions
{
switch (options.Mode)
{
case SynapseClientCreationMode.DefaultAzureCredentials:
var tokenCredential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
return new ArtifactsClient(options.WorkspaceUri, tokenCredential);
case SynapseClientCreationMode.ConnectionString:
// For connection string mode, we extract the workspace URI from the connection string
// and use DefaultAzureCredential for authentication
var workspaceUri = ExtractWorkspaceUriFromConnectionString(options.ConnectionString);
var credential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
return new ArtifactsClient(workspaceUri, credential);
default:
throw new UnreachableException($"Invalid client creation mode `{options.Mode}`.");
}
}

private static Uri ExtractWorkspaceUriFromConnectionString(string? connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString));
}

// Simple connection string parsing for Synapse
// Expected format: "Endpoint=https://myworkspace.dev.azuresynapse.net;..."
var parts = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries);
var endpointPart = parts.FirstOrDefault(p => p.Trim().StartsWith("Endpoint=", StringComparison.OrdinalIgnoreCase));

if (endpointPart is null)
{
throw new ArgumentException("Connection string must contain an 'Endpoint=' parameter.", nameof(connectionString));
}

var endpointValue = endpointPart.Split('=', 2)[1];
return new Uri(endpointValue);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace NetEvolve.HealthChecks.Azure.Synapse;

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
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 = ["azure", "synapse", "analytics"];

/// <summary>
/// Adds a health check for the Azure Synapse Analytics, to check the availability of a workspace.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the <see cref="SynapseWorkspaceAvailableHealthCheck"/>.</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 AddSynapseWorkspaceAvailability(
[NotNull] this IHealthChecksBuilder builder,
[NotNull] string name,
Action<SynapseWorkspaceAvailableOptions>? options = null,
params string[] tags
)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentNullException.ThrowIfNull(tags);

if (!builder.IsServiceTypeRegistered<AzureSynapseWorkspaceCheckMarker>())
{
_ = builder
.Services.AddSingleton<AzureSynapseWorkspaceCheckMarker>()
.AddSingleton<SynapseWorkspaceAvailableHealthCheck>()
.ConfigureOptions<SynapseWorkspaceAvailableConfigure>();

builder.Services.TryAddSingleton<ClientCreation>();
}

builder.ThrowIfNameIsAlreadyUsed<SynapseWorkspaceAvailableHealthCheck>(name);

if (options is not null)
{
_ = builder.Services.Configure(name, options);
}

return builder.AddCheck<SynapseWorkspaceAvailableHealthCheck>(
name,
HealthStatus.Unhealthy,
_defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase)
);
}

private sealed partial class AzureSynapseWorkspaceCheckMarker;
}
14 changes: 14 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.Synapse/ISynapseOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace NetEvolve.HealthChecks.Azure.Synapse;

using System;

internal interface ISynapseOptions
{
Uri? WorkspaceUri { get; }

string? ConnectionString { get; }

SynapseClientCreationMode? Mode { get; }

int Timeout { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(_ProjectTargetFrameworks)</TargetFrameworks>
<Description>Contains HealthChecks for Azure Synapse Analytics.</Description>
<PackageTags>$(PackageTags);azure;synapse;analytics</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Analytics.Synapse.Artifacts" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="NetEvolve.Extensions.Tasks" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NetEvolve.HealthChecks.Abstractions\NetEvolve.HealthChecks.Abstractions.csproj" />
</ItemGroup>
</Project>
76 changes: 76 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.Synapse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# NetEvolve.HealthChecks.Azure.Synapse

[![NuGet](https://img.shields.io/nuget/v/NetEvolve.HealthChecks.Azure.Synapse?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/)
[![NuGet](https://img.shields.io/nuget/dt/NetEvolve.HealthChecks.Azure.Synapse?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/)

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.

:bulb: This package is available for .NET 8.0 and later.

## Installation
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.
```powershell
dotnet add package NetEvolve.HealthChecks.Azure.Synapse
```

## Health Check - Azure Synapse Workspace Availability
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`.

### Usage
After adding the package, yo need to import the namespace `NetEvolve.HealthChecks.Azure.Synapse` and add the health check to the service collection.
```csharp
using NetEvolve.HealthChecks.Azure.Synapse;
```
Therefore, you can use two different approaches. In both approaches you have to provide a name for the health check.

### Parameters
- `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.
- `options`: The configuration options for the health check. If you don't provide any options, the health check will use the configuration based approach.
- `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.

### Variant 1: Configuration based
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.
```csharp
var builder = services.AddHealthChecks();

builder.AddSynapseWorkspaceAvailability("<name>");
```

The configuration looks like this:
```json
{
..., // other configuration
"HealthChecks": {
"AzureSynapse": {
"<name>": {
"ConnectionString": "<connection-string>", // required for ConnectionString mode
"WorkspaceUri": "<workspace-uri>", // required for DefaultAzureCredentials mode
"Mode": "<mode>", // optional, default is ServiceProvider
"Timeout": "<timeout>" // optional, default is 100 milliseconds
}
}
}
}
```

### Variant 2: Options based
The second one is to use the options based approach. Therefore, you have to create an instance of `SynapseWorkspaceAvailableOptions` and provide the configuration.
```csharp
var builder = services.AddHealthChecks();

builder.AddSynapseWorkspaceAvailability("<name>", options =>
{
options.ConnectionString = "<connection-string>";
options.WorkspaceUri = new Uri("<workspace-uri>");
options.Mode = SynapseClientCreationMode.DefaultAzureCredentials;
options.Timeout = "<timeout>";
});
```

### :bulb: You can always provide tags to all health checks, for grouping or filtering.

```csharp
var builder = services.AddHealthChecks();

builder.AddSynapseWorkspaceAvailability("<name>", options => ..., "azure");
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace NetEvolve.HealthChecks.Azure.Synapse;

using System;
using global::Azure.Analytics.Synapse.Artifacts;
using global::Azure.Identity;

/// <summary>
/// Describes the mode used to create the <see cref="ArtifactsClient"/>.
/// </summary>
public enum SynapseClientCreationMode
{
/// <summary>
/// The default mode. The <see cref="ArtifactsClient"/> is loading the preregistered instance from the <see cref="IServiceProvider"/>.
/// </summary>
ServiceProvider = 0,

/// <summary>
/// The <see cref="ArtifactsClient"/> is created using the <see cref="DefaultAzureCredential"/>.
/// </summary>
DefaultAzureCredentials = 1,

/// <summary>
/// The <see cref="ArtifactsClient"/> is created using the connection string.
/// </summary>
ConnectionString = 2,
}
Loading
Loading