Skip to content

Commit

Permalink
Add file health check similar to folder health check (#2027)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelmairegger authored Dec 4, 2023
1 parent 81c6697 commit 2aad15f
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <summary>
/// Add a health check for disk storage.
Expand Down Expand Up @@ -248,7 +249,7 @@ public static IHealthChecksBuilder AddWindowsServiceHealthCheck(
/// Add a healthcheck that allows to check for the existence of one or more folders.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">The action method to configure the health check parameters.</param>
/// <param name="setup">Delegate for configuring the health check. Optional.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'folder' 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
Expand All @@ -275,4 +276,62 @@ public static IHealthChecksBuilder AddFolder(
tags,
timeout));
}

/// <summary>
/// Add a healthcheck that allows to check for the existence of one or more files.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">Delegate for configuring the health check. Optional.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'file' 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 AddFile(
this IHealthChecksBuilder builder,
Action<FileHealthCheckOptions>? setup,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
return AddFile(builder, (_, options) => setup?.Invoke(options), name, failureStatus, tags, timeout);
}

/// <summary>
/// Add a healthcheck that allows to check for the existence of one or more files.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">The action method to configure the health check parameters.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'file' 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 AddFile(
this IHealthChecksBuilder builder,
Action<IServiceProvider, FileHealthCheckOptions>? setup,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? 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));
}
}
41 changes: 41 additions & 0 deletions src/HealthChecks.System/FileHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -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<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
List<string> 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));
}
}
}
19 changes: 19 additions & 0 deletions src/HealthChecks.System/FileHealthCheckOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace HealthChecks.System;

public class FileHealthCheckOptions
{
public List<string> 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;
}
}
80 changes: 80 additions & 0 deletions test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs
Original file line number Diff line number Diff line change
@@ -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);

Check failure on line 26 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

Check failure on line 26 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

Check failure on line 26 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

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);

Check failure on line 52 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

Check failure on line 52 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

Check failure on line 52 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

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);

Check failure on line 76 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

Check failure on line 76 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

Check failure on line 76 in test/HealthChecks.System.Tests/Functional/FileHealthCheckTests.cs

View workflow job for this annotation

GitHub Actions / build / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.

response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable);
}
}
15 changes: 15 additions & 0 deletions test/HealthChecks.System.Tests/HealthChecks.System.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> 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<string> 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) { }
Expand Down Expand Up @@ -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<HealthChecks.System.DiskStorageOptions>? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddFile(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action<HealthChecks.System.FileHealthCheckOptions>? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddFile(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action<System.IServiceProvider, HealthChecks.System.FileHealthCheckOptions>? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddFolder(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action<HealthChecks.System.FolderHealthCheckOptions>? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? 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<string>? 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<string>? tags = null, System.TimeSpan? timeout = default) { }
Expand Down

0 comments on commit 2aad15f

Please sign in to comment.