Skip to content

Commit

Permalink
Replace Nest to Elastic.Clients.Elasticsearch (#2244)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Adam Sitnik <[email protected]>
  • Loading branch information
Alirexaa and adamsitnik committed Jun 28, 2024
1 parent 4a49dfa commit 42c2c73
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public static IHealthChecksBuilder AddElasticsearch(
{
var options = new ElasticsearchOptions();
options.UseServer(elasticsearchUri);

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => new ElasticsearchHealthCheck(options),
Expand Down Expand Up @@ -68,11 +67,58 @@ public static IHealthChecksBuilder AddElasticsearch(

options.RequestTimeout ??= timeout;

if (options.Uri is null && !options.AuthenticateWithElasticCloud)
{
throw new InvalidOperationException($"there is no server to connect. consider using ${nameof(ElasticsearchOptions.UseElasticCloud)} or ${nameof(ElasticsearchOptions.UseServer)}");
}

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => new ElasticsearchHealthCheck(options),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for Elasticsearch databases.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="clientFactory">
/// An optional factory to obtain <see cref="Elastic.Clients.Elasticsearch.ElasticsearchClient" /> instance.
/// When not provided, <see cref="Elastic.Clients.Elasticsearch.ElasticsearchClient" /> is simply resolved from <see cref="IServiceProvider"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'elasticsearch' will be used for the name.</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>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddElasticsearch(
this IHealthChecksBuilder builder,
Func<IServiceProvider, Elastic.Clients.Elasticsearch.ElasticsearchClient>? clientFactory = null,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp =>
{
ElasticsearchOptions options = new()
{
RequestTimeout = timeout,
Client = clientFactory?.Invoke(sp) ?? sp.GetRequiredService<Elastic.Clients.Elasticsearch.ElasticsearchClient>()
};
return new ElasticsearchHealthCheck(options);
},
failureStatus,
tags,
timeout));
}
}
92 changes: 62 additions & 30 deletions src/HealthChecks.Elasticsearch/ElasticsearchHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
using System.Collections.Concurrent;
using Elasticsearch.Net;
using System.Diagnostics;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Nest;

namespace HealthChecks.Elasticsearch;

