Skip to content

Commit cd239e7

Browse files
Copilotsamtrion
andcommitted
feat: Create NetEvolve.HealthChecks.Azure.Kusto project with basic implementation
Co-authored-by: samtrion <[email protected]>
1 parent 9ad32a6 commit cd239e7

File tree

12 files changed

+561
-0
lines changed

12 files changed

+561
-0
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<PackageVersion Include="AWSSDK.SQS" Version="4.0.1.5" />
2727
<PackageVersion Include="Azure.Data.Tables" Version="12.11.0" />
2828
<PackageVersion Include="Azure.Identity" Version="1.16.0" />
29+
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="12.2.3" />
2930
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
3031
<PackageVersion Include="Azure.Storage.Blobs" Version="12.25.1" />
3132
<PackageVersion Include="Azure.Storage.Queues" Version="12.23.0" />
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
namespace NetEvolve.HealthChecks.Azure.Kusto;
2+
3+
using System;
4+
using System.Collections.Concurrent;
5+
using System.Diagnostics;
6+
using Azure.Core;
7+
using Azure.Identity;
8+
using Kusto.Data.Common;
9+
using Kusto.Data.Net.Client;
10+
using Microsoft.Extensions.DependencyInjection;
11+
12+
internal class ClientCreation
13+
{
14+
private ConcurrentDictionary<string, ICslQueryProvider>? _kustoClients;
15+
16+
internal ICslQueryProvider GetKustoClient<TOptions>(string name, TOptions options, IServiceProvider serviceProvider)
17+
where TOptions : class, IKustoOptions
18+
{
19+
if (options.Mode == KustoClientCreationMode.ServiceProvider)
20+
{
21+
return serviceProvider.GetRequiredService<ICslQueryProvider>();
22+
}
23+
24+
if (_kustoClients is null)
25+
{
26+
_kustoClients = new ConcurrentDictionary<string, ICslQueryProvider>(StringComparer.OrdinalIgnoreCase);
27+
}
28+
29+
return _kustoClients.GetOrAdd(name, _ => CreateKustoClient(options, serviceProvider));
30+
}
31+
32+
internal static ICslQueryProvider CreateKustoClient<TOptions>(TOptions options, IServiceProvider serviceProvider)
33+
where TOptions : class, IKustoOptions
34+
{
35+
KustoConnectionStringBuilder connectionStringBuilder;
36+
37+
#pragma warning disable IDE0010 // Add missing cases
38+
switch (options.Mode)
39+
{
40+
case KustoClientCreationMode.DefaultAzureCredentials:
41+
var tokenCredential = serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential();
42+
connectionStringBuilder = new KustoConnectionStringBuilder(
43+
options.ClusterUri!.ToString()
44+
).WithAadUserPromptAuthentication();
45+
break;
46+
case KustoClientCreationMode.ConnectionString:
47+
connectionStringBuilder = new KustoConnectionStringBuilder(options.ConnectionString);
48+
break;
49+
default:
50+
throw new UnreachableException($"Invalid client creation mode `{options.Mode}`.");
51+
}
52+
#pragma warning restore IDE0010 // Add missing cases
53+
54+
if (options.ConfigureConnectionStringBuilder is not null)
55+
{
56+
options.ConfigureConnectionStringBuilder(connectionStringBuilder);
57+
}
58+
59+
return KustoClientFactory.CreateCslQueryProvider(connectionStringBuilder);
60+
}
61+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace NetEvolve.HealthChecks.Azure.Kusto;
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", "kusto", "data", "analytics"];
17+
18+
/// <summary>
19+
/// Adds a health check for Azure Kusto (Data Explorer), to check the cluster connectivity and optionally database availability.
20+
/// </summary>
21+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
22+
/// <param name="name">The name of the <see cref="KustoHealthCheck"/>.</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 AddKusto(
31+
[NotNull] this IHealthChecksBuilder builder,
32+
[NotNull] string name,
33+
Action<KustoOptions>? 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<AzureKustoCheckMarker>())
42+
{
43+
_ = builder
44+
.Services.AddSingleton<AzureKustoCheckMarker>()
45+
.AddSingleton<KustoHealthCheck>()
46+
.ConfigureOptions<KustoConfigure>();
47+
48+
builder.Services.TryAddSingleton<ClientCreation>();
49+
}
50+
51+
builder.ThrowIfNameIsAlreadyUsed<KustoHealthCheck>(name);
52+
53+
if (options is not null)
54+
{
55+
_ = builder.Services.Configure(name, options);
56+
}
57+
58+
return builder.AddCheck<KustoHealthCheck>(
59+
name,
60+
HealthStatus.Unhealthy,
61+
_defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase)
62+
);
63+
}
64+
65+
private sealed partial class AzureKustoCheckMarker;
66+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace NetEvolve.HealthChecks.Azure.Kusto;
2+
3+
using System;
4+
using Kusto.Data.Common;
5+
6+
internal interface IKustoOptions
7+
{
8+
string? ConnectionString { get; }
9+
10+
Uri? ClusterUri { get; }
11+
12+
string? DatabaseName { get; }
13+
14+
KustoClientCreationMode? Mode { get; }
15+
16+
Action<KustoConnectionStringBuilder>? ConfigureConnectionStringBuilder { get; }
17+
18+
int Timeout { get; }
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace NetEvolve.HealthChecks.Azure.Kusto;
2+
3+
using System;
4+
using Kusto.Data.Common;
5+
6+
/// <summary>
7+
/// Describes the mode used to create the Kusto client.
8+
/// </summary>
9+
public enum KustoClientCreationMode
10+
{
11+
/// <summary>
12+
/// The default mode. The Kusto client is loading the preregistered instance from the <see cref="IServiceProvider"/>.
13+
/// </summary>
14+
ServiceProvider = 0,
15+
16+
/// <summary>
17+
/// The Kusto client is created using the <see cref="KustoOptions.ConnectionString"/>.
18+
/// </summary>
19+
ConnectionString = 1,
20+
21+
/// <summary>
22+
/// The Kusto client is created using Azure AD authentication with default credentials.
23+
/// </summary>
24+
DefaultAzureCredentials = 2,
25+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
namespace NetEvolve.HealthChecks.Azure.Kusto;
2+
3+
using System;
4+
using System.Threading;
5+
using Kusto.Data.Net.Client;
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 KustoConfigure : IConfigureNamedOptions<KustoOptions>, IValidateOptions<KustoOptions>
12+
{
13+
private readonly IConfiguration _configuration;
14+
private readonly IServiceProvider _serviceProvider;
15+
16+
public KustoConfigure(IConfiguration configuration, IServiceProvider serviceProvider)
17+
{
18+
_configuration = configuration;
19+
_serviceProvider = serviceProvider;
20+
}
21+
22+
public void Configure(string? name, KustoOptions options)
23+
{
24+
ArgumentException.ThrowIfNullOrWhiteSpace(name);
25+
_configuration.Bind($"HealthChecks:AzureKusto:{name}", options);
26+
}
27+
28+
public void Configure(KustoOptions options) => Configure(Options.DefaultName, options);
29+
30+
public ValidateOptionsResult Validate(string? name, KustoOptions options)
31+
{
32+
if (string.IsNullOrWhiteSpace(name))
33+
{
34+
return Fail("The name cannot be null or whitespace.");
35+
}
36+
37+
if (options is null)
38+
{
39+
return Fail("The option cannot be null.");
40+
}
41+
42+
if (options.Timeout < Timeout.Infinite)
43+
{
44+
return Fail("The timeout value must be a positive number in milliseconds or -1 for an infinite timeout.");
45+
}
46+
47+
var mode = options.Mode;
48+
49+
return options.Mode switch
50+
{
51+
KustoClientCreationMode.ServiceProvider => ValidateModeServiceProvider(),
52+
KustoClientCreationMode.ConnectionString => ValidateModeConnectionString(options),
53+
KustoClientCreationMode.DefaultAzureCredentials => ValidateModeDefaultAzureCredentials(options),
54+
_ => Fail($"The mode `{mode}` is not supported."),
55+
};
56+
}
57+
58+
private static ValidateOptionsResult ValidateModeDefaultAzureCredentials(KustoOptions options)
59+
{
60+
if (options.ClusterUri is null)
61+
{
62+
return Fail(
63+
$"The cluster URI cannot be null when using `{nameof(KustoClientCreationMode.DefaultAzureCredentials)}` mode."
64+
);
65+
}
66+
67+
if (!options.ClusterUri.IsAbsoluteUri)
68+
{
69+
return Fail(
70+
$"The cluster URI must be an absolute URI when using `{nameof(KustoClientCreationMode.DefaultAzureCredentials)}` mode."
71+
);
72+
}
73+
74+
return Success;
75+
}
76+
77+
private static ValidateOptionsResult ValidateModeConnectionString(KustoOptions options)
78+
{
79+
if (string.IsNullOrWhiteSpace(options.ConnectionString))
80+
{
81+
return Fail(
82+
$"The connection string cannot be null or whitespace when using `{nameof(KustoClientCreationMode.ConnectionString)}` mode."
83+
);
84+
}
85+
86+
return Success;
87+
}
88+
89+
private ValidateOptionsResult ValidateModeServiceProvider()
90+
{
91+
if (_serviceProvider.GetService<ICslQueryProvider>() is null)
92+
{
93+
return Fail(
94+
$"No service of type `{nameof(ICslQueryProvider)}` registered. Please register a Kusto client."
95+
);
96+
}
97+
98+
return Success;
99+
}
100+
}

0 commit comments

Comments
 (0)