Skip to content
This repository was archived by the owner on Dec 8, 2018. It is now read-only.

Commit 1f31e05

Browse files
authored
Add IHealthCheckPublisher for push-based checks (#498)
IHealthCheckPublisher allows you to configure and run health checks regularly inside an application, and push the notifications elsewhere. All publishers are part of a single queue with a configurable period and timeout.
1 parent 9722d89 commit 1f31e05

File tree

13 files changed

+943
-24
lines changed

13 files changed

+943
-24
lines changed

build/dependencies.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
<MicrosoftExtensionsLoggingConsolePackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsLoggingConsolePackageVersion>
2828
<MicrosoftExtensionsLoggingPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsLoggingPackageVersion>
2929
<MicrosoftExtensionsLoggingTestingPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsLoggingTestingPackageVersion>
30+
<MicrosoftExtensionsHostingAbstractionsPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsHostingAbstractionsPackageVersion>
31+
<MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>
3032
<MicrosoftExtensionsOptionsPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsOptionsPackageVersion>
3133
<MicrosoftExtensionsRazorViewsSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsRazorViewsSourcesPackageVersion>
3234
<MicrosoftExtensionsStackTraceSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsStackTraceSourcesPackageVersion>

samples/WelcomePageSample/web.config

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.Extensions.Diagnostics.HealthChecks
8+
{
9+
/// <summary>
10+
/// Represents a publisher of <see cref="HealthReport"/> information.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// The default health checks implementation provided an <c>IHostedService</c> implementation that can
15+
/// be used to execute health checks at regular intervals and provide the resulting <see cref="HealthReport"/>
16+
/// data to all registered <see cref="IHealthCheckPublisher"/> instances.
17+
/// </para>
18+
/// <para>
19+
/// To provide an <see cref="IHealthCheckPublisher"/> implementation, register an instance or type as a singleton
20+
/// service in the dependency injection container.
21+
/// </para>
22+
/// <para>
23+
/// <see cref="IHealthCheckPublisher"/> instances are provided with a <see cref="HealthReport"/> after executing
24+
/// health checks in a background thread. The use of <see cref="IHealthCheckPublisher"/> depend on hosting in
25+
/// an application using <c>IWebHost</c> or generic host (<c>IHost</c>). Execution of <see cref="IHealthCheckPublisher"/>
26+
/// instance is not related to execution of health checks via a middleware.
27+
/// </para>
28+
/// </remarks>
29+
public interface IHealthCheckPublisher
30+
{
31+
/// <summary>
32+
/// Publishes the provided <paramref name="report"/>.
33+
/// </summary>
34+
/// <param name="report">The <see cref="HealthReport"/>. The result of executing a set of health checks.</param>
35+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
36+
/// <returns>A <see cref="Task"/> which will complete when publishing is complete.</returns>
37+
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
38+
}
39+
}

src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,19 @@ private static void ValidateRegistrations(IEnumerable<HealthCheckRegistration> r
126126
}
127127
}
128128

129-
private static class Log
129+
internal static class EventIds
130130
{
131-
public static class EventIds
132-
{
133-
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
134-
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
131+
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
132+
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
135133

136-
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
137-
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
138-
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
139-
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
140-
}
134+
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
135+
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
136+
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
137+
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
138+
}
141139

