diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/AzureFunctionHealthCheck.csproj b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/AzureFunctionHealthCheck.csproj new file mode 100644 index 0000000..a454366 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/AzureFunctionHealthCheck.csproj @@ -0,0 +1,13 @@ + + + + net5.0 + TaleLearnCode.AzureFunctionHealthCheck + TaleLearnCode.AzureFunctionHealthCheck + + + + + + + diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/DefaultHealthCheckService.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/DefaultHealthCheckService.cs new file mode 100644 index 0000000..df3d646 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/DefaultHealthCheckService.cs @@ -0,0 +1,121 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + public class DefaultHealthCheckService : HealthCheckService + { + + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DefaultHealthCheckService( + IServiceScopeFactory serviceScopeFactory, + IOptions options, + ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(options)); + ValidateRegistrations(_options.Value.Registrations); + } + + /// + /// Performs the health check. + /// + /// The predicate. + /// The cancellation token. + /// + public override async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) + { + + ICollection registrations = _options.Value.Registrations; + + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + + HealthCheckContext healthCheckContext = new(); + Dictionary entries = new(StringComparer.OrdinalIgnoreCase); + + ValueStopwatch totalTime = ValueStopwatch.StartNew(); + Log.HealthCheckProcessingBegin(_logger); + + foreach (HealthCheckRegistration registration in registrations) + { + if (predicate != null && !predicate(registration)) + continue; + + cancellationToken.ThrowIfCancellationRequested(); + + IHealthCheck healthCheck = registration.Factory(scope.ServiceProvider); + + ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + healthCheckContext.Registration = registration; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + try + { + HealthCheckResult result = await healthCheck.CheckHealthAsync(healthCheckContext, cancellationToken); + TimeSpan duration = stopwatch.Elapsed; + + entry = new HealthReportEntry( + status: result.Status, + description: result.Description, + duration: duration, + exception: result.Exception, + data: result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, duration); + Log.HealthCheckData(_logger, registration, entry); + } + catch (Exception ex) when (ex as OperationCanceledException == null) + { + var duration = stopwatch.Elapsed; + entry = new HealthReportEntry( + status: HealthStatus.Unhealthy, + description: ex.Message, + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); + } + entries[registration.Name] = entry; + } + + var totalElapsedTime = totalTime.Elapsed; + var report = new HealthReport(entries, totalElapsedTime); + Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); + return report; + + } + + private static void ValidateRegistrations(IEnumerable registrations) + { + + List duplicateNames = registrations + .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateNames.Any()) + throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations)); + + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/EventIds.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/EventIds.cs new file mode 100644 index 0000000..8d38517 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/EventIds.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + internal static class EventIds + { + public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin"); + public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd"); + public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin"); + public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd"); + public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError"); + public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData"); + } +} diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthCheckDataLogValue.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthCheckDataLogValue.cs new file mode 100644 index 0000000..dfd60fd --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthCheckDataLogValue.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + /// + /// Represents value of a health check data log. + /// + /// + internal class HealthCheckDataLogValue : IReadOnlyList> + { + private readonly string _name; + private readonly List> _values; + private string _formatted; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The values. + public HealthCheckDataLogValue(string name, IReadOnlyDictionary values) + { + _name = name; + _values = values.ToList(); + _values.Add(new KeyValuePair("HealthCheckName", name)); + } + + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + /// index + public KeyValuePair this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new IndexOutOfRangeException(nameof(index)); + return _values[index]; + } + } + + public int Count => _values.Count; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator> GetEnumerator() + { + return _values.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _values.GetEnumerator(); + } + + /// + /// Converts the to a string. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (_formatted == null) + { + var builder = new StringBuilder(); + builder.AppendLine($"Health check data for {_name}:"); + + var values = _values; + for (var i = 0; i < values.Count; i++) + { + var kvp = values[i]; + builder.Append(" "); + builder.Append(kvp.Key); + builder.Append(": "); + + builder.AppendLine(kvp.Value?.ToString()); + } + + _formatted = builder.ToString(); + } + + return _formatted; + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthCheckServiceFunctionExtension.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthCheckServiceFunctionExtension.cs new file mode 100644 index 0000000..1ae47f0 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthCheckServiceFunctionExtension.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + /// + /// Provides extension methods for registering . + /// + public static class HealthCheckServiceFunctionExtension + { + + /// + /// Adds the to the container, using the provided delegates to register. + /// + /// The services. + public static IHealthChecksBuilder AddFunctionHealthChecks(this IServiceCollection services) + { + services.TryAddSingleton(); + return new HealthChecksBuilder(services); + } + + } +} diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthChecksBuilder.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthChecksBuilder.cs new file mode 100644 index 0000000..ec9d0a3 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/HealthChecksBuilder.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + /// + /// A builder used to register health checks. + /// + /// + public class HealthChecksBuilder : IHealthChecksBuilder + { + + /// + /// Initializes a new instance of the class. + /// + /// The into which instances should be registered. + public HealthChecksBuilder(IServiceCollection services) + { + Services = services; + } + + /// + /// Gets the into which instances should be registered. + /// + public IServiceCollection Services { get; } + + /// + /// Adds a for a health check. + /// + /// The . + /// An initialized + /// registration + public IHealthChecksBuilder Add(HealthCheckRegistration registration) + { + if (registration == default) throw new ArgumentNullException(nameof(registration)); + Services.Configure(options => + { + options.Registrations.Add(registration); + }); + return this; + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/Log.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/Log.cs new file mode 100644 index 0000000..503e6a8 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/Log.cs @@ -0,0 +1,105 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using System; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + internal static class Log + { + private static readonly Action _healthCheckProcessingBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckProcessingBegin, + "Running health checks"); + + private static readonly Action _healthCheckProcessingEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckProcessingEnd, + "Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}"); + + private static readonly Action _healthCheckBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckBegin, + "Running health check {HealthCheckName}"); + + // These are separate so they can have different log levels + private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'"; + + private static readonly Action _healthCheckEndHealthy = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndDegraded = LoggerMessage.Define( + LogLevel.Warning, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndUnhealthy = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndFailed = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckError = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckError, + "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms"); + + public static void HealthCheckProcessingBegin(ILogger logger) + { + _healthCheckProcessingBegin(logger, null); + } + + public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration) + { + _healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null); + } + + public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) + { + _healthCheckBegin(logger, registration.Name, null); + } + + public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) + { + switch (entry.Status) + { + case HealthStatus.Healthy: + _healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Degraded: + _healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Unhealthy: + _healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + } + } + + public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) + { + _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); + } + + public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry) + { + if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug)) + { + logger.Log( + LogLevel.Debug, + EventIds.HealthCheckData, + new HealthCheckDataLogValue(registration.Name, entry.Data), + null, + (state, ex) => state.ToString()); + } + } + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/ValueStopwatch.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/ValueStopwatch.cs new file mode 100644 index 0000000..7d3bac6 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck-old/ValueStopwatch.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + public struct ValueStopwatch + { + + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + private long value; + + /// + /// Creates and starts a new stopwatch instance. + /// + /// A new stopwatch which has been started. + public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); + + /// + /// Initializes a new instance of the struct. + /// + /// The timestamp. + private ValueStopwatch(long timestamp) + { + value = timestamp; + } + + /// + /// Gets a value indicating whether this instance is running. + /// + /// + /// true if this instance is running; otherwise, false. + /// + public bool IsRunning => value > 0; + + /// + /// Gets the elapsed ticks. + /// + /// + /// The elapsed ticks. + /// + public long ElapsedTicks + { + get + { + long timestamp = value; + + long delta; + if (IsRunning) + { + long start = timestamp; + long end = Stopwatch.GetTimestamp(); + delta = end - start; + } + else + { + delta = -timestamp; + } + return (long)(delta * TimestampToTicks); + } + + } + + public TimeSpan Elapsed => TimeSpan.FromTicks(ElapsedTicks); + + /// + /// Gets the raw timestamp value. + /// + /// A long representing the value of the raw timestamp. + public long GetRawTimestamp() => value; + + /// + /// Starts this stopwatch. + /// + public void Start() + { + long timestamp = value; + + if (IsRunning) return; // Already stated; nothing to do + + long newValue = Stopwatch.GetTimestamp() + timestamp; + if (newValue == 0) newValue = 1; + value = newValue; + } + + /// + /// Restarts the stopwatch. + /// + public void Restart() => value = Stopwatch.GetTimestamp(); + + /// + /// Stops this stopwatch. + /// + public void Stop() + { + long timestamp = value; + if (!IsRunning) return; + + long end = Stopwatch.GetTimestamp(); + long delta = end - timestamp; + + value = -delta; + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck.sln b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck.sln new file mode 100644 index 0000000..4ee8ada --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31213.239 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunctionHealthCheck", "AzureFunctionHealthCheck\AzureFunctionHealthCheck.csproj", "{EDC5DDE0-101A-4D80-9B65-322C50D8FD7A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EDC5DDE0-101A-4D80-9B65-322C50D8FD7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDC5DDE0-101A-4D80-9B65-322C50D8FD7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDC5DDE0-101A-4D80-9B65-322C50D8FD7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDC5DDE0-101A-4D80-9B65-322C50D8FD7A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B243C6A1-104D-436F-A7C6-3A2371023E45} + EndGlobalSection +EndGlobal diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/AzureFunctionHealthCheck.csproj b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/AzureFunctionHealthCheck.csproj new file mode 100644 index 0000000..a10c28b --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/AzureFunctionHealthCheck.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + TaleLearnCode.AzureFunctionHealthCheck + TaleLearnCode.AzureFunctionHealthCheck + true + 0.0.0-pre + TaleLearnCode + Green Events & Technology, LLC. + Azure Functions HealthCheck + Provides the mechanism to allow Azure Functions to use Microsoft HealthCheck diagnostics. + Copyright ©2021 by Green Events & Technology, LLC. All rights reserved. + MIT + https://github.com/TaleLearnCode/AzureFunctionHealthCheck + https://github.com/TaleLearnCode/AzureFunctionHealthCheck + git + Azure Functions HealthCheck + Initial beta release + en-US + + + + + + \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/DefaultHealthCheckService.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/DefaultHealthCheckService.cs new file mode 100644 index 0000000..df3d646 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/DefaultHealthCheckService.cs @@ -0,0 +1,121 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + public class DefaultHealthCheckService : HealthCheckService + { + + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DefaultHealthCheckService( + IServiceScopeFactory serviceScopeFactory, + IOptions options, + ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(options)); + ValidateRegistrations(_options.Value.Registrations); + } + + /// + /// Performs the health check. + /// + /// The predicate. + /// The cancellation token. + /// + public override async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) + { + + ICollection registrations = _options.Value.Registrations; + + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + + HealthCheckContext healthCheckContext = new(); + Dictionary entries = new(StringComparer.OrdinalIgnoreCase); + + ValueStopwatch totalTime = ValueStopwatch.StartNew(); + Log.HealthCheckProcessingBegin(_logger); + + foreach (HealthCheckRegistration registration in registrations) + { + if (predicate != null && !predicate(registration)) + continue; + + cancellationToken.ThrowIfCancellationRequested(); + + IHealthCheck healthCheck = registration.Factory(scope.ServiceProvider); + + ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + healthCheckContext.Registration = registration; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + try + { + HealthCheckResult result = await healthCheck.CheckHealthAsync(healthCheckContext, cancellationToken); + TimeSpan duration = stopwatch.Elapsed; + + entry = new HealthReportEntry( + status: result.Status, + description: result.Description, + duration: duration, + exception: result.Exception, + data: result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, duration); + Log.HealthCheckData(_logger, registration, entry); + } + catch (Exception ex) when (ex as OperationCanceledException == null) + { + var duration = stopwatch.Elapsed; + entry = new HealthReportEntry( + status: HealthStatus.Unhealthy, + description: ex.Message, + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); + } + entries[registration.Name] = entry; + } + + var totalElapsedTime = totalTime.Elapsed; + var report = new HealthReport(entries, totalElapsedTime); + Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); + return report; + + } + + private static void ValidateRegistrations(IEnumerable registrations) + { + + List duplicateNames = registrations + .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateNames.Any()) + throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations)); + + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/EventIds.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/EventIds.cs new file mode 100644 index 0000000..8d38517 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/EventIds.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + internal static class EventIds + { + public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin"); + public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd"); + public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin"); + public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd"); + public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError"); + public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData"); + } +} diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthCheckDataLogValue.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthCheckDataLogValue.cs new file mode 100644 index 0000000..dfd60fd --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthCheckDataLogValue.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + /// + /// Represents value of a health check data log. + /// + /// + internal class HealthCheckDataLogValue : IReadOnlyList> + { + private readonly string _name; + private readonly List> _values; + private string _formatted; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The values. + public HealthCheckDataLogValue(string name, IReadOnlyDictionary values) + { + _name = name; + _values = values.ToList(); + _values.Add(new KeyValuePair("HealthCheckName", name)); + } + + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + /// index + public KeyValuePair this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new IndexOutOfRangeException(nameof(index)); + return _values[index]; + } + } + + public int Count => _values.Count; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator> GetEnumerator() + { + return _values.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _values.GetEnumerator(); + } + + /// + /// Converts the to a string. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (_formatted == null) + { + var builder = new StringBuilder(); + builder.AppendLine($"Health check data for {_name}:"); + + var values = _values; + for (var i = 0; i < values.Count; i++) + { + var kvp = values[i]; + builder.Append(" "); + builder.Append(kvp.Key); + builder.Append(": "); + + builder.AppendLine(kvp.Value?.ToString()); + } + + _formatted = builder.ToString(); + } + + return _formatted; + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthCheckServiceFunctionExtension.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthCheckServiceFunctionExtension.cs new file mode 100644 index 0000000..1ae47f0 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthCheckServiceFunctionExtension.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + /// + /// Provides extension methods for registering . + /// + public static class HealthCheckServiceFunctionExtension + { + + /// + /// Adds the to the container, using the provided delegates to register. + /// + /// The services. + public static IHealthChecksBuilder AddFunctionHealthChecks(this IServiceCollection services) + { + services.TryAddSingleton(); + return new HealthChecksBuilder(services); + } + + } +} diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthChecksBuilder.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthChecksBuilder.cs new file mode 100644 index 0000000..ec9d0a3 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/HealthChecksBuilder.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + /// + /// A builder used to register health checks. + /// + /// + public class HealthChecksBuilder : IHealthChecksBuilder + { + + /// + /// Initializes a new instance of the class. + /// + /// The into which instances should be registered. + public HealthChecksBuilder(IServiceCollection services) + { + Services = services; + } + + /// + /// Gets the into which instances should be registered. + /// + public IServiceCollection Services { get; } + + /// + /// Adds a for a health check. + /// + /// The . + /// An initialized + /// registration + public IHealthChecksBuilder Add(HealthCheckRegistration registration) + { + if (registration == default) throw new ArgumentNullException(nameof(registration)); + Services.Configure(options => + { + options.Registrations.Add(registration); + }); + return this; + } + + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/Log.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/Log.cs new file mode 100644 index 0000000..503e6a8 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/Log.cs @@ -0,0 +1,105 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using System; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + internal static class Log + { + private static readonly Action _healthCheckProcessingBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckProcessingBegin, + "Running health checks"); + + private static readonly Action _healthCheckProcessingEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckProcessingEnd, + "Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}"); + + private static readonly Action _healthCheckBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckBegin, + "Running health check {HealthCheckName}"); + + // These are separate so they can have different log levels + private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'"; + + private static readonly Action _healthCheckEndHealthy = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndDegraded = LoggerMessage.Define( + LogLevel.Warning, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndUnhealthy = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndFailed = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckError = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckError, + "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms"); + + public static void HealthCheckProcessingBegin(ILogger logger) + { + _healthCheckProcessingBegin(logger, null); + } + + public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration) + { + _healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null); + } + + public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) + { + _healthCheckBegin(logger, registration.Name, null); + } + + public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) + { + switch (entry.Status) + { + case HealthStatus.Healthy: + _healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Degraded: + _healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Unhealthy: + _healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + } + } + + public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) + { + _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); + } + + public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry) + { + if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug)) + { + logger.Log( + LogLevel.Debug, + EventIds.HealthCheckData, + new HealthCheckDataLogValue(registration.Name, entry.Data), + null, + (state, ex) => state.ToString()); + } + } + } + +} \ No newline at end of file diff --git a/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/ValueStopwatch.cs b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/ValueStopwatch.cs new file mode 100644 index 0000000..7d3bac6 --- /dev/null +++ b/src/AzureFunctionHealthCheck/AzureFunctionHealthCheck/ValueStopwatch.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics; + +namespace TaleLearnCode.AzureFunctionHealthCheck +{ + + public struct ValueStopwatch + { + + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + private long value; + + /// + /// Creates and starts a new stopwatch instance. + /// + /// A new stopwatch which has been started. + public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); + + /// + /// Initializes a new instance of the struct. + /// + /// The timestamp. + private ValueStopwatch(long timestamp) + { + value = timestamp; + } + + /// + /// Gets a value indicating whether this instance is running. + /// + /// + /// true if this instance is running; otherwise, false. + /// + public bool IsRunning => value > 0; + + /// + /// Gets the elapsed ticks. + /// + /// + /// The elapsed ticks. + /// + public long ElapsedTicks + { + get + { + long timestamp = value; + + long delta; + if (IsRunning) + { + long start = timestamp; + long end = Stopwatch.GetTimestamp(); + delta = end - start; + } + else + { + delta = -timestamp; + } + return (long)(delta * TimestampToTicks); + } + + } + + public TimeSpan Elapsed => TimeSpan.FromTicks(ElapsedTicks); + + /// + /// Gets the raw timestamp value. + /// + /// A long representing the value of the raw timestamp. + public long GetRawTimestamp() => value; + + /// + /// Starts this stopwatch. + /// + public void Start() + { + long timestamp = value; + + if (IsRunning) return; // Already stated; nothing to do + + long newValue = Stopwatch.GetTimestamp() + timestamp; + if (newValue == 0) newValue = 1; + value = newValue; + } + + /// + /// Restarts the stopwatch. + /// + public void Restart() => value = Stopwatch.GetTimestamp(); + + /// + /// Stops this stopwatch. + /// + public void Stop() + { + long timestamp = value; + if (!IsRunning) return; + + long end = Stopwatch.GetTimestamp(); + long delta = end - timestamp; + + value = -delta; + } + + } + +} \ No newline at end of file