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

Replace Nest to Elastic.Clients.Elasticsearch #2244

Merged
merged 13 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
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,54 @@ 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 =>
{
var options = new ElasticsearchOptions();
options.RequestTimeout ??= timeout;
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
options.Client ??= clientFactory?.Invoke(sp) ?? sp.GetRequiredService<Elastic.Clients.Elasticsearch.ElasticsearchClient>();
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
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
Loading