File: TelemetryHealthChecksPublisherTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests\Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests.csproj (Microsoft.Extensions.Diagnostics.HealthChecks.Common.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Xunit;
 
namespace Microsoft.Extensions.Diagnostics.HealthChecks.Test;
 
public class TelemetryHealthChecksPublisherTests
{
    private const string HealthReportMetricName = "dotnet.health_check.reports";
    private const string UnhealthyHealthCheckMetricName = "dotnet.health_check.unhealthy_checks";
 
    public static TheoryData<List<HealthStatus>, bool, int, string, LogLevel, string> PublishAsyncArgs => new()
    {
        {
            new List<HealthStatus> { HealthStatus.Healthy },
            false,
            1,
            "Process reporting healthy: Healthy.",
            LogLevel.Debug,
            HealthStatus.Healthy.ToString()
        },
        {
            new List<HealthStatus> { HealthStatus.Degraded },
            true,
            1,
            "Process reporting unhealthy: Degraded. Health check entries are id0: {status: Degraded, description: desc0}",
            LogLevel.Warning,
            HealthStatus.Degraded.ToString()
        },
        {
            new List<HealthStatus> { HealthStatus.Unhealthy },
            false,
            1,
            "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Unhealthy, description: desc0}",
            LogLevel.Warning,
            HealthStatus.Unhealthy.ToString()
        },
        {
            new List<HealthStatus> { HealthStatus.Healthy, HealthStatus.Healthy },
            true,
            0,
            "Process reporting healthy: Healthy.",
            LogLevel.Debug,
            HealthStatus.Healthy.ToString()
        },
        {
            new List<HealthStatus> { HealthStatus.Healthy, HealthStatus.Unhealthy },
            true,
            1,
            "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Healthy, description: desc0}, id1: {status: Unhealthy, description: desc1}",
            LogLevel.Warning,
            HealthStatus.Unhealthy.ToString()
        },
        {
            new List<HealthStatus> { HealthStatus.Healthy, HealthStatus.Degraded, HealthStatus.Unhealthy },
            false,
            1,
            "Process reporting unhealthy: Unhealthy. Health check entries are " +
            "id0: {status: Healthy, description: desc0}, id1: {status: Degraded, description: desc1}, id2: {status: Unhealthy, description: desc2}",
            LogLevel.Warning,
            HealthStatus.Unhealthy.ToString()
        },
    };
 
    [Theory]
    [MemberData(nameof(PublishAsyncArgs))]
    public async Task PublishAsync(
        IList<HealthStatus> healthStatuses,
        bool logOnlyUnhealthy,
        int expectedLogCount,
        string expectedLogMessage,
        LogLevel expectedLogLevel,
        string expectedMetricStatus)
    {
        using var meter = new Meter(nameof(PublishAsync));
        var metrics = GetMockedMetrics(meter);
        using var healthyMetricCollector = new MetricCollector<long>(meter, HealthReportMetricName);
        using var unhealthyMetricCollector = new MetricCollector<long>(meter, UnhealthyHealthCheckMetricName);
 
        var logger = new FakeLogger<TelemetryHealthCheckPublisher>();
        var collector = logger.Collector;
 
        var options = Options.Options.Create(new TelemetryHealthCheckPublisherOptions
        {
            LogOnlyUnhealthy = logOnlyUnhealthy,
        });
 
        var publisher = new TelemetryHealthCheckPublisher(metrics, logger, options);
 
        await publisher.PublishAsync(CreateHealthReport(healthStatuses), CancellationToken.None);
        Assert.Equal(expectedLogCount, collector.Count);
        if (expectedLogCount > 0)
        {
            Assert.Equal(expectedLogMessage, collector.LatestRecord.Message);
            Assert.Equal(expectedLogLevel, collector.LatestRecord.Level);
        }
 
        var latest = healthyMetricCollector.LastMeasurement;
 
        Assert.NotNull(latest);
 
        latest.Value.Should().Be(1);
        latest.Tags.Should().ContainKey("dotnet.health_check.status").WhoseValue.Should().Be(expectedMetricStatus);
 
        var unhealthyCounters = unhealthyMetricCollector.GetMeasurementSnapshot();
 
        for (int i = 0; i < healthStatuses.Count; i++)
        {
            var healthStatus = healthStatuses[i];
            if (healthStatus != HealthStatus.Healthy)
            {
                Assert.Equal(1, GetValue(unhealthyCounters, GetKey(i), healthStatuses[i].ToString()));
            }
        }
    }
 
    [Fact]
    public void Ctor_ThrowsWhenOptionsValueNull()
    {
        using var meter = new Meter(nameof(Ctor_ThrowsWhenOptionsValueNull));
        var metrics = GetMockedMetrics(meter);
        var logger = new FakeLogger<TelemetryHealthCheckPublisher>();
 
        Assert.Throws<ArgumentException>(() => new TelemetryHealthCheckPublisher(metrics, logger, Options.Options.Create<TelemetryHealthCheckPublisherOptions>(null!)));
    }
 
    private static long GetValue(IReadOnlyCollection<CollectedMeasurement<long>> counters, string healthy, string status)
    {
        foreach (var counter in counters)
        {
            if (counter!.Tags["dotnet.health_check.name"]?.ToString() == healthy &&
                counter!.Tags["dotnet.health_check.status"]?.ToString() == status)
            {
                return counter.Value;
            }
        }
 
        return 0;
    }
 
    private static HealthReport CreateHealthReport(IEnumerable<HealthStatus> healthStatuses)
    {
        var healthStatusRecords = new Dictionary<string, HealthReportEntry>();
 
        int index = 0;
        foreach (var status in healthStatuses)
        {
            var entry = new HealthReportEntry(status, $"desc{index}", TimeSpan.Zero, null, null);
            healthStatusRecords.Add(GetKey(index), entry);
            index++;
        }
 
        return new HealthReport(healthStatusRecords, TimeSpan.Zero);
    }
 
    private static string GetKey(int index) => $"id{index}";
 
    private static HealthCheckMetrics GetMockedMetrics(Meter meter)
    {
        var meterFactoryMock = new Mock<IMeterFactory>();
        meterFactoryMock.Setup(x => x.Create(It.IsAny<MeterOptions>()))
            .Returns(meter);
 
        return new HealthCheckMetrics(meterFactoryMock.Object);
    }
}