diff --git a/AspNetCore.Diagnostics.HealthChecks.sln b/AspNetCore.Diagnostics.HealthChecks.sln index 6249bfaf2a..1af84c5a47 100644 --- a/AspNetCore.Diagnostics.HealthChecks.sln +++ b/AspNetCore.Diagnostics.HealthChecks.sln @@ -323,6 +323,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.ClickHouse", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.ClickHouse.Tests", "test\HealthChecks.ClickHouse.Tests\HealthChecks.ClickHouse.Tests.csproj", "{2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.Neo4jClient", "src\HealthChecks.Neo4jClient\HealthChecks.Neo4jClient.csproj", "{3B70557A-BC3F-4CEF-8EAB-C57CE07DCEC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.Neo4jClient.Tests", "test\HealthChecks.Neo4jClient.Tests\HealthChecks.Neo4jClient.Tests.csproj", "{64624A04-52CF-4558-811A-4FDD8E8FD4E6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -905,6 +909,14 @@ Global {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA}.Release|Any CPU.Build.0 = Release|Any CPU + {3B70557A-BC3F-4CEF-8EAB-C57CE07DCEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B70557A-BC3F-4CEF-8EAB-C57CE07DCEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B70557A-BC3F-4CEF-8EAB-C57CE07DCEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B70557A-BC3F-4CEF-8EAB-C57CE07DCEC5}.Release|Any CPU.Build.0 = Release|Any CPU + {64624A04-52CF-4558-811A-4FDD8E8FD4E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64624A04-52CF-4558-811A-4FDD8E8FD4E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64624A04-52CF-4558-811A-4FDD8E8FD4E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64624A04-52CF-4558-811A-4FDD8E8FD4E6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1054,6 +1066,8 @@ Global {44BB97EE-88DB-4C9B-8195-2C6D889AE391} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} {96E2B0A3-02BD-456B-8888-4D96DABA99EB} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} {2FB5CB9F-F870-48DE-BD1D-306AE86A67CA} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} + {3B70557A-BC3F-4CEF-8EAB-C57CE07DCEC5} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} + {64624A04-52CF-4558-811A-4FDD8E8FD4E6} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2B8C62A1-11B6-469F-874C-A02443256568} diff --git a/Directory.Packages.props b/Directory.Packages.props index 9035eadb36..7be64caaf2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,6 +73,7 @@ + @@ -111,10 +112,10 @@ - + - + \ No newline at end of file diff --git a/src/HealthChecks.Neo4jClient/DependencyInjection/Neo4jClientHealthCheckBuilderExtensions.cs b/src/HealthChecks.Neo4jClient/DependencyInjection/Neo4jClientHealthCheckBuilderExtensions.cs new file mode 100644 index 0000000000..38cf85ae23 --- /dev/null +++ b/src/HealthChecks.Neo4jClient/DependencyInjection/Neo4jClientHealthCheckBuilderExtensions.cs @@ -0,0 +1,83 @@ +using HealthChecks.Neo4jClient; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Neo4jClient; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure . +/// +public static class Neo4jClientHealthCheckBuilderExtensions +{ + private const string HEALTH_CHECK_NAME = "neo4j"; + + /// + /// Add a health check for Neo4j databases. + /// + /// The extension for . + /// A factory to build . + /// The health check name. Optional. If null the type name 'neo4j' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddNeo4jClient( + this IHealthChecksBuilder builder, + Func graphClientFactory, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + var healthCheckRegistration = new HealthCheckRegistration( + name ?? HEALTH_CHECK_NAME, + sp => + { + var graphClient = graphClientFactory(sp); + var options = new Neo4jClientHealthCheckOptions(graphClient); + + return new Neo4jClientHealthCheck(options); + }, + failureStatus, + tags, + timeout + ); + + return builder.Add(healthCheckRegistration); + } + + /// + /// Add a health check for Neo4j databases. + /// + /// The extension for . + /// instance for health check. + /// The health check name. Optional. If null the type name 'neo4j' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddNeo4jClient( + this IHealthChecksBuilder builder, + Neo4jClientHealthCheckOptions healthCheckOptions, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + var healthCheckRegistration = new HealthCheckRegistration( + name ?? HEALTH_CHECK_NAME, + _ => new Neo4jClientHealthCheck(healthCheckOptions), + failureStatus, + tags, + timeout + ); + + return builder.Add(healthCheckRegistration); + } +} diff --git a/src/HealthChecks.Neo4jClient/HealthChecks.Neo4jClient.csproj b/src/HealthChecks.Neo4jClient/HealthChecks.Neo4jClient.csproj new file mode 100644 index 0000000000..b4ba232d95 --- /dev/null +++ b/src/HealthChecks.Neo4jClient/HealthChecks.Neo4jClient.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/HealthChecks.Neo4jClient/Neo4jClientHealthCheck.cs b/src/HealthChecks.Neo4jClient/Neo4jClientHealthCheck.cs new file mode 100644 index 0000000000..098c45b3af --- /dev/null +++ b/src/HealthChecks.Neo4jClient/Neo4jClientHealthCheck.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Neo4jClient; + +namespace HealthChecks.Neo4jClient; + +/// +/// A health check for Neo4j databases. +/// +public class Neo4jClientHealthCheck : IHealthCheck +{ + private readonly Neo4jClientHealthCheckOptions _options; + + /// + /// Creates an instance with the options passed to it + /// + public Neo4jClientHealthCheck(Neo4jClientHealthCheckOptions options) + { + _options = Guard.ThrowIfNull(options); + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + _options.GraphClient ??= new BoltGraphClient(new Uri(_options.Host), + _options.Username, + _options.Password, + _options.Realm, + _options.EncryptionLevel, + _options.SerializeNullValues, + _options.UseDriverDataTypes); + + + var graphClient = _options.GraphClient; + + await graphClient.ConnectAsync().ConfigureAwait(false); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy(ex.Message, ex); + } + } +} diff --git a/src/HealthChecks.Neo4jClient/Neo4jClientHealthCheckOptions.cs b/src/HealthChecks.Neo4jClient/Neo4jClientHealthCheckOptions.cs new file mode 100644 index 0000000000..fea46ebda2 --- /dev/null +++ b/src/HealthChecks.Neo4jClient/Neo4jClientHealthCheckOptions.cs @@ -0,0 +1,74 @@ +using Neo4j.Driver; +using Neo4jClient; + +namespace HealthChecks.Neo4jClient; + +/// +/// Options for . +/// +public class Neo4jClientHealthCheckOptions +{ + /// + /// Client for connecting to a database. + /// + public IGraphClient? GraphClient { get; set; } + + /// + /// Host that will be used to connect to the database. + /// + public string? Host { get; set; } + + /// + /// Username that will be used to connect to the database using the . + /// + public string? Username { get; set; } + + /// + /// Password that will be used to connect to the database using the . + /// + public string? Password { get; set; } + + /// + /// Realm that will be used to connect to the database using the . + /// + public string? Realm { get; set; } + + /// + /// Sets the encryption level for connecting to the database + /// + public EncryptionLevel? EncryptionLevel { get; set; } + + /// + /// Sets the encryption level for connecting to the database + /// + public bool SerializeNullValues { get; set; } = false; + + /// + /// Sets the encryption level for connecting to the database + /// + public bool UseDriverDataTypes { get; set; } = false; + + /// + /// Creates instance of . + /// + /// The client for connecting to the database. + public Neo4jClientHealthCheckOptions(IGraphClient graphClient) + { + GraphClient = Guard.ThrowIfNull(graphClient); + } + + /// + /// Creates instance of . + /// + /// Host that will be used to connect to the database. example: bolt://localhost:7687 + /// Username that will be used to connect to the database. + /// Password that will be used to connect to the database. + /// realm that will be used to connect to the database. + public Neo4jClientHealthCheckOptions(string? host, string? username, string? password, string? realm) + { + Host = Guard.ThrowIfNull(host, true); + Username = Guard.ThrowIfNull(username, true); + Password = Guard.ThrowIfNull(password, true); + Realm = realm; + } +} diff --git a/src/HealthChecks.Neo4jClient/README.md b/src/HealthChecks.Neo4jClient/README.md new file mode 100644 index 0000000000..4a8c78872c --- /dev/null +++ b/src/HealthChecks.Neo4jClient/README.md @@ -0,0 +1,24 @@ +# Neo4j Health check + +This health сheck verifies the status of a database in a neo4j DBMS using the BOLT protocol to establish a connection + +This library uses the [Neo4jClient](https://www.nuget.org/packages/Neo4jClient/) package to connect to the database + +# Sample + +Using options class +```csharp +var options = new Neo4jClientHealthCheckOptions("bolt://localhost:7687", "neo4j", "neo4j", realm: null); + +services.AddHealthChecks() + .AddNeo4jClient(options); +``` + +Using client from service provider +```csharp +var graphClient = new BoltGraphClient("bolt://localhost:7687", "neo4j", "neo4j"); +services.AddSingleton(graphClient); + +services.AddHealthChecks() + .AddNeo4jClient(sp => sp.GetRequiredService()); +``` diff --git a/test/HealthChecks.Neo4jClient.Tests/DependencyInjection/RegistrationTests.cs b/test/HealthChecks.Neo4jClient.Tests/DependencyInjection/RegistrationTests.cs new file mode 100644 index 0000000000..e4fbe568cd --- /dev/null +++ b/test/HealthChecks.Neo4jClient.Tests/DependencyInjection/RegistrationTests.cs @@ -0,0 +1,73 @@ +using Neo4jClient; + +namespace HealthChecks.Neo4jClient.Tests.DependencyInjection; + +public class RegistrationTests +{ + [Fact] + public async Task add_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + var boltClient = new BoltGraphClient("bolt://localhost:7687", "neo4j", "neo4j"); + await boltClient.ConnectAsync(); + await boltClient.Cypher + .Create("(a:Test{Name: $param})") + .WithParam("param", "name123") + .ExecuteWithoutResultsAsync(); + + services.AddSingleton(boltClient); + + services.AddHealthChecks() + .AddNeo4jClient(f => f.GetRequiredService()); + + await using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + + registration.Name.ShouldBe("neo4j"); + } + + [Fact] + public async Task add_health_check_when_an_instance_of_bolt_graph_client_is_passed_to_options_class() + { + var services = new ServiceCollection(); + var boltClient = new BoltGraphClient("bolt://localhost:7687", "neo4j", "neo4j"); + await boltClient.ConnectAsync(); + await boltClient.Cypher + .Create("(a:Test{Name: $param})") + .WithParam("param", "name123") + .ExecuteWithoutResultsAsync(); + + services.AddSingleton(boltClient); + + var healthCheckOptions = new Neo4jClientHealthCheckOptions(boltClient); + + services.AddHealthChecks() + .AddNeo4jClient(healthCheckOptions); + + await using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + + registration.Name.ShouldBe("neo4j"); + } + + [Fact] + public async Task add_health_check_when_bolt_graph_client_configured_from_options_class() + { + var services = new ServiceCollection(); + var healthCheckOptions = new Neo4jClientHealthCheckOptions("bolt://localhost:7687", "neo4j", "neo4j", null); + + services.AddHealthChecks() + .AddNeo4jClient(healthCheckOptions); + + await using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + + registration.Name.ShouldBe("neo4j"); + } +} diff --git a/test/HealthChecks.Neo4jClient.Tests/Functional/Neo4jClientHealthCheckTests.cs b/test/HealthChecks.Neo4jClient.Tests/Functional/Neo4jClientHealthCheckTests.cs new file mode 100644 index 0000000000..03e3f2767f --- /dev/null +++ b/test/HealthChecks.Neo4jClient.Tests/Functional/Neo4jClientHealthCheckTests.cs @@ -0,0 +1,33 @@ +using System.Net; +using Neo4jClient; + +namespace HealthChecks.Neo4jClient.Tests.Functional; + +public class Neo4jClientHealthCheckTests +{ + [Fact] + public async Task be_unhealthy_when_username_not_right_with_503_status_code_response_from_test_server() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + var graphClient = new BoltGraphClient("bolt://localhost:7687", "neo4j_should_be_not_right_name", "neo4j"); + services.AddSingleton(graphClient); + + services.AddHealthChecks() + .AddNeo4jClient(_ => _.GetRequiredService(), tags: ["neo4j"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("neo4j") + }); + }); + + using var server = new TestServer(webHostBuilder); + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } +} diff --git a/test/HealthChecks.Neo4jClient.Tests/HealthChecks.Neo4jClient.Tests.csproj b/test/HealthChecks.Neo4jClient.Tests/HealthChecks.Neo4jClient.Tests.csproj new file mode 100644 index 0000000000..71e8df6597 --- /dev/null +++ b/test/HealthChecks.Neo4jClient.Tests/HealthChecks.Neo4jClient.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/HealthChecks.Neo4jClient.Tests/HealthChecks.Neo4jClient.approved.txt b/test/HealthChecks.Neo4jClient.Tests/HealthChecks.Neo4jClient.approved.txt new file mode 100644 index 0000000000..5b829dd65a --- /dev/null +++ b/test/HealthChecks.Neo4jClient.Tests/HealthChecks.Neo4jClient.approved.txt @@ -0,0 +1,29 @@ +namespace HealthChecks.Neo4jClient +{ + public class Neo4jClientHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck + { + public Neo4jClientHealthCheck(HealthChecks.Neo4jClient.Neo4jClientHealthCheckOptions options) { } + public System.Threading.Tasks.Task CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { } + } + public class Neo4jClientHealthCheckOptions + { + public Neo4jClientHealthCheckOptions(Neo4jClient.IGraphClient graphClient) { } + public Neo4jClientHealthCheckOptions(string? host, string? username, string? password, string? realm) { } + public Neo4j.Driver.EncryptionLevel? EncryptionLevel { get; set; } + public Neo4jClient.IGraphClient? GraphClient { get; set; } + public string? Host { get; set; } + public string? Password { get; set; } + public string? Realm { get; set; } + public bool SerializeNullValues { get; set; } + public bool UseDriverDataTypes { get; set; } + public string? Username { get; set; } + } +} +namespace Microsoft.Extensions.DependencyInjection +{ + public static class Neo4jClientHealthCheckBuilderExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddNeo4jClient(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, HealthChecks.Neo4jClient.Neo4jClientHealthCheckOptions healthCheckOptions, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddNeo4jClient(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func graphClientFactory, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } + } +} \ No newline at end of file