263 lines
11 KiB
C#
263 lines
11 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
|
{
|
|
internal sealed class HealthCheckPublisherHostedService : IHostedService
|
|
{
|
|
private readonly HealthCheckService _healthCheckService;
|
|
private readonly IOptions<HealthCheckPublisherOptions> _options;
|
|
private readonly ILogger _logger;
|
|
private readonly IHealthCheckPublisher[] _publishers;
|
|
|
|
private CancellationTokenSource _stopping;
|
|
private Timer _timer;
|
|
|
|
public HealthCheckPublisherHostedService(
|
|
HealthCheckService healthCheckService,
|
|
IOptions<HealthCheckPublisherOptions> options,
|
|
ILogger<HealthCheckPublisherHostedService> logger,
|
|
IEnumerable<IHealthCheckPublisher> publishers)
|
|
{
|
|
if (healthCheckService == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(healthCheckService));
|
|
}
|
|
|
|
if (options == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
if (logger == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
if (publishers == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(publishers));
|
|
}
|
|
|
|
_healthCheckService = healthCheckService;
|
|
_options = options;
|
|
_logger = logger;
|
|
_publishers = publishers.ToArray();
|
|
|
|
_stopping = new CancellationTokenSource();
|
|
}
|
|
|
|
internal bool IsStopping => _stopping.IsCancellationRequested;
|
|
|
|
internal bool IsTimerRunning => _timer != null;
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (_publishers.Length == 0)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// IMPORTANT - make sure this is the last thing that happens in this method. The timer can
|
|
// fire before other code runs.
|
|
_timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_stopping.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore exceptions thrown as a result of a cancellation.
|
|
}
|
|
|
|
if (_publishers.Length == 0)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
|
|
private async void Timer_Tick(object state)
|
|
{
|
|
await RunAsync();
|
|
}
|
|
|
|
// Internal for testing
|
|
internal async Task RunAsync()
|
|
{
|
|
var duration = ValueStopwatch.StartNew();
|
|
Logger.HealthCheckPublisherProcessingBegin(_logger);
|
|
|
|
CancellationTokenSource cancellation = null;
|
|
try
|
|
{
|
|
var timeout = _options.Value.Timeout;
|
|
|
|
cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
|
|
cancellation.CancelAfter(timeout);
|
|
|
|
await RunAsyncCore(cancellation.Token);
|
|
|
|
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
|
|
}
|
|
catch (OperationCanceledException) when (IsStopping)
|
|
{
|
|
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
|
// a timeout and we want to log it.
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// This is an error, publishing failed.
|
|
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex);
|
|
}
|
|
finally
|
|
{
|
|
cancellation.Dispose();
|
|
}
|
|
}
|
|
|
|
private async Task RunAsyncCore(CancellationToken cancellationToken)
|
|
{
|
|
// Forcibly yield - we want to unblock the timer thread.
|
|
await Task.Yield();
|
|
|
|
// The health checks service does it's own logging, and doesn't throw exceptions.
|
|
var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken);
|
|
|
|
var publishers = _publishers;
|
|
var tasks = new Task[publishers.Length];
|
|
for (var i = 0; i < publishers.Length; i++)
|
|
{
|
|
tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken);
|
|
}
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken)
|
|
{
|
|
var duration = ValueStopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
Logger.HealthCheckPublisherBegin(_logger, publisher);
|
|
|
|
await publisher.PublishAsync(report, cancellationToken);
|
|
Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime());
|
|
}
|
|
catch (OperationCanceledException) when (IsStopping)
|
|
{
|
|
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
|
// a timeout and we want to log it.
|
|
}
|
|
catch (OperationCanceledException ocex)
|
|
{
|
|
Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime());
|
|
throw ocex;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
internal static class EventIds
|
|
{
|
|
public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin");
|
|
public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd");
|
|
public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError");
|
|
|
|
public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin");
|
|
public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd");
|
|
public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError");
|
|
public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout");
|
|
}
|
|
|
|
private static class Logger
|
|
{
|
|
private static readonly Action<ILogger, Exception> _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
|
|
LogLevel.Debug,
|
|
EventIds.HealthCheckPublisherProcessingBegin,
|
|
"Running health check publishers");
|
|
|
|
private static readonly Action<ILogger, double, Exception> _healthCheckPublisherProcessingEnd = LoggerMessage.Define<double>(
|
|
LogLevel.Debug,
|
|
EventIds.HealthCheckPublisherProcessingEnd,
|
|
"Health check publisher processing completed after {ElapsedMilliseconds}ms");
|
|
|
|
private static readonly Action<ILogger, IHealthCheckPublisher, Exception> _healthCheckPublisherBegin = LoggerMessage.Define<IHealthCheckPublisher>(
|
|
LogLevel.Debug,
|
|
EventIds.HealthCheckPublisherBegin,
|
|
"Running health check publisher '{HealthCheckPublisher}'");
|
|
|
|
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherEnd = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
|
LogLevel.Debug,
|
|
EventIds.HealthCheckPublisherEnd,
|
|
"Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
|
|
|
|
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherError = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
|
LogLevel.Error,
|
|
EventIds.HealthCheckPublisherError,
|
|
"Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
|
|
|
|
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherTimeout = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
|
LogLevel.Error,
|
|
EventIds.HealthCheckPublisherTimeout,
|
|
"Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms");
|
|
|
|
public static void HealthCheckPublisherProcessingBegin(ILogger logger)
|
|
{
|
|
_healthCheckPublisherProcessingBegin(logger, null);
|
|
}
|
|
|
|
public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null)
|
|
{
|
|
_healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception);
|
|
}
|
|
|
|
public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher)
|
|
{
|
|
_healthCheckPublisherBegin(logger, publisher, null);
|
|
}
|
|
|
|
public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
|
{
|
|
_healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null);
|
|
}
|
|
|
|
public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception)
|
|
{
|
|
_healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception);
|
|
}
|
|
|
|
public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
|
{
|
|
_healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null);
|
|
}
|
|
}
|
|
}
|
|
}
|