// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Diagnostics.HealthChecks { internal class HealthCheckService : IHealthCheckService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // We're specifically going out of our way to do this at startup time. We want to make sure you // get any kind of health-check related error as early as possible. Waiting until someone // actually tries to **run** health checks would be real baaaaad. using (var scope = _scopeFactory.CreateScope()) { var healthChecks = scope.ServiceProvider.GetRequiredService>(); EnsureNoDuplicates(healthChecks); } } public Task CheckHealthAsync(CancellationToken cancellationToken = default) => CheckHealthAsync(predicate: null, cancellationToken); public async Task CheckHealthAsync( Func predicate, CancellationToken cancellationToken = default) { using (var scope = _scopeFactory.CreateScope()) { var healthChecks = scope.ServiceProvider.GetRequiredService>(); var results = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var healthCheck in healthChecks) { if (predicate != null && !predicate(healthCheck)) { continue; } cancellationToken.ThrowIfCancellationRequested(); // If the health check does things like make Database queries using EF or backend HTTP calls, // it may be valuable to know that logs it generates are part of a health check. So we start a scope. using (_logger.BeginScope(new HealthCheckLogScope(healthCheck.Name))) { HealthCheckResult result; try { Log.HealthCheckBegin(_logger, healthCheck); var stopwatch = ValueStopwatch.StartNew(); result = await healthCheck.CheckHealthAsync(cancellationToken); Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime()); } catch (Exception ex) { Log.HealthCheckError(_logger, healthCheck, ex); result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null); } // This can only happen if the result is default(HealthCheckResult) if (result.Status == HealthCheckStatus.Unknown) { // This is different from the case above. We throw here because a health check is doing something specifically incorrect. throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown"); } results[healthCheck.Name] = result; } } return new CompositeHealthCheckResult(results); } } private static void EnsureNoDuplicates(IEnumerable healthChecks) { // Scan the list for duplicate names to provide a better error if there are duplicates. var duplicateNames = healthChecks .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) .Where(g => g.Count() > 1) .Select(g => g.Key) .ToList(); if (duplicateNames.Count > 0) { throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(healthChecks)); } } private static class Log { public static class EventIds { public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); } private static readonly Action _healthCheckBegin = LoggerMessage.Define( LogLevel.Debug, EventIds.HealthCheckBegin, "Running health check {HealthCheckName}"); private static readonly Action _healthCheckEnd = LoggerMessage.Define( LogLevel.Debug, EventIds.HealthCheckEnd, "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); private static readonly Action _healthCheckError = LoggerMessage.Define( LogLevel.Error, EventIds.HealthCheckError, "Health check {HealthCheckName} threw an unhandled exception"); public static void HealthCheckBegin(ILogger logger, IHealthCheck healthCheck) { _healthCheckBegin(logger, healthCheck.Name, null); } public static void HealthCheckEnd(ILogger logger, IHealthCheck healthCheck, HealthCheckResult result, TimeSpan duration) { _healthCheckEnd(logger, healthCheck.Name, duration.TotalMilliseconds, result.Status, null); } public static void HealthCheckError(ILogger logger, IHealthCheck healthCheck, Exception exception) { _healthCheckError(logger, healthCheck.Name, exception); } } } }