Netcore: Health check notifier hosted service (#9295)

* Implemented health check notifier as a hosted service.
Added validation to health check settings.

* Registered health check notifier as a hosted service.
Modified health check nested settings to use concrete classes to align with other configuration models.

* Resolved issues with email sending using development server.

* PR review comments and fixed failing unit test.

* Changed period and delay millisecond and hourly values to TimeSpans.
Changed configuration of first run time for health check notifications to use H:mm format.

* Set up SecureSocketOptions as a locally defined enum.

* Tightened up time format validation to verify input is an actual time (with hours and minutes only) and not a timespan.

* Aligned naming and namespace of health check configuration related classes with other configuration classes.

* Created constants for hex colors used in formatting health check results as HTML.

* Revert "Tightened up time format validation to verify input is an actual time (with hours and minutes only) and not a timespan."

This reverts commit f9bb8a7a825bcb58146879f18b47922e09453e2d.

* Renamed method to be clear validation is of a TimeSpan and not a time.

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Andy Butland
2020-10-30 13:56:13 +01:00
committed by GitHub
parent 4ae329589a
commit bdb8f34da3
31 changed files with 636 additions and 248 deletions

View File

@@ -128,11 +128,17 @@ namespace Umbraco.Infrastructure.HealthCheck
return html;
}
internal Dictionary<string, IEnumerable<HealthCheckStatus>> ResultsAsDictionary => _results;
private string ApplyHtmlHighlighting(string html)
{
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, "5cb85c");
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, "f0ad4e");
return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, "d9534f");
const string SuccessHexColor = "5cb85c";
const string WarningHexColor = "f0ad4e";
const string ErrorHexColor = "d9534f";
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor);
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor);
return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor);
}
private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color)

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
IOptions<ContentSettings> contentSettings)
: base(healthChecksSettings)
{
var recipientEmail = Settings?["recipientEmail"]?.Value;
var recipientEmail = Settings?["RecipientEmail"];
if (string.IsNullOrWhiteSpace(recipientEmail))
{
Enabled = false;
@@ -45,7 +45,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
public string RecipientEmail { get; }
public override async Task SendAsync(HealthCheckResults results, CancellationToken token)
public override async Task SendAsync(HealthCheckResults results)
{
if (ShouldSend(results) == false)
{

View File

@@ -8,6 +8,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
public interface IHealthCheckNotificationMethod : IDiscoverable
{
bool Enabled { get; }
Task SendAsync(HealthCheckResults results, CancellationToken token);
Task SendAsync(HealthCheckResults results);
}
}

View File

@@ -1,11 +1,9 @@
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.HealthCheck;
using Umbraco.Core.HealthCheck.Checks;
using Umbraco.Infrastructure.HealthCheck;
namespace Umbraco.Web.HealthCheck.NotificationMethods
@@ -22,8 +20,8 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
return;
}
var notificationMethods = healthCheckSettings.Value.NotificationSettings.NotificationMethods;
if(!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod))
var notificationMethods = healthCheckSettings.Value.Notification.NotificationMethods;
if (!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod))
{
Enabled = false;
return;
@@ -41,13 +39,13 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
public HealthCheckNotificationVerbosity Verbosity { get; protected set; }
public IReadOnlyDictionary<string, INotificationMethodSettings> Settings { get; }
public IDictionary<string, string> Settings { get; }
protected bool ShouldSend(HealthCheckResults results)
{
return Enabled && (!FailureOnly || !results.AllChecksSuccessful);
}
public abstract Task SendAsync(HealthCheckResults results, CancellationToken token);
public abstract Task SendAsync(HealthCheckResults results);
}
}

View File

