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

@@ -0,0 +1,41 @@
using System;
using NUnit.Framework;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Configuration.Models.Extensions;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Extensions
{
[TestFixture]
public class HealthCheckSettingsExtensionsTests
{
[Test]
public void Returns_Notification_Delay_From_Provided_Time()
{
var settings = new HealthChecksSettings
{
Notification = new HealthChecksNotificationSettings
{
FirstRunTime = "1230",
}
};
var now = DateTime.Now.Date.AddHours(12);
var result = settings.GetNotificationDelay(now, TimeSpan.Zero);
Assert.AreEqual(30, result.Minutes);
}
[Test]
public void Returns_Notification_Delay_From_Default_When_Provided_Time_Too_Close_To_Current_Time()
{
var settings = new HealthChecksSettings
{
Notification = new HealthChecksNotificationSettings
{
FirstRunTime = "1230",
}
};
var now = DateTime.Now.Date.AddHours(12).AddMinutes(25);
var result = settings.GetNotificationDelay(now, TimeSpan.FromMinutes(10));
Assert.AreEqual(10, result.Minutes);
}
}
}

View File

@@ -1,7 +1,6 @@
using NUnit.Framework;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Configuration.Models.Validation;
using Umbraco.Tests.Common.Builders;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validation
{

View File

@@ -0,0 +1,42 @@
using System;
using NUnit.Framework;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Configuration.Models.Validation;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validation
{
[TestFixture]
public class HealthChecksSettingsValidationTests
{
[Test]
public void Returns_Success_ForValid_Configuration()
{
var validator = new HealthChecksSettingsValidator();
var options = BuildHealthChecksSettings();
var result = validator.Validate("settings", options);
Assert.True(result.Succeeded);
}
[Test]
public void Returns_Fail_For_Configuration_With_Invalid_Notification_FirstRunTime()
{
var validator = new HealthChecksSettingsValidator();
var options = BuildHealthChecksSettings(firstRunTime: "25:00");
var result = validator.Validate("settings", options);
Assert.False(result.Succeeded);
}
private static HealthChecksSettings BuildHealthChecksSettings(string firstRunTime = "12:00")
{
return new HealthChecksSettings
{
Notification = new HealthChecksNotificationSettings
{
Enabled = true,
FirstRunTime = firstRunTime,
Period = TimeSpan.FromHours(1),
}
};
}
}
}

View File

@@ -293,14 +293,23 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper
Assert.AreEqual(expected, output);
}
#region Cases
[TestCase("val$id!ate|this|str'ing", "$!'", '-', "val-id-ate|this|str-ing")]
[TestCase("val$id!ate|this|str'ing", "$!'", '*', "val*id*ate|this|str*ing")]
#endregion
public void ReplaceManyByOneChar(string input, string toReplace, char replacement, string expected)
{
var output = input.ReplaceMany(toReplace.ToArray(), replacement);
Assert.AreEqual(expected, output);
}
[TestCase("", false)]
[TestCase("12:34", true)]
[TestCase("1:14:23", true)]
[TestCase("25:03", false)]
[TestCase("18:61", false)]
public void IsValidTimeSpan(string input, bool expected)
{
var result = input.IsValidTimeSpan();
Assert.AreEqual(expected, result);
}
}
}

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.HealthCheck;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Core.Sync;
using Umbraco.Infrastructure.HealthCheck;
using Umbraco.Infrastructure.HostedServices;
using Umbraco.Web.HealthCheck;
using Umbraco.Web.HealthCheck.NotificationMethods;
namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices
{
[TestFixture]
public class HealthCheckNotifierTests
{
private Mock<IHealthCheckNotificationMethod> _mockNotificationMethod;
private const string Check1Id = "00000000-0000-0000-0000-000000000001";
private const string Check2Id = "00000000-0000-0000-0000-000000000002";
private const string Check3Id = "00000000-0000-0000-0000-000000000003";
[Test]
public void Does_Not_Execute_When_Not_Enabled()
{
var sut = CreateHealthCheckNotifier(enabled: false);
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Never);
}
[Test]
public void Does_Not_Execute_When_Runtime_State_Is_Not_Run()
{
var sut = CreateHealthCheckNotifier(runtimeLevel: RuntimeLevel.Boot);
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Never);
}
[Test]
public void Does_Not_Execute_When_Server_Role_Is_Replica()
{
var sut = CreateHealthCheckNotifier(serverRole: ServerRole.Replica);
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Never);
}
[Test]
public void Does_Not_Execute_When_Server_Role_Is_Unknown()
{
var sut = CreateHealthCheckNotifier(serverRole: ServerRole.Unknown);
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Never);
}
[Test]
public void Does_Not_Execute_When_Not_Main_Dom()
{
var sut = CreateHealthCheckNotifier(isMainDom: false);
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Never);
}
[Test]
public void Does_Not_Execute_With_No_Enabled_Notification_Methods()
{
var sut = CreateHealthCheckNotifier(notificationEnabled: false);
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Never);
}
[Test]
public void Executes_With_Enabled_Notification_Methods()
{
var sut = CreateHealthCheckNotifier();
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny<HealthCheckResults>()), Times.Once);
}
[Test]
public void Executes_Only_Enabled_Checks()
{
var sut = CreateHealthCheckNotifier();
sut.ExecuteAsync(null);
_mockNotificationMethod.Verify(x => x.SendAsync(It.Is<HealthCheckResults>(
y => y.ResultsAsDictionary.Count == 1 && y.ResultsAsDictionary.ContainsKey("Check1"))), Times.Once);
}
private HealthCheckNotifier CreateHealthCheckNotifier(
bool enabled = true,
RuntimeLevel runtimeLevel = RuntimeLevel.Run,
ServerRole serverRole = ServerRole.Single,
bool isMainDom = true,
bool notificationEnabled = true)
{
var settings = new HealthChecksSettings
{
Notification = new HealthChecksNotificationSettings
{
Enabled = enabled,
DisabledChecks = new List<DisabledHealthCheckSettings>
{
new DisabledHealthCheckSettings { Id = Guid.Parse(Check3Id) }
}
},
DisabledChecks = new List<DisabledHealthCheckSettings>
{
new DisabledHealthCheckSettings { Id = Guid.Parse(Check2Id) }
}
};
var checks = new HealthCheckCollection(new List<HealthCheck>
{
new TestHealthCheck1(),
new TestHealthCheck2(),
new TestHealthCheck3(),
});
_mockNotificationMethod = new Mock<IHealthCheckNotificationMethod>();
_mockNotificationMethod.SetupGet(x => x.Enabled).Returns(notificationEnabled);
var notifications = new HealthCheckNotificationMethodCollection(new List<IHealthCheckNotificationMethod> { _mockNotificationMethod.Object });
var mockRunTimeState = new Mock<IRuntimeState>();
mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel);
var mockServerRegistrar = new Mock<IServerRegistrar>();
mockServerRegistrar.Setup(x => x.GetCurrentServerRole()).Returns(serverRole);
var mockMainDom = new Mock<IMainDom>();
mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom);
var mockScopeProvider = new Mock<IScopeProvider>();
var mockLogger = new Mock<ILogger<HealthCheckNotifier>>();
var mockProfilingLogger = new Mock<IProfilingLogger>();
return new HealthCheckNotifier(Options.Create(settings), checks, notifications,
mockRunTimeState.Object, mockServerRegistrar.Object, mockMainDom.Object, mockScopeProvider.Object,
mockLogger.Object, mockProfilingLogger.Object);
}
[HealthCheck(Check1Id, "Check1")]
private class TestHealthCheck1 : TestHealthCheck
{
}
[HealthCheck(Check2Id, "Check2")]
private class TestHealthCheck2 : TestHealthCheck
{
}
[HealthCheck(Check3Id, "Check3")]
private class TestHealthCheck3 : TestHealthCheck
{
}
private class TestHealthCheck : HealthCheck
{
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
{
return new HealthCheckStatus("Check message");
}
public override IEnumerable<HealthCheckStatus> GetStatus()
{
return Enumerable.Empty<HealthCheckStatus>();
}
}
}
}