diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ApplicationLifecycleHealthCheck.cs similarity index 77% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ApplicationLifecycleHealthCheck.cs index 283202e704d..d80b30523ae 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ApplicationLifecycleHealthCheck.cs @@ -14,7 +14,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; internal sealed class ApplicationLifecycleHealthCheck : IHealthCheck { private static readonly Task _healthy = Task.FromResult(HealthCheckResult.Healthy()); - private static readonly Task _unhealthy = Task.FromResult(HealthCheckResult.Unhealthy()); + private static readonly Task _unhealthyNotStarted = Task.FromResult(HealthCheckResult.Unhealthy("Not Started")); + private static readonly Task _unhealthyStopping = Task.FromResult(HealthCheckResult.Unhealthy("Stopping")); + private static readonly Task _unhealthyStopped = Task.FromResult(HealthCheckResult.Unhealthy("Stopped")); private readonly IHostApplicationLifetime _appLifetime; /// @@ -42,9 +44,23 @@ public ApplicationLifecycleHealthCheck(IHostApplicationLifetime appLifetime) public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { bool isStarted = _appLifetime.ApplicationStarted.IsCancellationRequested; + if (!isStarted) + { + return _unhealthyNotStarted; + } + bool isStopping = _appLifetime.ApplicationStopping.IsCancellationRequested; + if (isStopping) + { + return _unhealthyStopping; + } + bool isStopped = _appLifetime.ApplicationStopped.IsCancellationRequested; - bool isHealthy = isStarted && !isStopping && !isStopped; - return isHealthy ? _healthy : _unhealthy; + if (isStopped) + { + return _unhealthyStopped; + } + + return _healthy; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.ApplicationLifecycle.cs similarity index 67% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.ApplicationLifecycle.cs index 2051ec264f7..89aec5d6439 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.ApplicationLifecycle.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Shared.Diagnostics; @@ -12,28 +11,17 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; /// /// Controls various health check features. /// -public static partial class CoreHealthChecksExtensions +public static partial class CommonHealthChecksExtensions { - /// - /// Registers a health check provider that's tied to the application's lifecycle. - /// - /// The builder to add the provider to. - /// The value of . - /// If is . - public static IHealthChecksBuilder AddApplicationLifecycleHealthCheck(this IHealthChecksBuilder builder) - => Throw.IfNull(builder) - .AddCheck("ApplicationLifecycleHealthCheck"); - /// /// Registers a health check provider that's tied to the application's lifecycle. /// /// The builder to add the provider to. /// A list of tags that can be used to filter health checks. /// The value of . - [Experimental] public static IHealthChecksBuilder AddApplicationLifecycleHealthCheck(this IHealthChecksBuilder builder, params string[] tags) => Throw.IfNull(builder) - .AddCheck("ApplicationLifecycleHealthCheck", tags: Throw.IfNull(tags)); + .AddCheck("ApplicationLifecycle", tags: Throw.IfNull(tags)); /// /// Registers a health check provider that's tied to the application's lifecycle. @@ -44,5 +32,5 @@ public static IHealthChecksBuilder AddApplicationLifecycleHealthCheck(this IHeal /// If or are . public static IHealthChecksBuilder AddApplicationLifecycleHealthCheck(this IHealthChecksBuilder builder, IEnumerable tags) => Throw.IfNull(builder) - .AddCheck("ApplicationLifecycleHealthCheck", tags: Throw.IfNull(tags)); + .AddCheck("ApplicationLifecycle", tags: Throw.IfNull(tags)); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.Manual.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.Manual.cs index f65759864df..4e87ee9690a 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.Manual.cs @@ -8,19 +8,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; -public static partial class CoreHealthChecksExtensions +public static partial class CommonHealthChecksExtensions { - /// - /// Registers a health check provider that enables manual control of the application's health. - /// - /// The builder to add the provider to. - /// The value of . - /// If is . - public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilder builder) - => Throw.IfNull(builder) - .AddManualHealthCheckDependencies() - .AddCheck("ManualHealthCheck"); - /// /// Registers a health check provider that enables manual control of the application's health. /// @@ -31,7 +20,7 @@ public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilde public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilder builder, params string[] tags) => Throw.IfNull(builder) .AddManualHealthCheckDependencies() - .AddCheck("ManualHealthCheck", tags: Throw.IfNull(tags)); + .AddCheck("Manual", tags: Throw.IfNull(tags)); /// /// Registers a health check provider that enables manual control of the application's health. @@ -43,7 +32,7 @@ public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilde public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilder builder, IEnumerable tags) => Throw.IfNull(builder) .AddManualHealthCheckDependencies() - .AddCheck("ManualHealthCheck", tags: Throw.IfNull(tags)); + .AddCheck("Manual", tags: Throw.IfNull(tags)); /// /// Sets the manual health check to the healthy state. @@ -63,8 +52,8 @@ public static void ReportUnhealthy(this IManualHealthCheck manualHealthCheck, st => Throw.IfNull(manualHealthCheck).Result = HealthCheckResult.Unhealthy(Throw.IfNullOrWhitespace(reason)); private static IHealthChecksBuilder AddManualHealthCheckDependencies(this IHealthChecksBuilder builder) - => builder - .Services.AddSingleton() + => builder.Services + .AddSingleton() .AddTransient(typeof(IManualHealthCheck<>), typeof(ManualHealthCheck<>)) .AddHealthChecks(); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.TelemetryPublisher.cs similarity index 98% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.TelemetryPublisher.cs index 0363baa0ef7..f1dc9db7433 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/CommonHealthChecksExtensions.TelemetryPublisher.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; -public static partial class CoreHealthChecksExtensions +public static partial class CommonHealthChecksExtensions { /// /// Registers a health check publisher which emits telemetry representing the application's health. diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/IManualHealthCheck.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheck.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/IManualHealthCheck.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Log.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Log.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Log.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs similarity index 91% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs index cfdf5d39d36..0302b1a896f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheck.cs @@ -30,10 +30,10 @@ public HealthCheckResult Result } } - private readonly IManualHealthCheckTracker _tracker; + private readonly ManualHealthCheckTracker _tracker; [SuppressMessage("Major Code Smell", "S3366:\"this\" should not be exposed from constructors", Justification = "It's OK, just registering into a list")] - public ManualHealthCheck(IManualHealthCheckTracker tracker) + public ManualHealthCheck(ManualHealthCheckTracker tracker) { Result = HealthCheckResult.Unhealthy("Initial state"); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheckService.cs similarity index 92% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheckService.cs index 24a08f4bf35..34cb8b4f3f8 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheckService.cs @@ -14,9 +14,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; /// internal sealed class ManualHealthCheckService : IHealthCheck { - private readonly IManualHealthCheckTracker _tracker; + private readonly ManualHealthCheckTracker _tracker; - public ManualHealthCheckService(IManualHealthCheckTracker tracker) + public ManualHealthCheckService(ManualHealthCheckTracker tracker) { _tracker = tracker; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheckTracker.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheckTracker.cs index 63e653c2672..ec4812ce5e1 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/ManualHealthCheckTracker.cs @@ -8,13 +8,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; -internal sealed class ManualHealthCheckTracker : IManualHealthCheckTracker +internal sealed class ManualHealthCheckTracker { private static readonly HealthCheckResult _healthy = HealthCheckResult.Healthy(); private readonly ConcurrentDictionary _checks = new(); - /// public void Register(IManualHealthCheck check) { _ = _checks.AddOrUpdate(check, true, (_, _) => true); @@ -25,7 +24,6 @@ public void Unregister(IManualHealthCheck checkToRemove) _ = _checks.TryRemove(checkToRemove, out _); } - /// public HealthCheckResult GetHealthCheckResult() { // Construct string showing all reasons for unhealthy manual health checks diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Metric.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Metric.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Metric.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Metric.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Microsoft.Extensions.Diagnostics.HealthChecks.Common.csproj similarity index 100% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.csproj rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Microsoft.Extensions.Diagnostics.HealthChecks.Common.csproj diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Microsoft.Extensions.Diagnostics.HealthChecks.Common.json b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Microsoft.Extensions.Diagnostics.HealthChecks.Common.json new file mode 100644 index 00000000000..fec01235911 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Microsoft.Extensions.Diagnostics.HealthChecks.Common.json @@ -0,0 +1,77 @@ +{ + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.Common, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Types": [ + { + "Type": "static class Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions", + "Stage": "Stable", + "Methods": [ + { + "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddApplicationLifecycleHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, params string[] tags);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddApplicationLifecycleHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Collections.Generic.IEnumerable tags);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddManualHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, params string[] tags);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddManualHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Collections.Generic.IEnumerable tags);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddTelemetryHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services);", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddTelemetryHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfigurationSection section);", + "Stage": "Experimental" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.AddTelemetryHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure);", + "Stage": "Experimental" + }, + { + "Member": "static void Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.ReportHealthy(this Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck manualHealthCheck);", + "Stage": "Stable" + }, + { + "Member": "static void Microsoft.Extensions.Diagnostics.HealthChecks.CommonHealthChecksExtensions.ReportUnhealthy(this Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck manualHealthCheck, string reason);", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck : System.IDisposable", + "Stage": "Stable", + "Properties": [ + { + "Member": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck.Result { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "interface Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck, System.IDisposable", + "Stage": "Stable" + }, + { + "Type": "class Microsoft.Extensions.Diagnostics.HealthChecks.TelemetryHealthCheckPublisherOptions", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.HealthChecks.TelemetryHealthCheckPublisherOptions.TelemetryHealthCheckPublisherOptions();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "bool Microsoft.Extensions.Diagnostics.HealthChecks.TelemetryHealthCheckPublisherOptions.LogOnlyUnhealthy { get; set; }", + "Stage": "Experimental" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/TelemetryHealthCheckPublisher.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisher.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/TelemetryHealthCheckPublisher.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisherOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/TelemetryHealthCheckPublisherOptions.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisherOptions.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/TelemetryHealthCheckPublisherOptions.cs index c4e8da6c921..7a82c5efc2e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisherOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/TelemetryHealthCheckPublisherOptions.cs @@ -17,6 +17,5 @@ public class TelemetryHealthCheckPublisherOptions /// /// Default set to false. /// - [Experimental] public bool LogOnlyUnhealthy { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs deleted file mode 100644 index 6316f1d98b3..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks; - -public static partial class CoreHealthChecksExtensions -{ - /// - /// Registers a health status publisher which opens a TCP port if the application is considered healthy. - /// - /// The to add the publisher to. - /// The value of . - /// If is . - public static IServiceCollection AddKubernetesHealthCheckPublisher(this IServiceCollection services) - { - _ = Throw.IfNull(services); - - return services.AddSingleton(); - } - - /// - /// Registers a health status publisher which opens a TCP port if the application is considered healthy. - /// - /// The to add the publisher to. - /// Configuration for . - /// The value of . - /// If or are . - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(KubernetesHealthCheckPublisherOptions))] - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "Addressed by [DynamicDependency]")] - public static IServiceCollection AddKubernetesHealthCheckPublisher( - this IServiceCollection services, - IConfigurationSection section) - { - _ = Throw.IfNull(services); - _ = Throw.IfNull(section); - - return services - .Configure(section) - .AddKubernetesHealthCheckPublisher(); - } - - /// - /// Registers a health status publisher which opens a TCP port if the application is considered healthy. - /// - /// The to add the publisher to. - /// Configuration for . - /// The value of . - /// If or are . - public static IServiceCollection AddKubernetesHealthCheckPublisher( - this IServiceCollection services, - Action configure) - { - _ = Throw.IfNull(services); - _ = Throw.IfNull(configure); - - return services - .Configure(configure) - .AddKubernetesHealthCheckPublisher(); - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs deleted file mode 100644 index 6fc00cacc72..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks; - -/// -/// A helper to track all instances of IManualHealthCheck registered in the application. -/// -internal interface IManualHealthCheckTracker -{ - /// - /// Registers a new IManualHealthCheck into the tracker. - /// - /// The manual health check to be added. - void Register(IManualHealthCheck check); - - /// - /// Removes a IManualHealthCheck from the tracker. - /// - /// The manual health check to be removed. - void Unregister(IManualHealthCheck checkToRemove); - - /// - /// Gets the HealthCheckResult generated from the registered list of IManualHealthCheck. - /// - /// - /// A containing the HealthCheckResult generated. - /// - HealthCheckResult GetHealthCheckResult(); -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs deleted file mode 100644 index 3966d1407a4..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks; - -/// -/// Opens a TCP port if the service is healthy and closes it otherwise. -/// -internal sealed class KubernetesHealthCheckPublisher : IHealthCheckPublisher -{ - private readonly TcpListener _server; - private readonly int _maxLengthOfPendingConnectionsQueue; - - /// - /// Initializes a new instance of the class. - /// - /// Creation options. - public KubernetesHealthCheckPublisher(IOptions options) - { - var value = Throw.IfMemberNull(options, options.Value); - - _server = new TcpListener(IPAddress.Any, value.TcpPort); - _maxLengthOfPendingConnectionsQueue = value.MaxPendingConnections; - } - - /// - /// Publishes the provided report. - /// - /// The . The result of executing a set of health checks. - /// Not used in the current implementation. - /// Task.CompletedTask. - public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) - { - _ = Throw.IfNull(report); - - if (report.Status == HealthStatus.Healthy) - { - if (!_server.Server.IsBound) - { - _server.Start(_maxLengthOfPendingConnectionsQueue); - _ = Task.Run(() => TcpServerAsync(), CancellationToken.None); - } - } - else - { - _server.Stop(); - } - - return Task.CompletedTask; - } - - [SuppressMessage("Blocker Bug", "S2190:Recursion should not be infinite", Justification = "runs in background")] - [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = "runs in background")] - private async Task TcpServerAsync() - { - while (true) - { - using var client = await _server.AcceptTcpClientAsync().ConfigureAwait(false); - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs deleted file mode 100644 index b3d1fbe4f38..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks; - -/// -/// Options to control the Kubernetes health status publisher. -/// -[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "In place numbers make the ranges cleaner")] -public class KubernetesHealthCheckPublisherOptions -{ - private const int DefaultMaxPendingConnections = 10; - private const int DefaultTcpPort = 2305; - - /// - /// Gets or sets the TCP port which gets opened if the application is healthy and closed otherwise. - /// - /// - /// Default set to 2305. - /// - [Range(1, 65535)] - public int TcpPort { get; set; } = DefaultTcpPort; - - /// - /// Gets or sets the maximum length of the pending connections queue. - /// - /// - /// Default set to 10. - /// - [Range(1, 10000)] - public int MaxPendingConnections { get; set; } = DefaultMaxPendingConnections; -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.json b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.json deleted file mode 100644 index c5d83b796c7..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", - "Types": [ - { - "Type": "static class Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions", - "Stage": "Stable", - "Methods": [ - { - "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddApplicationLifecycleHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddApplicationLifecycleHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, params string[] tags);", - "Stage": "Experimental" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddApplicationLifecycleHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Collections.Generic.IEnumerable tags);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddKubernetesHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddKubernetesHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfigurationSection section);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddKubernetesHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddManualHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddManualHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, params string[] tags);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddManualHealthCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Collections.Generic.IEnumerable tags);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddTelemetryHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services);", - "Stage": "Stable" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddTelemetryHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfigurationSection section);", - "Stage": "Experimental" - }, - { - "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.AddTelemetryHealthCheckPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure);", - "Stage": "Experimental" - }, - { - "Member": "static void Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.ReportHealthy(this Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck manualHealthCheck);", - "Stage": "Stable" - }, - { - "Member": "static void Microsoft.Extensions.Diagnostics.HealthChecks.CoreHealthChecksExtensions.ReportUnhealthy(this Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck manualHealthCheck, string reason);", - "Stage": "Stable" - } - ] - }, - { - "Type": "interface Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck : System.IDisposable", - "Stage": "Stable", - "Properties": [ - { - "Member": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck.Result { get; set; }", - "Stage": "Stable" - } - ] - }, - { - "Type": "interface Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IManualHealthCheck, System.IDisposable", - "Stage": "Stable" - }, - { - "Type": "class Microsoft.Extensions.Diagnostics.HealthChecks.KubernetesHealthCheckPublisherOptions", - "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.Diagnostics.HealthChecks.KubernetesHealthCheckPublisherOptions.KubernetesHealthCheckPublisherOptions();", - "Stage": "Stable" - } - ], - "Properties": [ - { - "Member": "int Microsoft.Extensions.Diagnostics.HealthChecks.KubernetesHealthCheckPublisherOptions.MaxPendingConnections { get; set; }", - "Stage": "Stable" - }, - { - "Member": "int Microsoft.Extensions.Diagnostics.HealthChecks.KubernetesHealthCheckPublisherOptions.TcpPort { get; set; }", - "Stage": "Stable" - } - ] - }, - { - "Type": "class Microsoft.Extensions.Diagnostics.HealthChecks.TelemetryHealthCheckPublisherOptions", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.Diagnostics.HealthChecks.TelemetryHealthCheckPublisherOptions.TelemetryHealthCheckPublisherOptions();", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "bool Microsoft.Extensions.Diagnostics.HealthChecks.TelemetryHealthCheckPublisherOptions.LogOnlyUnhealthy { get; set; }", - "Stage": "Experimental" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckExtensions.cs index a35b643a5a8..b131402940c 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckExtensions.cs @@ -18,20 +18,6 @@ public static class ResourceUtilizationHealthCheckExtensions { private const string HealthCheckName = "container resources"; - /// - /// Registers a health check provider that monitors resource utilization to assess the application's health. - /// - /// The builder to add the provider to. - /// The value of . - /// If is . - public static IHealthChecksBuilder AddResourceUtilizationHealthCheck(this IHealthChecksBuilder builder) - { - _ = Throw.IfNull(builder); - - _ = builder.Services.AddValidatedOptions(); - return builder.AddCheck(HealthCheckName); - } - /// /// Registers a health check provider that monitors resource utilization to assess the application's health. /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesExtensions.cs new file mode 100644 index 00000000000..e77cc7ddf09 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesExtensions.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +/// +/// Extensions for setting up probes for Kubernetes. +/// +public static class KubernetesProbesExtensions +{ + /// + /// Registers liveness, startup and readiness probes using the default options. + /// + /// The dependency injection container to add the probe to. + /// The value of . + public static IServiceCollection AddKubernetesProbes(this IServiceCollection services) + => services.AddKubernetesProbes((_) => { }); + + /// + /// Registers liveness, startup and readiness probes using the configured options. + /// + /// The dependency injection container to add the probe to. + /// Configuration for . + /// The value of . + public static IServiceCollection AddKubernetesProbes(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services.AddKubernetesProbes(section.Bind); + } + + /// + /// Registers liveness, startup and readiness probes using the configured options. + /// + /// The dependency injection container to add the probe to. + /// Configure action for . + /// The value of . + public static IServiceCollection AddKubernetesProbes(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + var wrapperOptions = new KubernetesProbesOptions(); + + return services + .AddTcpEndpointHealthCheck(ProbeTags.Liveness, options => + { + wrapperOptions.LivenessProbe = options; + configure(wrapperOptions); + var originalPredicate = options.FilterChecks; + if (originalPredicate == null) + { + options.FilterChecks = (check) => check.Tags.Contains(ProbeTags.Liveness); + } + else + { + options.FilterChecks = (check) => check.Tags.Contains(ProbeTags.Liveness) && originalPredicate(check); + } + }) + .AddTcpEndpointHealthCheck(ProbeTags.Startup, options => + { + wrapperOptions.StartupProbe = options; + configure(wrapperOptions); + var originalPredicate = options.FilterChecks; + if (originalPredicate == null) + { + options.FilterChecks = (check) => check.Tags.Contains(ProbeTags.Startup); + } + else + { + options.FilterChecks = (check) => check.Tags.Contains(ProbeTags.Startup) && originalPredicate(check); + } + }) + .AddTcpEndpointHealthCheck(ProbeTags.Readiness, (options) => + { + wrapperOptions.ReadinessProbe = options; + configure(wrapperOptions); + var originalPredicate = options.FilterChecks; + if (originalPredicate == null) + { + options.FilterChecks = (check) => check.Tags.Contains(ProbeTags.Readiness); + } + else + { + options.FilterChecks = (check) => check.Tags.Contains(ProbeTags.Readiness) && originalPredicate(check); + } + }); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesOptions.EndpointOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesOptions.EndpointOptions.cs new file mode 100644 index 00000000000..516483221b9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesOptions.EndpointOptions.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +public partial class KubernetesProbesOptions +{ + /// + /// Options to control TCP-based health check probes. + /// + [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "In place numbers make the ranges cleaner")] + [SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "It's fine")] + public class EndpointOptions + { + private const int DefaultMaxPendingConnections = 10; + private const int DefaultTcpPort = 2305; + + /// + /// Gets or sets the TCP port which gets opened if service is healthy and closed otherwise. + /// + /// + /// Default set to 2305. + /// + [Range(1, 65535)] + public int TcpPort { get; set; } = DefaultTcpPort; + + /// + /// Gets or sets the maximum length of the pending connections queue. + /// + /// + /// Default set to 10. + /// + [Range(1, 10000)] + public int MaxPendingConnections { get; set; } = DefaultMaxPendingConnections; + + /// + /// Gets or sets the interval at which the health of the application is assessed. + /// + /// + /// Default set to 30 seconds. + /// + [TimeSpan("00:00:05", "00:05:00")] + public TimeSpan Period { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets a predicate that is used to include health checks based on user-defined criteria. + /// + /// + /// Default set to which has the effect of enabling all health checks. + /// + public Func? FilterChecks { get; set; } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesOptions.cs new file mode 100644 index 00000000000..59f7fbed4df --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/KubernetesProbesOptions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Probes; + +/// +/// Options for Kubernetes probes. +/// +public partial class KubernetesProbesOptions +{ + private const int DefaultLivenessProbePort = 2305; + private const int DefaultStartupProbePort = 2306; + private const int DefaultReadinessProbePort = 2307; + + /// + /// Gets or sets the options for the liveness probe. + /// + /// + /// Default port is 2305. + /// + public EndpointOptions LivenessProbe { get; set; } = new EndpointOptions + { + TcpPort = DefaultLivenessProbePort, + }; + + /// + /// Gets or sets the options for the startup probe. + /// + /// + /// Default port is 2306. + /// + public EndpointOptions StartupProbe { get; set; } = new EndpointOptions + { + TcpPort = DefaultStartupProbePort, + }; + + /// + /// Gets or sets the options for the readiness probe. + /// + /// + /// Default port is 2307. + /// + public EndpointOptions ReadinessProbe { get; set; } = new EndpointOptions + { + TcpPort = DefaultReadinessProbePort, + }; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/Log.cs new file mode 100644 index 00000000000..e61f2b5dea9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/Log.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +internal static partial class Log +{ + [LogMethod(LogLevel.Error, "Error updating health status through TCP endpoint")] + public static partial void SocketExceptionCaughtTcpEndpoint(this ILogger logger, Exception e); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckExtensions.cs new file mode 100644 index 00000000000..1188325e569 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckExtensions.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +/// +/// Extension methods for for . +/// +internal static class TcpEndpointHealthCheckExtensions +{ + /// + /// Registers health status reporting using a TCP port + /// if service is considered as healthy . + /// + /// The to add the services to. + /// The same instance of the for chaining. + internal static IServiceCollection AddTcpEndpointHealthCheck(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services.AddTcpEndpointHealthCheck(Microsoft.Extensions.Options.Options.DefaultName); + } + + /// + /// Registers health status reporting using a TCP port + /// if service is considered as healthy . + /// + /// The to add the services to. + /// Name used to retrieve the options. + /// The same instance of the for chaining. + internal static IServiceCollection AddTcpEndpointHealthCheck(this IServiceCollection services, string name) + { + _ = Throw.IfNull(services); + + _ = services.AddHealthChecks(); + + _ = services + .AddValidatedOptions(name); + + _ = services.AddSingleton(provider => + { + var options = provider.GetRequiredService>().Get(name); + return ActivatorUtilities.CreateInstance(provider, options); + }); + + return services; + } + + /// + /// Registers health status reporting using a TCP port + /// if service is considered as healthy . + /// + /// The to add the services to. + /// Configuration for . + /// The same instance of the for chaining. + internal static IServiceCollection AddTcpEndpointHealthCheck( + this IServiceCollection services, + Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.Configure(configure); + + return services.AddTcpEndpointHealthCheck(); + } + + /// + /// Registers health status reporting using a TCP port + /// if service is considered as healthy . + /// + /// The to add the services to. + /// Name for the options. + /// Configuration for . + /// The same instance of the for chaining. + internal static IServiceCollection AddTcpEndpointHealthCheck( + this IServiceCollection services, + string name, + Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.Configure(name, configure); + + return services.AddTcpEndpointHealthCheck(name); + } + + /// + /// Registers health status reporting using a TCP port + /// if service is considered as healthy . + /// + /// The to add the services to. + /// Configuration for . + /// The same instance of the for chaining. + internal static IServiceCollection AddTcpEndpointHealthCheck( + this IServiceCollection services, + IConfigurationSection configurationSection) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configurationSection); + + _ = services.Configure(configurationSection); + + return services.AddTcpEndpointHealthCheck(); + } + + /// + /// Registers health status reporting using a TCP port + /// if service is considered as healthy . + /// + /// The to add the services to. + /// Name for the options. + /// Configuration for . + /// The same instance of the for chaining. + internal static IServiceCollection AddTcpEndpointHealthCheck( + this IServiceCollection services, + string name, + IConfigurationSection configurationSection) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configurationSection); + + _ = services.Configure(name, configurationSection); + + return services.AddTcpEndpointHealthCheck(name); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckOptionsValidator.cs new file mode 100644 index 00000000000..b606417706d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +[OptionsValidator] +internal sealed partial class EndpointOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckService.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckService.cs new file mode 100644 index 00000000000..8c29a411ea3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Kubernetes/TcpEndpointHealthCheckService.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +/// +/// Opens a TCP port if the service is healthy and closes it otherwise. +/// +internal sealed class TcpEndpointHealthCheckService : BackgroundService +{ + internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; + + private readonly ILogger _logger; + private readonly HealthCheckService _healthCheckService; + private readonly KubernetesProbesOptions.EndpointOptions _options; + private readonly TcpListener _listener; + + public TcpEndpointHealthCheckService(ILogger logger, HealthCheckService healthCheckService, KubernetesProbesOptions.EndpointOptions options) + { + _logger = logger; + _healthCheckService = healthCheckService; + _options = options; + + _listener = new TcpListener(IPAddress.Any, _options.TcpPort); + } + + internal async Task UpdateHealthStatusAsync(CancellationToken cancellationToken) + { + try + { + var report = await _healthCheckService.CheckHealthAsync(_options.FilterChecks, cancellationToken).ConfigureAwait(false); + if (report.Status == HealthStatus.Healthy) + { + if (!_listener.Server.IsBound) + { + _listener.Start(_options.MaxPendingConnections); + _ = Task.Run(() => OpenTcpAsync(cancellationToken), cancellationToken); + } + } + else + { + _listener.Stop(); + } + } + catch (SocketException ex) + { + _logger.SocketExceptionCaughtTcpEndpoint(ex); + } + } + + /// + /// Executes the health checks in the and opens the registered TCP port if the service is healthy and closes it otherwise. + /// + /// The . + /// Task. + protected async override Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + _listener.Start(); + + while (!stoppingToken.IsCancellationRequested) + { + await UpdateHealthStatusAsync(stoppingToken).ConfigureAwait(false); + await TimeProvider.Delay(_options.Period, stoppingToken).ConfigureAwait(false); + } + + _listener.Stop(); + } + + [SuppressMessage("Blocker Bug", "S2190:Recursion should not be infinite", Justification = "runs in background")] + private async Task OpenTcpAsync(CancellationToken cancellationToken) + { + while (true) + { +#if NET6_0_OR_GREATER + using var client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false); +#else + using var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); +#endif + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Microsoft.Extensions.Diagnostics.Probes.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Microsoft.Extensions.Diagnostics.Probes.csproj new file mode 100644 index 00000000000..f754007d104 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Microsoft.Extensions.Diagnostics.Probes.csproj @@ -0,0 +1,38 @@ + + + Microsoft.Extensions.Diagnostics.Probes + Provides support for environmental probes. + Resilience + + + + true + true + true + true + + + + dev + 100 + 75 + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Microsoft.Extensions.Diagnostics.Probes.json b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Microsoft.Extensions.Diagnostics.Probes.json new file mode 100644 index 00000000000..56d414e22df --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/Microsoft.Extensions.Diagnostics.Probes.json @@ -0,0 +1,72 @@ +{ + "Name": "Microsoft.Extensions.Diagnostics.Probes, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Types": [ + { + "Type": "class Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesOptions", + "Stage": "Experimental", + "Methods": [ + { + "Member": "Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesOptions.EndpointOptions.EndpointOptions();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "System.Func Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesOptions.EndpointOptions.FilterChecks { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "int Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesOptions.EndpointOptions.MaxPendingConnections { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "System.TimeSpan Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesOptions.EndpointOptions.Period { get; set; }", + "Stage": "Experimental" + }, + { + "Member": "int Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesOptions.EndpointOptions.TcpPort { get; set; }", + "Stage": "Experimental" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesExtensions", + "Stage": "Experimental", + "Methods": [ + { + "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesExtensions.AddKubernetesProbes(this Microsoft.Extensions.DependencyInjection.IServiceCollection services);", + "Stage": "Experimental" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesExtensions.AddKubernetesProbes(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfigurationSection section);", + "Stage": "Experimental" + }, + { + "Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.Diagnostics.Probes.KubernetesProbesExtensions.AddKubernetesProbes(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure);", + "Stage": "Experimental" + } + ] + }, + { + "Type": "static class Microsoft.Extensions.Diagnostics.Probes.ProbeTags", + "Stage": "Experimental", + "Fields": [ + { + "Member": "const string Microsoft.Extensions.Diagnostics.Probes.ProbeTags.Liveness", + "Stage": "Experimental", + "Value": "_probe_liveness" + }, + { + "Member": "const string Microsoft.Extensions.Diagnostics.Probes.ProbeTags.Readiness", + "Stage": "Experimental", + "Value": "_probe_readiness" + }, + { + "Member": "const string Microsoft.Extensions.Diagnostics.Probes.ProbeTags.Startup", + "Stage": "Experimental", + "Value": "_probe_startup" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/ProbeTags.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/ProbeTags.cs new file mode 100644 index 00000000000..20c5de72376 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/ProbeTags.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Probes; + +/// +/// Standardized health check tags for probes. +/// +public static class ProbeTags +{ + /// + /// Liveness probe tag. + /// + public const string Liveness = "_probe_liveness"; + + /// + /// Startup probe tag. + /// + public const string Startup = "_probe_startup"; + + /// + /// Readiness probe tag. + /// + public const string Readiness = "_probe_readiness"; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj index 3d4645b319d..7a022b51590 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj @@ -34,7 +34,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ApplicationLifecycleHealthCheckTest.cs similarity index 95% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ApplicationLifecycleHealthCheckTest.cs index a28347a4f4a..9d306daf44d 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ApplicationLifecycleHealthCheckTest.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; public class ApplicationLifecycleHealthCheckTest { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs similarity index 94% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs index 3f170498df0..c754eade209 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs @@ -9,7 +9,7 @@ using Moq; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; public class ApplicationLifecycleHealthChecksExtensionsTest { @@ -59,7 +59,7 @@ private static void AssertAddedHealthCheck(IServiceCollection serviceCollecti foreach (var r in registrations) { Assert.True(r.Factory(serviceProvider) is T); - Assert.Equal("ApplicationLifecycleHealthCheck", r.Name); + Assert.Equal("ApplicationLifecycle", r.Name); } } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ManualHealthCheckExtensionsTest.cs similarity index 92% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ManualHealthCheckExtensionsTest.cs index a0bec663ed7..3ab6641839b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ManualHealthCheckExtensionsTest.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; public class ManualHealthCheckExtensionsTest { @@ -17,7 +17,7 @@ public void AddManualHealthCheck_DependenciesAreRegistered() var serviceCollection = new ServiceCollection(); serviceCollection.AddHealthChecks().AddManualHealthCheck(); - AssertAddedHealthCheck(serviceCollection, "ManualHealthCheck"); + AssertAddedHealthCheck(serviceCollection, "Manual"); } [Fact] @@ -26,7 +26,7 @@ public void AddManualHealthCheck_WithTags_DependenciesAreRegistered() var serviceCollection = new ServiceCollection(); serviceCollection.AddHealthChecks().AddManualHealthCheck("test1", "test2"); - AssertAddedHealthCheck(serviceCollection, "ManualHealthCheck"); + AssertAddedHealthCheck(serviceCollection, "Manual"); } [Fact] @@ -35,7 +35,7 @@ public void AddManualHealthCheck_WithTagsEnumerable_DependenciesAreRegistered() var serviceCollection = new ServiceCollection(); serviceCollection.AddHealthChecks().AddManualHealthCheck(new List { "test1", "test2" }); - AssertAddedHealthCheck(serviceCollection, "ManualHealthCheck"); + AssertAddedHealthCheck(serviceCollection, "Manual"); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ManualHealthCheckTest.cs similarity index 98% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ManualHealthCheckTest.cs index 9c13bce64d0..b0230b370ee 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/ManualHealthCheckTest.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; public class ManualHealthCheckTest { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests.csproj similarity index 78% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests.csproj index b59be01cd0f..cfc865e1184 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests.csproj @@ -1,11 +1,11 @@  - Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests - Unit tests for Microsoft.Extensions.Diagnostics.HealthChecks.Core + Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests + Unit tests for Microsoft.Extensions.Diagnostics.HealthChecks.Common - + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/MockHostApplicationLifetime.cs similarity index 94% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/MockHostApplicationLifetime.cs index 1dd58df8e22..2135a15ca42 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/MockHostApplicationLifetime.cs @@ -5,7 +5,7 @@ using System.Threading; using Microsoft.Extensions.Hosting; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; internal class MockHostApplicationLifetime : IHostApplicationLifetime, IDisposable { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs similarity index 97% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs index 2764b792d49..a290a81f392 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; public class TelemetryHealthChecksPublisherExtensionsTest { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/TelemetryHealthChecksPublisherTest.cs similarity index 98% rename from test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs rename to test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/TelemetryHealthChecksPublisherTest.cs index 2b907420d23..0edab8d2179 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests/TelemetryHealthChecksPublisherTest.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Telemetry.Testing.Metering; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests; public class TelemetryHealthChecksPublisherTest { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs deleted file mode 100644 index 8cf2e96cd36..00000000000 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; - -public class KubernetesHealthCheckPublisherExtensionsTest -{ - [Fact] - public void AddKubernetesHealthCheckPublisherTest() - { - var serviceCollection = new ServiceCollection(); - - _ = serviceCollection.Configure(o => { }); - serviceCollection.AddKubernetesHealthCheckPublisher(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var publishers = serviceProvider.GetRequiredService>(); - - Assert.Single(publishers); - foreach (var p in publishers) - { - Assert.True(p is KubernetesHealthCheckPublisher); - } - } - - [Fact] - public void AddKubernetesHealthCheckPublisherTest_WithAction() - { - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddKubernetesHealthCheckPublisher(o => - { - o.TcpPort = 2305; - o.MaxPendingConnections = 10; - }); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var publishers = serviceProvider.GetRequiredService>(); - - Assert.Single(publishers); - foreach (var p in publishers) - { - Assert.True(p is KubernetesHealthCheckPublisher); - } - } - - [Fact] - public void AddKubernetesHealthCheckPublisherTest_WithConfigurationSection() - { - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddKubernetesHealthCheckPublisher(SetupKubernetesHealthCheckPublisherConfiguration("2305", "10") - .GetSection(nameof(KubernetesHealthCheckPublisherOptions))); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var publishers = serviceProvider.GetRequiredService>(); - - Assert.Single(publishers); - foreach (var p in publishers) - { - Assert.True(p is KubernetesHealthCheckPublisher); - } - } - - [Fact] - public void TestNullChecks() - { - Assert.Throws(() => new ServiceCollection().AddKubernetesHealthCheckPublisher((Action)null!)); - Assert.Throws(() => new ServiceCollection().AddKubernetesHealthCheckPublisher((IConfigurationSection)null!)); - } - - private static IConfiguration SetupKubernetesHealthCheckPublisherConfiguration( - string tcpPort, - string maxLengthOfPendingConnectionsQueue) - { - KubernetesHealthCheckPublisherOptions kubernetesHealthCheckPublisherOptions; - - var configurationDict = new Dictionary - { - { - $"{nameof(KubernetesHealthCheckPublisherOptions)}:{nameof(kubernetesHealthCheckPublisherOptions.TcpPort)}", - tcpPort - }, - { - $"{nameof(KubernetesHealthCheckPublisherOptions)}:{nameof(kubernetesHealthCheckPublisherOptions.MaxPendingConnections)}", - maxLengthOfPendingConnectionsQueue - } - }; - - return new ConfigurationBuilder().AddInMemoryCollection(configurationDict).Build(); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs deleted file mode 100644 index a56ebfd615c..00000000000 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Microsoft.TestUtilities; -using Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; - -public class KubernetesHealthCheckPublisherTest -{ - [ConditionalFact] - [OSSkipCondition(OperatingSystems.Linux)] - public async Task PublishAsync_CheckIfTcpPortIsOpenedAfterHealthStatusEvents() - { - KubernetesHealthCheckPublisherOptions options = new KubernetesHealthCheckPublisherOptions(); - KubernetesHealthCheckPublisher publisher = new KubernetesHealthCheckPublisher(Microsoft.Extensions.Options.Options.Create(options)); - - Assert.False(IsTcpPortOpened()); - - await publisher.PublishAsync(CreateHealthReport(HealthStatus.Healthy), CancellationToken.None); - Assert.True(IsTcpPortOpened()); - - await publisher.PublishAsync(CreateHealthReport(HealthStatus.Healthy), CancellationToken.None); - Assert.True(IsTcpPortOpened()); - - await publisher.PublishAsync(CreateHealthReport(HealthStatus.Unhealthy), CancellationToken.None); - Assert.False(IsTcpPortOpened()); - - await publisher.PublishAsync(CreateHealthReport(HealthStatus.Unhealthy), CancellationToken.None); - Assert.False(IsTcpPortOpened()); - - await publisher.PublishAsync(CreateHealthReport(HealthStatus.Healthy), CancellationToken.None); - Assert.True(IsTcpPortOpened()); - } - - [Fact] - public void Ctor_ThrowsWhenOptionsValueNull() - { - Assert.Throws(() => new KubernetesHealthCheckPublisher(Microsoft.Extensions.Options.Options.Create(null!))); - } - - private static HealthReport CreateHealthReport(HealthStatus healthStatus) - { - HealthReportEntry entry = new HealthReportEntry(healthStatus, null, TimeSpan.Zero, null, null); - var healthStatusRecords = new Dictionary { { "id", entry } }; - return new HealthReport(healthStatusRecords, TimeSpan.Zero); - } - - private static bool IsTcpPortOpened() - { - try - { - using TcpClient tcpClient = new TcpClient("localhost", 2305); - return true; - } - catch (SocketException e) - { - if (e.SocketErrorCode == SocketError.ConnectionRefused) - { - return false; - } - else - { - throw; - } - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/KubernetesProbesExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/KubernetesProbesExtensionsTest.cs new file mode 100644 index 00000000000..a37587c8053 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/KubernetesProbesExtensionsTest.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.AspNetCore.TestHost; +#else +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Internal; +#endif +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.Extensions.Hosting.Testing; +#endif +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Probes.Tests; + +public class KubernetesProbesExtensionsTest +{ + [Fact] + public void AddKubernetesProbes_RegistersAllProbes() + { + using var host = CreateWebHost((services) => + { + services.AddKubernetesProbes().AddHealthChecks(); + }); + + var hostedServices = host.Services.GetServices().Where(service => service.GetType().Name == "TcpEndpointHealthCheckService"); + var configurations = host.Services.GetServices>(); + + Assert.Equal(3, hostedServices.Count()); + Assert.Single(configurations); + Assert.Single(configurations); + var config = configurations.First(); + + var livenessRegistration = new HealthCheckRegistration("liveness", Mock.Of(), null, new[] { ProbeTags.Liveness }); + var startupRegistration = new HealthCheckRegistration("startup", Mock.Of(), null, new[] { ProbeTags.Startup }); + var readinessRegistration = new HealthCheckRegistration("readiness", Mock.Of(), null, new[] { ProbeTags.Readiness }); + + var livenessConfig = config.Get(ProbeTags.Liveness); + Assert.True(livenessConfig.FilterChecks(livenessRegistration)); + Assert.False(livenessConfig.FilterChecks(startupRegistration)); + Assert.False(livenessConfig.FilterChecks(readinessRegistration)); + + var startupConfig = config.Get(ProbeTags.Startup); + Assert.False(startupConfig.FilterChecks(livenessRegistration)); + Assert.True(startupConfig.FilterChecks(startupRegistration)); + Assert.False(startupConfig.FilterChecks(readinessRegistration)); + + var readinessConfig = config.Get(ProbeTags.Readiness); + Assert.False(readinessConfig.FilterChecks(livenessRegistration)); + Assert.False(readinessConfig.FilterChecks(startupRegistration)); + Assert.True(readinessConfig.FilterChecks(readinessRegistration)); + } + + [Fact] + public void AddKubernetesProbes_WithConfigureAction_RegistersAllProbes() + { + using var host = CreateWebHost((services) => + { + services.AddKubernetesProbes(options => + { + options.LivenessProbe.TcpPort = 1; + options.StartupProbe.TcpPort = 2; + options.ReadinessProbe.TcpPort = 3; + }).AddHealthChecks(); + }); + + var hostedServices = host.Services.GetServices().Where(service => service.GetType().Name == "TcpEndpointHealthCheckService"); + var configurations = host.Services.GetServices>(); + + Assert.Equal(3, hostedServices.Count()); + Assert.Single(configurations); + var config = configurations.First(); + Assert.Equal(1, config.Get(ProbeTags.Liveness).TcpPort); + Assert.Equal(2, config.Get(ProbeTags.Startup).TcpPort); + Assert.Equal(3, config.Get(ProbeTags.Readiness).TcpPort); + } + + [Fact] + public void AddKubernetesProbes_WithConfigurationSection_RegistersAllProbes() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["KubernetesProbes:LivenessProbe:TcpPort"] = "1", + ["KubernetesProbes:StartupProbe:TcpPort"] = "2", + ["KubernetesProbes:ReadinessProbe:TcpPort"] = "3", + }) + .Build(); + + using var host = CreateWebHost((services) => + { + services.AddKubernetesProbes(configuration.GetSection("KubernetesProbes")).AddHealthChecks(); + }); + + var hostedServices = host.Services.GetServices().Where(service => service.GetType().Name == "TcpEndpointHealthCheckService"); + var configurations = host.Services.GetServices>(); + + Assert.Equal(3, hostedServices.Count()); + Assert.Single(configurations); + var config = configurations.First(); + Assert.Equal(1, config.Get(ProbeTags.Liveness).TcpPort); + Assert.Equal(2, config.Get(ProbeTags.Startup).TcpPort); + Assert.Equal(3, config.Get(ProbeTags.Readiness).TcpPort); + } + +#if NETCOREAPP3_1_OR_GREATER + private static IHost CreateWebHost(Action configureServices) + { + return FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(configureServices)) + .Build(); + } +#else + private static IWebHost CreateWebHost(Action configureServices) + { + return new WebHostBuilder() + .ConfigureServices(configureServices) + .Configure(app => app + .UseEndpointRouting() + .UseRouter(routes => { }) + .UseMvc()) + .Build(); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/MockHealthCheckService.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/MockHealthCheckService.cs new file mode 100644 index 00000000000..b3430b25269 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/MockHealthCheckService.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.Diagnostics.Probes.Tests; + +internal class MockHealthCheckService : HealthCheckService +{ + private readonly Task _healthyReport = CreateHealthReport(HealthStatus.Healthy); + private readonly Task _unhealthyReport = CreateHealthReport(HealthStatus.Unhealthy); + public bool IsHealthy = true; + + public override Task CheckHealthAsync(Func? predicate, CancellationToken cancellationToken = default) + { + return IsHealthy ? _healthyReport : _unhealthyReport; + } + + private static Task CreateHealthReport(HealthStatus healthStatus) + { + HealthReportEntry entry = new HealthReportEntry(healthStatus, null, TimeSpan.Zero, null, null); + var healthStatusRecords = new Dictionary { { "id", entry } }; + return Task.FromResult(new HealthReport(healthStatusRecords, TimeSpan.Zero)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/TcpEndpointHealthCheckExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/TcpEndpointHealthCheckExtensionsTest.cs new file mode 100644 index 00000000000..ced23a042aa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/TcpEndpointHealthCheckExtensionsTest.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.AspNetCore.TestHost; +#else +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Internal; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.Extensions.Hosting.Testing; +#endif +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Probes.Tests; + +public class TcpEndpointHealthCheckExtensionsTest +{ + [Fact] + public void AddTcpEndpointHealthCheckTest_WithoutConfig() + { + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck(); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + + Assert.Single(hostedServices); + } + + [Fact] + public void AddTcpEndpointHealthCheckTest_WithAction() + { + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck(o => + { + o.FilterChecks = _ => false; + o.Period = TimeSpan.FromSeconds(15); + }); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + var configurations = host.Services.GetServices>(); + + Assert.Single(hostedServices); + var config = Assert.Single(configurations); + Assert.Equal(TimeSpan.FromSeconds(15), config.Value.Period); + } + + [Fact] + public void AddTcpEndpointHealthCheckTest_WithName_WithoutConfig() + { + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck("Liveness"); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + + Assert.Single(hostedServices); + } + + [Fact] + public void AddTcpEndpointHealthCheckTest_WithName_WithAction() + { + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck("Liveness", o => + { + o.FilterChecks = _ => false; + o.Period = TimeSpan.FromSeconds(5); + }); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + var configurations = host.Services.GetServices>(); + + Assert.Single(hostedServices); + var config = Assert.Single(configurations); + Assert.NotNull(config.Get("Liveness")); + Assert.Equal(TimeSpan.FromSeconds(5), config.Get("Liveness").Period); + } + + [Fact] + public void AddTcpEndpointHealthCheckTest_WithConfigurationSection() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TcpHealthCheck:TcpPort"] = "1234", + }) + .Build(); + + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck(config.GetSection("TcpHealthCheck")); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + var configurations = host.Services.GetServices>(); + + Assert.Single(hostedServices); + var configuration = Assert.Single(configurations); + Assert.Equal(1234, configuration.Value.TcpPort); + } + + [Fact] + public void AddTcpEndpointHealthCheckTest_WithName_WithConfigurationSection() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TcpHealthCheck:TcpPort"] = "1234", + }) + .Build(); + + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck("Liveness", config.GetSection("TcpHealthCheck")); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + var configurations = host.Services.GetServices>(); + + Assert.Single(hostedServices); + Assert.Single(configurations); + var configuration = configurations.First(); + Assert.NotNull(configuration.Get("Liveness")); + Assert.Equal(1234, configuration.Get("Liveness").TcpPort); + } + + [Fact] + public void AddTcpEndpointHealthCheckTest_MultipleNamed() + { + using var host = CreateWebHost(services => + { + services + .AddRouting() + .AddTcpEndpointHealthCheck("Liveness") + .AddTcpEndpointHealthCheck("Readiness"); + }); + + var hostedServices = host.Services.GetServices().Where(x => x is TcpEndpointHealthCheckService); + var configurations = host.Services.GetServices>(); + + Assert.Equal(2, hostedServices.Count()); + Assert.Single(configurations); + var config = configurations.First(); + Assert.NotNull(config.Get("Liveness")); + Assert.NotNull(config.Get("Readiness")); + } + +#if NETCOREAPP3_1_OR_GREATER + private static IHost CreateWebHost(Action configureServices) + { + return FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(configureServices)) + .Build(); + } +#else + private static IWebHost CreateWebHost(Action configureServices) + { + return new WebHostBuilder() + .ConfigureServices(configureServices) + .Configure(app => app + .UseEndpointRouting() + .UseRouter(routes => { }) + .UseMvc()) + .Build(); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/TcpEndpointHealthCheckServiceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/TcpEndpointHealthCheckServiceTest.cs new file mode 100644 index 00000000000..4ea035b6caf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Kubernetes/TcpEndpointHealthCheckServiceTest.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Probes.Tests; + +[CollectionDefinition(nameof(TcpEndpointHealthCheckServiceTest), DisableParallelization = true)] +public class TcpEndpointHealthCheckServiceTest +{ + [Fact] + public async Task ExecuteAsync_CheckListenerOpenAndCloseAfterHealthStatusEvents() + { + var port = GetFreeTcpPort(); + + using var cts = new CancellationTokenSource(); + + var healthCheckService = new MockHealthCheckService(); + + var options = new KubernetesProbesOptions.EndpointOptions + { + TcpPort = port, + }; + var timeProvider = new FakeTimeProvider(); + using var tcpEndpointHealthCheckService = new TcpEndpointHealthCheckService( + new FakeLogger(), + healthCheckService, + options) + { + TimeProvider = timeProvider + }; + + Assert.False(IsTcpOpened(port)); + + await tcpEndpointHealthCheckService.StartAsync(cts.Token); + await tcpEndpointHealthCheckService.UpdateHealthStatusAsync(cts.Token); + + Assert.True(IsTcpOpened(port)); + + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + Assert.True(IsTcpOpened(port)); + + healthCheckService.IsHealthy = false; + await tcpEndpointHealthCheckService.UpdateHealthStatusAsync(cts.Token); + + Assert.False(IsTcpOpened(port)); + + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + Assert.False(IsTcpOpened(port)); + + healthCheckService.IsHealthy = true; + await tcpEndpointHealthCheckService.UpdateHealthStatusAsync(cts.Token); + + Assert.True(IsTcpOpened(port)); + + cts.Cancel(); + } + +#if NET5_0_OR_GREATER + [Fact] + public async Task ExecuteAsync_Does_Nothing_On_Cancellation() + { + var port = GetFreeTcpPort(); + + var healthCheckService = new MockHealthCheckService(); + + var options = new KubernetesProbesOptions.EndpointOptions + { + TcpPort = port, + }; + var timeProvider = new FakeTimeProvider(); + using var tcpEndpointHealthCheckService = new TcpEndpointHealthCheckService( + new FakeLogger(), + healthCheckService, + options) + { + TimeProvider = timeProvider + }; + + using var cts = new CancellationTokenSource(); + + cts.Cancel(); + await tcpEndpointHealthCheckService.StartAsync(cts.Token); + + Assert.False(IsTcpOpened(port)); + } +#endif + + private static bool IsTcpOpened(int port) + { + try + { + using TcpClient tcpClient = new TcpClient(); + tcpClient.Connect(new IPEndPoint(IPAddress.Loopback, port)); + tcpClient.Close(); + return true; + } + catch (SocketException e) + { + if (e.SocketErrorCode == SocketError.ConnectionRefused) + { + return false; + } + else + { + throw; + } + } + } + + private static int GetFreeTcpPort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj new file mode 100644 index 00000000000..b3930f6e323 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj @@ -0,0 +1,21 @@ + + + Microsoft.Extensions.Diagnostics.Probes.Tests + Unit tests for Microsoft.Extensions.Diagnostics.Probes + + + + + + + + + + + + + + + + +