@@ -1,77 +1,86 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Configuration.Models.Extensions;
using Umbraco.Core.HealthCheck;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Core.Sync;
using Umbraco.Web.HealthCheck;
using Microsoft.Extensions.Logging;
using Umbraco.Infrastructure.HealthCheck;
using Umbraco.Web.HealthCheck;
namespace Umbraco.Web.Scheduling
namespace Umbraco.Infrastructure.HostedServices
{
public class HealthCheckNotifier : RecurringTaskBase
/// <summary>
/// Hosted service implementation for recurring health check notifications.
/// </summary>
public class HealthCheckNotifier : RecurringHostedServiceBase
{
private readonly IMainDom _mainDom;
private readonly HealthChecksSettings _healthChecksSettings;
private readonly HealthCheckCollection _healthChecks;
private readonly HealthCheckNotificationMethodCollection _notifications;
private readonly IScopeProvider _scopeProvider;
private readonly IProfilingLogger _profilingLogger;
private readonly ILogger<HealthCheckNotifier> _logger;
private readonly HealthChecksSettings _healthChecksSettings;
private readonly IServerRegistrar _serverRegistrar;
private readonly IRuntimeState _runtimeState;
private readonly IServerRegistrar _serverRegistrar;
private readonly IMainDom _mainDom;
private readonly IScopeProvider _scopeProvider;
private readonly ILogger<HealthCheckNotifier> _logger;
private readonly IProfilingLogger _profilingLogger;
public HealthCheckNotifier(
IBackgroundTaskRunner<RecurringTaskBase> runner,
int delayMilliseconds,
int periodMilliseconds,
IOptions<HealthChecksSettings> healthChecksSettings,
HealthCheckCollection healthChecks,
HealthCheckNotificationMethodCollection notifications,
IMainDom mainDom,
IProfilingLogger profilingLogger ,
ILogger<HealthCheckNotifier> logger,
HealthChecksSettings healthChecksSettings,
IServerRegistrar serverRegistrar,
IRuntimeState runtimeState,
IScopeProvider scopeProvider)
: base(runner, delayMilliseconds, periodMilliseconds)
IServerRegistrar serverRegistrar,
IMainDom mainDom,
IScopeProvider scopeProvider,
ILogger<HealthCheckNotifier> logger,
IProfilingLogger profilingLogger)
: base(healthChecksSettings.Value.Notification.Period,
healthChecksSettings.Value.GetNotificationDelay(DateTime.Now, DefaultDelay))
{
_healthChecksSettings = healthChecksSettings.Value;
_healthChecks = healthChecks;
_notifications = notifications;
_runtimeState = runtimeState;
_serverRegistrar = serverRegistrar;
_mainDom = mainDom;
_scopeProvider = scopeProvider;
_runtimeState = runtimeState;
_profilingLogger = profilingLogger ;
_logger = logger;
_healthChecksSettings = healthChecksSettings;
_serverRegistrar = serverRegistrar;
_runtimeState = runtimeState;
_profilingLogger = profilingLogger;
}
public override async Task<bool> PerformRunAsync(CancellationToken token)
public override async void ExecuteAsync(object state)
{
if (_healthChecksSettings.Notification.Enabled == false)
{
return;
}
if (_runtimeState.Level != RuntimeLevel.Run)
return true; // repeat...
{
return;
}
switch (_serverRegistrar.GetCurrentServerRole())
{
case ServerRole.Replica:
_logger.LogDebug("Does not run on replica servers.");
return true; // DO repeat, server role can change
return;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return true; // DO repeat, server role can change
return;
}
// ensure we do not run if not main domain, but do NOT lock it
// Ensure we do not run if not main domain, but do NOT lock it
if (_mainDom.IsMainDom == false)
{
_logger.LogDebug("Does not run if not MainDom.");
return false; // do NOT repeat, going down
return;
}
// Ensure we use an explicit scope since we are running on a background thread and plugin health
@@ -80,13 +89,10 @@ namespace Umbraco.Web.Scheduling
using (var scope = _scopeProvider.CreateScope())
using (_profilingLogger.DebugDuration<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
{
var healthCheckConfig = _healthChecksSettings;
// Don't notify for any checks that are disabled, nor for any disabled
// just for notifications
var disabledCheckIds = healthCheckConfig.NotificationSettings.DisabledChecks
// Don't notify for any checks that are disabled, nor for any disabled just for notifications.
var disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks
.Select(x => x.Id)
.Union(healthCheckConfig.DisabledChecks
.Union(_healthChecksSettings.DisabledChecks
.Select(x => x.Id))
.Distinct()
.ToArray();
@@ -97,14 +103,12 @@ namespace Umbraco.Web.Scheduling
var results = new HealthCheckResults(checks);
results.LogResults();
// Send using registered notification methods that are enabled
// Send using registered notification methods that are enabled.
foreach (var notificationMethod in _notifications.Where(x => x.Enabled))
await notificationMethod.SendAsync(results, token);
{
await notificationMethod.SendAsync(results);
}
}
return true; // repeat
}
public override bool IsAsync => true;
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace Umbraco.Infrastructure.HostedServices
{
/// <summary>
/// Provides a base class for recurring background tasks implemented as hosted services.
/// </summary>
/// <remarks>
/// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks
/// </remarks>
public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
{
protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3);
private readonly TimeSpan _period;
private readonly TimeSpan _delay;
private Timer _timer;
protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay)
{
_period = period;
_delay = delay;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds);
return Task.CompletedTask;
}
public abstract void ExecuteAsync(object state);
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
}

View File

@@ -303,7 +303,6 @@ namespace Umbraco.Core.Runtime
composition.HealthChecks()
.Add(() => composition.TypeLoader.GetTypes<HealthCheck.HealthCheck>());
composition.WithCollectionBuilder<HealthCheckNotificationMethodCollectionBuilder>()
.Add(() => composition.TypeLoader.GetTypes<IHealthCheckNotificationMethod>());

View File

@@ -36,10 +36,7 @@ namespace Umbraco.Web.Scheduling
private readonly ILoggerFactory _loggerFactory;
private readonly IApplicationShutdownRegistry _applicationShutdownRegistry;
private readonly IScopeProvider _scopeProvider;
private readonly HealthCheckCollection _healthChecks;
private readonly HealthCheckNotificationMethodCollection _notifications;
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly HealthChecksSettings _healthChecksSettings;
private readonly IServerMessenger _serverMessenger;
private readonly IRequestAccessor _requestAccessor;
private readonly IBackofficeSecurityFactory _backofficeSecurityFactory;
@@ -59,9 +56,8 @@ namespace Umbraco.Web.Scheduling
public SchedulerComponent(IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar,
IContentService contentService, IAuditService auditService,
HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications,
IScopeProvider scopeProvider, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger profilingLogger , ILoggerFactory loggerFactory,
IApplicationShutdownRegistry applicationShutdownRegistry, IOptions<HealthChecksSettings> healthChecksSettings,
IScopeProvider scopeProvider, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger profilingLogger, ILoggerFactory loggerFactory,
IApplicationShutdownRegistry applicationShutdownRegistry,
IServerMessenger serverMessenger, IRequestAccessor requestAccessor,
IOptions<LoggingSettings> loggingSettings, IOptions<KeepAliveSettings> keepAliveSettings,
IHostingEnvironment hostingEnvironment,
@@ -73,15 +69,11 @@ namespace Umbraco.Web.Scheduling
_contentService = contentService;
_auditService = auditService;
_scopeProvider = scopeProvider;
_profilingLogger = profilingLogger ;
_profilingLogger = profilingLogger;
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<SchedulerComponent>();
_applicationShutdownRegistry = applicationShutdownRegistry;
_umbracoContextFactory = umbracoContextFactory;
_healthChecks = healthChecks;
_notifications = notifications;
_healthChecksSettings = healthChecksSettings.Value ?? throw new ArgumentNullException(nameof(healthChecksSettings));
_serverMessenger = serverMessenger;
_requestAccessor = requestAccessor;
_backofficeSecurityFactory = backofficeSecurityFactory;
@@ -98,7 +90,6 @@ namespace Umbraco.Web.Scheduling
_publishingRunner = new BackgroundTaskRunner<IBackgroundTask>("ScheduledPublishing", logger, _applicationShutdownRegistry);
_scrubberRunner = new BackgroundTaskRunner<IBackgroundTask>("LogScrubber", logger, _applicationShutdownRegistry);
_fileCleanupRunner = new BackgroundTaskRunner<IBackgroundTask>("TempFileCleanup", logger, _applicationShutdownRegistry);
_healthCheckRunner = new BackgroundTaskRunner<IBackgroundTask>("HealthCheckNotifier", logger, _applicationShutdownRegistry);
// we will start the whole process when a successful request is made
_requestAccessor.RouteAttempt += RegisterBackgroundTasksOnce;
@@ -138,10 +129,6 @@ namespace Umbraco.Web.Scheduling
tasks.Add(RegisterLogScrubber(_loggingSettings));
tasks.Add(RegisterTempFileCleanup());
var healthCheckConfig = _healthChecksSettings;
if (healthCheckConfig.NotificationSettings.Enabled)
tasks.Add(RegisterHealthCheckNotifier(healthCheckConfig, _healthChecks, _notifications, _profilingLogger));
return tasks.ToArray();
});
}
@@ -164,32 +151,6 @@ namespace Umbraco.Web.Scheduling
return task;
}
private IBackgroundTask RegisterHealthCheckNotifier(HealthChecksSettings healthCheckSettingsConfig,
HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications,
IProfilingLogger logger)
{
// If first run time not set, start with just small delay after application start
int delayInMilliseconds;
if (string.IsNullOrEmpty(healthCheckSettingsConfig.NotificationSettings.FirstRunTime))
{
delayInMilliseconds = DefaultDelayMilliseconds;
}
else
{
// Otherwise start at scheduled time
delayInMilliseconds = DateTime.Now.PeriodicMinutesFrom(healthCheckSettingsConfig.NotificationSettings.FirstRunTime) * 60 * 1000;
if (delayInMilliseconds < DefaultDelayMilliseconds)
{
delayInMilliseconds = DefaultDelayMilliseconds;
}
}
var periodInMilliseconds = healthCheckSettingsConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000;
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _mainDom, logger, _loggerFactory.CreateLogger<HealthCheckNotifier>(), _healthChecksSettings, _serverRegistrar, _runtime, _scopeProvider);
_healthCheckRunner.TryAdd(task);
return task;
}
private IBackgroundTask RegisterLogScrubber(LoggingSettings settings)
{
// log scrubbing

View File

@@ -1,6 +1,7 @@
using System;
using System.Net.Mail;
using System.Threading.Tasks;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
using MimeKit.Text;
@@ -45,15 +46,15 @@ namespace Umbraco.Core
{
OnSendEmail(new SendEmailEventArgs(message));
}
else
else if (_smtpConfigured.Value == true)
{
using (var client = new SmtpClient())
{
client.Connect(_globalSettings.Smtp.Host,
_globalSettings.Smtp.Port,
(MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
client.Connect(_globalSettings.Smtp.Host, _globalSettings.Smtp.Port);
if (!(_globalSettings.Smtp.Username is null &&
_globalSettings.Smtp.Password is null))
if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null))
{
client.Authenticate(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
}
@@ -75,14 +76,15 @@ namespace Umbraco.Core
{
OnSendEmail(new SendEmailEventArgs(message));
}
else
else if (_smtpConfigured.Value == true)
{
using (var client = new SmtpClient())
{
await client.ConnectAsync(_globalSettings.Smtp.Host, _globalSettings.Smtp.Port);
await client.ConnectAsync(_globalSettings.Smtp.Host,
_globalSettings.Smtp.Port,
(MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
if (!(_globalSettings.Smtp.Username is null &&
_globalSettings.Smtp.Password is null))
if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null))
{
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
}
@@ -125,8 +127,7 @@ namespace Umbraco.Core
private static void OnSendEmail(SendEmailEventArgs e)
{
var handler = SendEmail;
if (handler != null) handler(null, e);
SendEmail?.Invoke(null, e);
}
private MimeMessage ConstructEmailMessage(EmailMessage mailMessage)
@@ -138,10 +139,10 @@ namespace Umbraco.Core
var messageToSend = new MimeMessage
{
Subject = mailMessage.Subject,
From = { new MailboxAddress(fromEmail)},
From = { MailboxAddress.Parse(fromEmail) },
Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }
};
messageToSend.To.Add(new MailboxAddress(mailMessage.To));
messageToSend.To.Add(MailboxAddress.Parse(mailMessage.To));
return messageToSend;
}