public class ElasticsearchHealthCheck : IHealthCheck
{
private static readonly ConcurrentDictionary<string, ElasticClient> _connections = new();
private static readonly ConcurrentDictionary<string, ElasticsearchClient> _connections = new();

private readonly ElasticsearchOptions _options;

public ElasticsearchHealthCheck(ElasticsearchOptions options)
{
Debug.Assert(options.Uri is not null || options.Client is not null || options.AuthenticateWithElasticCloud);
_options = Guard.ThrowIfNull(options);
}

Expand All @@ -21,60 +23,90 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
if (!_connections.TryGetValue(_options.Uri, out var lowLevelClient))
ElasticsearchClient? elasticsearchClient = null;
if (_options.Client is not null)
{
var settings = new ConnectionSettings(new Uri(_options.Uri));
elasticsearchClient = _options.Client;
}
else
{
ElasticsearchClientSettings? settings = null;

settings = _options.AuthenticateWithElasticCloud
? new ElasticsearchClientSettings(_options.CloudId!, new ApiKey(_options.ApiKey!))
: new ElasticsearchClientSettings(new Uri(_options.Uri!));


if (_options.RequestTimeout.HasValue)
{
settings = settings.RequestTimeout(_options.RequestTimeout.Value);
}

if (_options.AuthenticateWithBasicCredentials)
if (!_connections.TryGetValue(_options.Uri!, out elasticsearchClient))
{
settings = settings.BasicAuthentication(_options.UserName, _options.Password);
}
else if (_options.AuthenticateWithCertificate)
{
settings = settings.ClientCertificate(_options.Certificate);
}
else if (_options.AuthenticateWithApiKey)
{
settings = settings.ApiKeyAuthentication(_options.ApiKeyAuthenticationCredentials);
}

if (_options.CertificateValidationCallback != null)
{
settings = settings.ServerCertificateValidationCallback(_options.CertificateValidationCallback);
}
if (_options.AuthenticateWithBasicCredentials)
{
if (_options.UserName is null)
{
throw new ArgumentNullException(nameof(_options.UserName));
}
if (_options.Password is null)
{
throw new ArgumentNullException(nameof(_options.Password));
}
settings = settings.Authentication(new BasicAuthentication(_options.UserName, _options.Password));
}
else if (_options.AuthenticateWithCertificate)
{
if (_options.Certificate is null)
{
throw new ArgumentNullException(nameof(_options.Certificate));
}
settings = settings.ClientCertificate(_options.Certificate);
}
else if (_options.AuthenticateWithApiKey)
{
if (_options.ApiKey is null)
{
throw new ArgumentNullException(nameof(_options.ApiKey));
}
settings.Authentication(new ApiKey(_options.ApiKey));
}

lowLevelClient = new ElasticClient(settings);
if (_options.CertificateValidationCallback != null)
{
settings = settings.ServerCertificateValidationCallback(_options.CertificateValidationCallback);
}

if (!_connections.TryAdd(_options.Uri, lowLevelClient))
{
lowLevelClient = _connections[_options.Uri];
elasticsearchClient = new ElasticsearchClient(settings);

if (!_connections.TryAdd(_options.Uri!, elasticsearchClient))
{
elasticsearchClient = _connections[_options.Uri!];
}
}
}

if (_options.UseClusterHealthApi)
{
var healthResponse = await lowLevelClient.Cluster.HealthAsync(ct: cancellationToken).ConfigureAwait(false);
var healthResponse = await elasticsearchClient.Cluster.HealthAsync(cancellationToken: cancellationToken).ConfigureAwait(false);

if (healthResponse.ApiCall.HttpStatusCode != 200)
if (healthResponse.ApiCallDetails.HttpStatusCode != 200)
{
return new HealthCheckResult(context.Registration.FailureStatus);
}

return healthResponse.Status switch
{
Health.Green => HealthCheckResult.Healthy(),
Health.Yellow => HealthCheckResult.Degraded(),
Elastic.Clients.Elasticsearch.HealthStatus.Green => HealthCheckResult.Healthy(),
Elastic.Clients.Elasticsearch.HealthStatus.Yellow => HealthCheckResult.Degraded(),
_ => new HealthCheckResult(context.Registration.FailureStatus)
};
}

var pingResult = await lowLevelClient.PingAsync(ct: cancellationToken).ConfigureAwait(false);
bool isSuccess = pingResult.ApiCall.HttpStatusCode == 200;
var pingResult = await elasticsearchClient.PingAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
bool isSuccess = pingResult.ApiCallDetails.HttpStatusCode == 200;

return isSuccess
? HealthCheckResult.Healthy()
Expand Down
45 changes: 39 additions & 6 deletions src/HealthChecks.Elasticsearch/ElasticsearchOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Elasticsearch.Net;
using Elastic.Clients.Elasticsearch;

namespace HealthChecks.Elasticsearch;

Expand All @@ -9,36 +9,47 @@ namespace HealthChecks.Elasticsearch;
/// </summary>
public class ElasticsearchOptions
{
public string Uri { get; private set; } = null!;
public string? Uri { get; private set; }

public string? UserName { get; private set; }

public string? Password { get; private set; }

public X509Certificate? Certificate { get; private set; }
public string? CloudId { get; private set; }

public string? CloudApiKey { get; private set; }

public ApiKeyAuthenticationCredentials? ApiKeyAuthenticationCredentials { get; private set; }
public X509Certificate? Certificate { get; private set; }

public bool AuthenticateWithBasicCredentials { get; private set; }

public bool AuthenticateWithCertificate { get; private set; }

public bool AuthenticateWithApiKey { get; private set; }

public bool AuthenticateWithElasticCloud { get; private set; }

public bool UseClusterHealthApi { get; set; }

public string? ApiKey { get; private set; }

public Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool>? CertificateValidationCallback { get; private set; }

public TimeSpan? RequestTimeout { get; set; }

public ElasticsearchClient? Client { get; internal set; }

public ElasticsearchOptions UseBasicAuthentication(string name, string password)
{
UserName = Guard.ThrowIfNull(name);
Password = Guard.ThrowIfNull(password);

CloudId = string.Empty;
CloudApiKey = string.Empty;
Certificate = null;
AuthenticateWithApiKey = false;
AuthenticateWithCertificate = false;
AuthenticateWithElasticCloud = false;
AuthenticateWithBasicCredentials = true;
return this;
}
Expand All @@ -49,26 +60,48 @@ public ElasticsearchOptions UseCertificate(X509Certificate certificate)

UserName = string.Empty;
Password = string.Empty;
CloudId = string.Empty;
CloudApiKey = string.Empty;
AuthenticateWithApiKey = false;
AuthenticateWithBasicCredentials = false;
AuthenticateWithElasticCloud = false;
AuthenticateWithCertificate = true;
return this;
}

public ElasticsearchOptions UseApiKey(ApiKeyAuthenticationCredentials apiKey)
public ElasticsearchOptions UseApiKey(string apiKey)
{
ApiKeyAuthenticationCredentials = Guard.ThrowIfNull(apiKey);
ApiKey = Guard.ThrowIfNull(apiKey);

UserName = string.Empty;
Password = string.Empty;
CloudId = string.Empty;
CloudApiKey = string.Empty;
Certificate = null;
AuthenticateWithBasicCredentials = false;
AuthenticateWithCertificate = false;
AuthenticateWithElasticCloud = false;
AuthenticateWithApiKey = true;

return this;
}

public ElasticsearchOptions UseElasticCloud(string cloudId, string cloudApiKey)
{
CloudId = Guard.ThrowIfNull(cloudId);
CloudApiKey = Guard.ThrowIfNull(cloudApiKey);

UserName = string.Empty;
Password = string.Empty;
Certificate = null;
AuthenticateWithBasicCredentials = false;
AuthenticateWithCertificate = false;
AuthenticateWithApiKey = false;
AuthenticateWithElasticCloud = true;
return this;
}


public ElasticsearchOptions UseServer(string uri)
{
Uri = Guard.ThrowIfNull(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NEST" Version="7.17.5" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.14.3" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.6" />

</ItemGroup>

</Project>
Loading

0 comments on commit 42c2c73

Please sign in to comment.