140+
private static class Log
141+
{
142142
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
143143
LogLevel.Debug,
144144
EventIds.HealthCheckProcessingBegin,

src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.Extensions.DependencyInjection.Extensions;
55
using Microsoft.Extensions.Diagnostics.HealthChecks;
6+
using Microsoft.Extensions.Hosting;
67

78
namespace Microsoft.Extensions.DependencyInjection
89
{
@@ -24,7 +25,8 @@ public static class HealthCheckServiceCollectionExtensions
2425
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
2526
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
2627
{
27-
services.TryAdd(ServiceDescriptor.Singleton<HealthCheckService, DefaultHealthCheckService>());
28+
services.TryAddSingleton<HealthCheckService, DefaultHealthCheckService>();
29+
services.TryAddSingleton<IHostedService, HealthCheckPublisherHostedService>();
2830
return new HealthChecksBuilder(services);
2931
}
3032
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Hosting;
10+
using Microsoft.Extensions.Internal;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
13+
14+
namespace Microsoft.Extensions.Diagnostics.HealthChecks
15+
{
16+
internal sealed class HealthCheckPublisherHostedService : IHostedService
17+
{
18+
private readonly HealthCheckService _healthCheckService;
19+
private readonly IOptions<HealthCheckPublisherOptions> _options;
20+
private readonly ILogger _logger;
21+
private readonly IHealthCheckPublisher[] _publishers;
22+
23+
private CancellationTokenSource _stopping;
24+
private Timer _timer;
25+
26+
public HealthCheckPublisherHostedService(
27+
HealthCheckService healthCheckService,
28+
IOptions<HealthCheckPublisherOptions> options,
29+
ILogger<HealthCheckPublisherHostedService> logger,
30+
IEnumerable<IHealthCheckPublisher> publishers)
31+
{
32+
if (healthCheckService == null)
33+
{
34+
throw new ArgumentNullException(nameof(healthCheckService));
35+
}
36+
37+
if (options == null)
38+
{
39+
throw new ArgumentNullException(nameof(options));
40+
}
41+
42+
if (logger == null)
43+
{
44+
throw new ArgumentNullException(nameof(logger));
45+
}
46+
47+
if (publishers == null)
48+
{
49+
throw new ArgumentNullException(nameof(publishers));
50+
}
51+
52+
_healthCheckService = healthCheckService;
53+
_options = options;
54+
_logger = logger;
55+
_publishers = publishers.ToArray();
56+
57+
_stopping = new CancellationTokenSource();
58+
}
59+
60+
internal bool IsStopping => _stopping.IsCancellationRequested;
61+
62+
internal bool IsTimerRunning => _timer != null;
63+
64+
public Task StartAsync(CancellationToken cancellationToken = default)
65+
{
66+
if (_publishers.Length == 0)
67+
{
68+
return Task.CompletedTask;
69+
}
70+
71+
// IMPORTANT - make sure this is the last thing that happens in this method. The timer can
72+
// fire before other code runs.
73+
_timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
74+
75+
return Task.CompletedTask;
76+
}
77+
78+
public Task StopAsync(CancellationToken cancellationToken = default)
79+
{
80+
try
81+
{
82+
_stopping.Cancel();
83+
}
84+
catch
85+
{
86+
// Ignore exceptions thrown as a result of a cancellation.
87+
}
88+
89+
if (_publishers.Length == 0)
90+
{
91+
return Task.CompletedTask;
92+
}
93+
94+
_timer?.Dispose();
95+
_timer = null;
96+
97+
98+
return Task.CompletedTask;
99+
}
100+
101+
// Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
102+
private async void Timer_Tick(object state)
103+
{
104+
await RunAsync();
105+
}
106+
107+
// Internal for testing
108+
internal async Task RunAsync()
109+
{
110+
var duration = ValueStopwatch.StartNew();
111+
Logger.HealthCheckPublisherProcessingBegin(_logger);
112+
113+
CancellationTokenSource cancellation = null;
114+
try
115+
{
116+
var timeout = _options.Value.Timeout;
117+
118+
cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
119+
cancellation.CancelAfter(timeout);
120+
121+
await RunAsyncCore(cancellation.Token);
122+
123+
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
124+
}
125+
catch (OperationCanceledException) when (IsStopping)
126+
{
127+
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
128+
// a timeout and we want to log it.
129+
}
130+
catch (Exception ex)
131+
{
132+
// This is an error, publishing failed.
133+
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex);
134+
}
135+
finally
136+
{
137+
cancellation.Dispose();
138+
}
139+
}
140+
141+
private async Task RunAsyncCore(CancellationToken cancellationToken)
142+
{
143+
// Forcibly yield - we want to unblock the timer thread.
144+
await Task.Yield();
145+
146+
// The health checks service does it's own logging, and doesn't throw exceptions.
147+
var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken);
148+
149+
var publishers = _publishers;
150+
var tasks = new Task[publishers.Length];
151+
for (var i = 0; i < publishers.Length; i++)
152+
{
153+
tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken);
154+
}
155+
156+
await Task.WhenAll(tasks);
157+
}
158+
159+
private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken)
160+
{
161+
var duration = ValueStopwatch.StartNew();
162+
163+
try
164+
{
165+
Logger.HealthCheckPublisherBegin(_logger, publisher);
166+
167+
await publisher.PublishAsync(report, cancellationToken);
168+
Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime());
169+
}
170+
catch (OperationCanceledException) when (IsStopping)
171+
{
172+
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
173+
// a timeout and we want to log it.
174+
}
175+
catch (OperationCanceledException ocex)
176+
{
177+
Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime());
178+
throw ocex;
179+
}
180+
catch (Exception ex)
181+
{
182+
Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex);
183+
throw ex;
184+
}
185+
}
186+
187+
internal static class EventIds
188+
{
189+
public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin");
190+
public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd");
191+
public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError");
192+
193+
public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin");
194+
public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd");
195+
public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError");
196+
public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout");
197+
}
198+
199+
private static class Logger
200+
{
201+
private static readonly Action<ILogger, Exception> _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
202+
LogLevel.Debug,
203+
EventIds.HealthCheckPublisherProcessingBegin,
204+
"Running health check publishers");
205+
206+
private static readonly Action<ILogger, double, Exception> _healthCheckPublisherProcessingEnd = LoggerMessage.Define<double>(
207+
LogLevel.Debug,
208+
EventIds.HealthCheckPublisherProcessingEnd,
209+
"Health check publisher processing completed after {ElapsedMilliseconds}ms");
210+
211+
private static readonly Action<ILogger, IHealthCheckPublisher, Exception> _healthCheckPublisherBegin = LoggerMessage.Define<IHealthCheckPublisher>(
212+
LogLevel.Debug,
213+
EventIds.HealthCheckPublisherBegin,
214+
"Running health check publisher '{HealthCheckPublisher}'");
215+
216+
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherEnd = LoggerMessage.Define<IHealthCheckPublisher, double>(
217+
LogLevel.Debug,
218+
EventIds.HealthCheckPublisherEnd,
219+
"Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
220+
221+
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherError = LoggerMessage.Define<IHealthCheckPublisher, double>(
222+
LogLevel.Error,
223+
EventIds.HealthCheckPublisherError,
224+
"Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
225+
226+
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherTimeout = LoggerMessage.Define<IHealthCheckPublisher, double>(
227+
LogLevel.Error,
228+
EventIds.HealthCheckPublisherTimeout,
229+
"Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms");
230+
231+
public static void HealthCheckPublisherProcessingBegin(ILogger logger)
232+
{
233+
_healthCheckPublisherProcessingBegin(logger, null);
234+
}
235+
236+
public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null)
237+
{
238+
_healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception);
239+
}
240+
241+
public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher)
242+
{
243+
_healthCheckPublisherBegin(logger, publisher, null);
244+
}
245+
246+
public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
247+
{
248+
_healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null);
249+
}
250+
251+
public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception)
252+
{
253+
_healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception);
254+
}
255+
256+
public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
257+
{
258+
_healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null);
259+
}
260+
}
261+
}
262+
}

0 commit comments

Comments
 (0)