Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AssemblyName>TaleLearnCode.AzureFunctionHealthCheck</AssemblyName>
<RootNamespace>TaleLearnCode.AzureFunctionHealthCheck</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.5" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<HealthCheckServiceOptions> _options;
private readonly ILogger<DefaultHealthCheckService> _logger;

public DefaultHealthCheckService(
IServiceScopeFactory serviceScopeFactory,
IOptions<HealthCheckServiceOptions> options,
ILogger<DefaultHealthCheckService> 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);
}

/// <summary>
/// Performs the health check.
/// </summary>
/// <param name="predicate">The predicate.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
public override async Task<HealthReport> CheckHealthAsync(
Func<HealthCheckRegistration, bool> predicate,
CancellationToken cancellationToken = default)
{

ICollection<HealthCheckRegistration> registrations = _options.Value.Registrations;

using IServiceScope scope = _serviceScopeFactory.CreateScope();

HealthCheckContext healthCheckContext = new();
Dictionary<string, HealthReportEntry> 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<HealthCheckRegistration> registrations)
{

List<string> 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));

}

}

}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TaleLearnCode.AzureFunctionHealthCheck
{

/// <summary>
/// Represents value of a health check data log.
/// </summary>
/// <seealso cref="IReadOnlyList{KeyValuePair{string, object}}" />
internal class HealthCheckDataLogValue : IReadOnlyList<KeyValuePair<string, object>>
{
private readonly string _name;
private readonly List<KeyValuePair<string, object>> _values;
private string _formatted;

/// <summary>
/// Initializes a new instance of the <see cref="HealthCheckDataLogValue"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="values">The values.</param>
public HealthCheckDataLogValue(string name, IReadOnlyDictionary<string, object> values)
{
_name = name;
_values = values.ToList();
_values.Add(new KeyValuePair<string, object>("HealthCheckName", name));
}

/// <summary>
/// Gets the <see cref="KeyValuePair{string, Object}"/> at the specified index.
/// </summary>
/// <value>
/// The <see cref="KeyValuePair{String, Object}"/>.
/// </value>
/// <param name="index">The index.</param>
/// <returns></returns>
/// <exception cref="IndexOutOfRangeException">index</exception>
public KeyValuePair<string, object> this[int index]
{
get
{
if (index < 0 || index >= Count)
throw new IndexOutOfRangeException(nameof(index));
return _values[index];
}
}

public int Count => _values.Count;

/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return _values.GetEnumerator();
}

/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return _values.GetEnumerator();
}

/// <summary>
/// Converts the <see cref="HealthCheckDataLogValue"/> to a string.
/// </summary>
/// <returns>
/// A <see cref="String" /> that represents this instance.
/// </returns>
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;
}

}

}
Original file line number Diff line number Diff line change
@@ -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
{

/// <summary>
/// Provides extension methods for registering <see cref="HealthCheckService"/>.
/// </summary>
public static class HealthCheckServiceFunctionExtension
{

/// <summary>
/// Adds the <see cref="HealthCheckService"/> to the container, using the provided delegates to register.
/// </summary>
/// <param name="services">The services.</param>
public static IHealthChecksBuilder AddFunctionHealthChecks(this IServiceCollection services)
{
services.TryAddSingleton<HealthCheckService, DefaultHealthCheckService>();
return new HealthChecksBuilder(services);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;

namespace TaleLearnCode.AzureFunctionHealthCheck
{

/// <summary>
/// A builder used to register health checks.
/// </summary>
/// <seealso cref="IHealthChecksBuilder" />
public class HealthChecksBuilder : IHealthChecksBuilder
{

/// <summary>
/// Initializes a new instance of the <see cref="HealthChecksBuilder"/> class.
/// </summary>
/// <param name="services">The <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" /> into which <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" /> instances should be registered.</param>
public HealthChecksBuilder(IServiceCollection services)
{
Services = services;
}

/// <summary>
/// Gets the <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" /> into which <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" /> instances should be registered.
/// </summary>
public IServiceCollection Services { get; }

/// <summary>
/// Adds a <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration" /> for a health check.
/// </summary>
/// <param name="registration">The <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration" />.</param>
/// <returns>An initialized <see cref="HealthChecksBuilder"/></returns>
/// <exception cref="ArgumentNullException">registration</exception>
public IHealthChecksBuilder Add(HealthCheckRegistration registration)
{
if (registration == default) throw new ArgumentNullException(nameof(registration));
Services.Configure<HealthCheckServiceOptions>(options =>
{
options.Registrations.Add(registration);
});
return this;
}

}

}
Loading