From 2aad15f22b6a12371b74bffe79832a15769b0705 Mon Sep 17 00:00:00 2001 From: Michael Mairegger Date: Mon, 4 Dec 2023 16:05:45 +0100 Subject: [PATCH] Add file health check similar to folder health check (#2027) --- .../SystemHealthCheckBuilderExtensions.cs | 61 +++++++++++++- src/HealthChecks.System/FileHealthCheck.cs | 41 ++++++++++ .../FileHealthCheckOptions.cs | 19 +++++ .../Functional/FileHealthCheckTests.cs | 80 +++++++++++++++++++ .../HealthChecks.System.approved.txt | 15 ++++ 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/HealthChecks.System/FileHealthCheck.cs create mode 100644 src/HealthChecks.System/FileHealthCheckOptions.cs create mode 100644 test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs diff --git a/src/HealthChecks.System/DependencyInjection/SystemHealthCheckBuilderExtensions.cs b/src/HealthChecks.System/DependencyInjection/SystemHealthCheckBuilderExtensions.cs index 0ee8538000..dd2f3bda52 100644 --- a/src/HealthChecks.System/DependencyInjection/SystemHealthCheckBuilderExtensions.cs +++ b/src/HealthChecks.System/DependencyInjection/SystemHealthCheckBuilderExtensions.cs @@ -16,6 +16,7 @@ public static class SystemHealthCheckBuilderExtensions private const string PROCESS_ALLOCATED_MEMORY = "process_allocated_memory"; private const string WINDOWS_SERVICE_NAME = "windowsservice"; private const string FOLDER_NAME = "folder"; + private const string FILE_NAME = "file"; /// /// Add a health check for disk storage. @@ -248,7 +249,7 @@ public static IHealthChecksBuilder AddWindowsServiceHealthCheck( /// Add a healthcheck that allows to check for the existence of one or more folders. /// /// The . - /// The action method to configure the health check parameters. + /// Delegate for configuring the health check. Optional. /// The health check name. Optional. If null the type name 'folder' will be used for the name. /// /// The that should be reported when the health check fails. Optional. If null then @@ -275,4 +276,62 @@ public static IHealthChecksBuilder AddFolder( tags, timeout)); } + + /// + /// Add a healthcheck that allows to check for the existence of one or more files. + /// + /// The . + /// Delegate for configuring the health check. Optional. + /// The health check name. Optional. If null the type name 'file' 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 specified . + public static IHealthChecksBuilder AddFile( + this IHealthChecksBuilder builder, + Action? setup, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return AddFile(builder, (_, options) => setup?.Invoke(options), name, failureStatus, tags, timeout); + } + + /// + /// Add a healthcheck that allows to check for the existence of one or more files. + /// + /// The . + /// The action method to configure the health check parameters. + /// The health check name. Optional. If null the type name 'file' 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 specified . + public static IHealthChecksBuilder AddFile( + this IHealthChecksBuilder builder, + Action? setup, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + name ?? FILE_NAME, + sp => + { + var options = new FileHealthCheckOptions(); + setup?.Invoke(sp, options); + return new FileHealthCheck(options); + }, + failureStatus, + tags, + timeout)); + } } diff --git a/src/HealthChecks.System/FileHealthCheck.cs b/src/HealthChecks.System/FileHealthCheck.cs new file mode 100644 index 0000000000..ff37a00822 --- /dev/null +++ b/src/HealthChecks.System/FileHealthCheck.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecks.System; + +public class FileHealthCheck : IHealthCheck +{ + private readonly FileHealthCheckOptions _fileOptions; + + public FileHealthCheck(FileHealthCheckOptions fileOptions) + { + _fileOptions = fileOptions; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + List errorList = new(); + foreach (string file in _fileOptions.Files) + { + if (!string.IsNullOrEmpty(file)) + { + if (!File.Exists(file)) + { + errorList.Add($"File {file} does not exist."); + if (!_fileOptions.CheckAllFiles) + { + break; + } + } + } + } + + return Task.FromResult(errorList.GetHealthState(context)); + } + catch (Exception ex) + { + return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, exception: ex)); + } + } +} diff --git a/src/HealthChecks.System/FileHealthCheckOptions.cs b/src/HealthChecks.System/FileHealthCheckOptions.cs new file mode 100644 index 0000000000..d1c70ff7a6 --- /dev/null +++ b/src/HealthChecks.System/FileHealthCheckOptions.cs @@ -0,0 +1,19 @@ +namespace HealthChecks.System; + +public class FileHealthCheckOptions +{ + public List Files { get; } = new(); + public bool CheckAllFiles { get; set; } + + public FileHealthCheckOptions AddFile(string file) + { + Files.Add(file); + return this; + } + + public FileHealthCheckOptions WithCheckAllFiles() + { + CheckAllFiles = true; + return this; + } +} diff --git a/test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs b/test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs new file mode 100644 index 0000000000..423b9eb8e4 --- /dev/null +++ b/test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs @@ -0,0 +1,80 @@ +using System.Net; + +namespace HealthChecks.System.Tests.Functional; + +public class file_healthcheck_should +{ + [Fact] + public async Task be_healthy_if_file_is_available() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddFile(setup => setup.AddFile(Path.Combine(Directory.GetCurrentDirectory(), "HealthChecks.System.Tests.dll")), tags: new string[] { "file" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("file") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync().ConfigureAwait(false); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_healthy_if_no_file_provided() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddFile(setup => + { + }, tags: new string[] { "file" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("file") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync().ConfigureAwait(false); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_unhealthy_if_file_is_not_available() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddFile(setup => setup.AddFile($"{Directory.GetCurrentDirectory()}/non-existing-file"), tags: new string[] { "file" }); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("file") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync().ConfigureAwait(false); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } +} diff --git a/test/HealthChecks.System.Tests/HealthChecks.System.approved.txt b/test/HealthChecks.System.Tests/HealthChecks.System.approved.txt index cb59cc415c..52b1fee702 100644 --- a/test/HealthChecks.System.Tests/HealthChecks.System.approved.txt +++ b/test/HealthChecks.System.Tests/HealthChecks.System.approved.txt @@ -14,6 +14,19 @@ namespace HealthChecks.System public HealthChecks.System.DiskStorageOptions WithCheckAllDrives() { } public delegate string ErrorDescription(string driveName, long minimumFreeMegabytes, long? actualFreeMegabytes); } + public class FileHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck + { + public FileHealthCheck(HealthChecks.System.FileHealthCheckOptions fileOptions) { } + public System.Threading.Tasks.Task CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { } + } + public class FileHealthCheckOptions + { + public FileHealthCheckOptions() { } + public bool CheckAllFiles { get; set; } + public System.Collections.Generic.List Files { get; } + public HealthChecks.System.FileHealthCheckOptions AddFile(string file) { } + public HealthChecks.System.FileHealthCheckOptions WithCheckAllFiles() { } + } public class FolderHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { public FolderHealthCheck(HealthChecks.System.FolderHealthCheckOptions folderOptions) { } @@ -54,6 +67,8 @@ namespace Microsoft.Extensions.DependencyInjection public static class SystemHealthCheckBuilderExtensions { public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddDiskStorageHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action? setup, 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 AddFile(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action? setup, 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 AddFile(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action? setup, 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 AddFolder(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action? setup, 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 AddPrivateMemoryHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, long maximumMemoryBytes, 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 AddProcessAllocatedMemoryHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, int maximumMegabytesAllocated, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { }