File: Metrics\MetricCollectorTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Diagnostics.Testing.Tests\Microsoft.Extensions.Diagnostics.Testing.Tests.csproj (Microsoft.Extensions.Diagnostics.Testing.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.Diagnostics.Metrics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Time.Testing;
using Xunit;
 
namespace Microsoft.Extensions.Diagnostics.Metrics.Testing.Test;
 
public static class MetricCollectorTests
{
    [Fact]
    public static void Constructor_NullAndEmptyChecks()
    {
        Assert.Throws<ArgumentNullException>(() => new MetricCollector<long>((Instrument<long>)null!));
        Assert.Throws<ArgumentNullException>(() => new MetricCollector<long>((ObservableInstrument<long>)null!));
        Assert.Throws<ArgumentNullException>(() => new MetricCollector<long>(new Meter(Guid.NewGuid().ToString()), null!));
        Assert.Throws<ArgumentNullException>(() => new MetricCollector<long>(null!, "Hello"));
        Assert.Throws<ArgumentNullException>(() => new MetricCollector<long>(null, null!, "Hello"));
 
        Assert.Throws<ArgumentException>(() => new MetricCollector<long>(new Meter(Guid.NewGuid().ToString()), string.Empty));
        Assert.Throws<ArgumentException>(() => new MetricCollector<long>(null, string.Empty, "Hello"));
        Assert.Throws<ArgumentException>(() => new MetricCollector<long>(null, "Hello", string.Empty));
    }
 
    [Fact]
    public static void Constructor_TypeChecks()
    {
        using var meter = new Meter(Guid.NewGuid().ToString());
        var counter = meter.CreateCounter<long>("Counter");
 
        Assert.Throws<InvalidOperationException>(() => new MetricCollector<Guid>(meter, "Counter"));
        Assert.Throws<InvalidOperationException>(() => new MetricCollector<Guid>(null, meter.Name, "Counter"));
    }
 
    [Fact]
    public static void Constructor_Meter()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        using var collector = new MetricCollector<long>(meter, CounterName, timeProvider);
 
        Assert.Null(collector.Instrument);
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
 
        var counter = meter.CreateCounter<long>(CounterName);
        counter.Add(3);
 
        // verify the update was recorded
        Assert.Equal(counter, collector.Instrument);
        Assert.NotNull(collector.LastMeasurement);
 
        // verify measurement info is correct
        Assert.Single(collector.GetMeasurementSnapshot());
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(3, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(now, collector.LastMeasurement.Timestamp);
 
        timeProvider.Advance(TimeSpan.FromSeconds(1));
        counter.Add(2);
 
        // verify measurement info is correct
        Assert.Equal(2, collector.GetMeasurementSnapshot().Count);
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(2, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(timeProvider.GetUtcNow(), collector.LastMeasurement.Timestamp);
 
        collector.Clear();
        Assert.Equal(counter, collector.Instrument);
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
        Assert.Null(collector.LastMeasurement);
    }
 
    [Fact]
    public static void Constructor_Instrument()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        var counter = meter.CreateCounter<long>(CounterName);
        using var collector = new MetricCollector<long>(counter, timeProvider);
 
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
 
        counter.Add(3);
 
        // verify the update was recorded
        Assert.Equal(counter, collector.Instrument);
        Assert.NotNull(collector.LastMeasurement);
 
        // verify measurement info is correct
        Assert.Single(collector.GetMeasurementSnapshot());
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(3, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(now, collector.LastMeasurement.Timestamp);
 
        timeProvider.Advance(TimeSpan.FromSeconds(1));
        counter.Add(2);
 
        // verify measurement info is correct
        Assert.Equal(2, collector.GetMeasurementSnapshot().Count);
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(2, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(timeProvider.GetUtcNow(), collector.LastMeasurement.Timestamp);
 
        collector.Clear();
        Assert.Equal(counter, collector.Instrument);
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
        Assert.Null(collector.LastMeasurement);
    }
 
    [Fact]
    public static void Constructor_Scope()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        var scope = new object();
        using var meter = new Meter(Guid.NewGuid().ToString(), null, null, scope);
        var counter = meter.CreateCounter<long>(CounterName);
        using var collector = new MetricCollector<long>(scope, meter.Name, counter.Name, timeProvider);
        using var collector2 = new MetricCollector<long>(new object(), meter.Name, counter.Name, timeProvider);
 
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
 
        counter.Add(3);
 
        // verify the update was recorded
        Assert.Equal(counter, collector.Instrument);
        Assert.NotNull(collector.LastMeasurement);
 
        // verify measurement info is correct
        Assert.Single(collector.GetMeasurementSnapshot());
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(3, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(now, collector.LastMeasurement.Timestamp);
 
        timeProvider.Advance(TimeSpan.FromSeconds(1));
        counter.Add(2);
 
        // verify measurement info is correct
        Assert.Equal(2, collector.GetMeasurementSnapshot().Count);
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(2, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(timeProvider.GetUtcNow(), collector.LastMeasurement.Timestamp);
 
        collector.Clear();
        Assert.Equal(counter, collector.Instrument);
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
        Assert.Null(collector.LastMeasurement);
 
        Assert.Null(collector2.LastMeasurement);
    }
 
    [Fact]
    public static void Constructor_ObservableInstrument()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        int observationCount = 0;
 
        var counter = meter.CreateObservableCounter<long>(CounterName, () =>
        {
            if (observationCount == 0)
            {
                observationCount++;
                return 3;
            }
            else
            {
                return 2;
            }
        });
 
        using var collector = new MetricCollector<long>(counter, timeProvider);
 
        Assert.Empty(collector.GetMeasurementSnapshot());
        Assert.Null(collector.LastMeasurement);
 
        collector.RecordObservableInstruments();
 
        // verify the update was recorded
        Assert.Equal(counter, collector.Instrument);
        Assert.NotNull(collector.LastMeasurement);
 
        // verify measurement info is correct
        Assert.Single(collector.GetMeasurementSnapshot());
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(3, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(now, collector.LastMeasurement.Timestamp);
 
        timeProvider.Advance(TimeSpan.FromSeconds(1));
        collector.RecordObservableInstruments();
 
        // verify measurement info is correct
        Assert.Equal(2, collector.GetMeasurementSnapshot().Count);
        Assert.Same(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
        Assert.Equal(2, collector.LastMeasurement.Value);
        Assert.Empty(collector.LastMeasurement.Tags);
        Assert.Equal(timeProvider.GetUtcNow(), collector.LastMeasurement.Timestamp);
    }
 
    [Fact]
    public static async Task Wait()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        using var collector = new MetricCollector<long>(meter, CounterName, timeProvider);
        var counter = meter.CreateCounter<long>(CounterName);
 
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await collector.WaitForMeasurementsAsync(-1));
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await collector.WaitForMeasurementsAsync(0));
 
        Assert.Equal(0, collector.WaitersCount);
 
        var wait = collector.WaitForMeasurementsAsync(2);
        Assert.Equal(1, collector.WaitersCount);
        Assert.False(wait.IsCompleted);
 
        counter.Add(1);
        Assert.False(wait.IsCompleted);
        Assert.Equal(1, collector.WaitersCount);
 
        counter.Add(1);
        Assert.True(wait.IsCompleted);
        Assert.False(wait.IsFaulted);
        Assert.Equal(0, collector.WaitersCount);
 
        collector.Clear();
        counter.Add(1);
        wait = collector.WaitForMeasurementsAsync(1);
        Assert.True(wait.IsCompleted);
        Assert.False(wait.IsFaulted);
        Assert.Equal(0, collector.WaitersCount);
    }
 
    [Fact]
    public static async Task WaitWithCancellation()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        using var collector = new MetricCollector<long>(meter, CounterName, timeProvider);
        var counter = meter.CreateCounter<long>(CounterName);
 
        using var cts = new CancellationTokenSource();
        var wait = collector.WaitForMeasurementsAsync(1, cts.Token);
 
        Assert.Equal(1, collector.WaitersCount);
 
        cts.Cancel();
 
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => wait);
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
 
        Assert.Equal(0, collector.WaitersCount);
    }
 
    [Fact]
    public static async Task WaitWithTimeout()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        using var collector = new MetricCollector<long>(meter, CounterName, timeProvider);
        var counter = meter.CreateCounter<long>(CounterName);
 
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await collector.WaitForMeasurementsAsync(-1));
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await collector.WaitForMeasurementsAsync(0));
 
        var wait = collector.WaitForMeasurementsAsync(2, TimeSpan.FromSeconds(1));
        Assert.False(wait.IsCompleted);
        Assert.Equal(1, collector.WaitersCount);
 
        counter.Add(1);
        Assert.False(wait.IsCompleted);
        Assert.Equal(1, collector.WaitersCount);
 
        collector.Clear();
        wait = collector.WaitForMeasurementsAsync(1, TimeSpan.FromSeconds(1));
        Assert.Equal(2, collector.WaitersCount);
        counter.Add(1);
        Assert.Equal(1, collector.WaitersCount);
 
        // Task should be complete. Error if not complete after delay.
        await wait.WaitAsync(TimeSpan.FromSeconds(5), TimeProvider.System);
    }
 
    [Fact]
    public static async Task WaitWithTimeout_CanceledFromTimeout()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        using var meter = new Meter(Guid.NewGuid().ToString());
        using var collector = new MetricCollector<long>(meter, CounterName);
        var counter = meter.CreateCounter<long>(CounterName);
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => collector.WaitForMeasurementsAsync(1, TimeSpan.FromMilliseconds(50)));
    }
 
    [Fact]
    public static async Task Dispose()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        var collector = new MetricCollector<long>(meter, CounterName, timeProvider);
        var counter = meter.CreateCounter<long>(CounterName);
 
        var wait = collector.WaitForMeasurementsAsync(2, TimeSpan.FromSeconds(1));
 
        collector.Dispose();
        collector.Dispose();    // second call is a nop
 
        Assert.Throws<ObjectDisposedException>(() => collector.GetMeasurementSnapshot());
        Assert.Throws<ObjectDisposedException>(() => collector.Clear());
        Assert.Throws<ObjectDisposedException>(() => collector.LastMeasurement);
        Assert.Throws<ObjectDisposedException>(() => collector.RecordObservableInstruments());
 
        await Assert.ThrowsAsync<ObjectDisposedException>(async () => await collector.WaitForMeasurementsAsync(1));
        await Assert.ThrowsAsync<ObjectDisposedException>(async () => await collector.WaitForMeasurementsAsync(1, TimeSpan.FromSeconds(1)));
 
        // HACK: there seems to be something executing asynchronously which takes a while to finish. This needs to be tracked on
        //       I'm adding the sleep here to keep the test from being flaky.
        Thread.Sleep(2000);
 
        Assert.True(wait.IsCompleted);
        Assert.True(wait.IsFaulted);
 
        collector = new MetricCollector<long>(meter, CounterName, timeProvider);
        collector.Dispose();
        counter.Add(1);
        Assert.Throws<ObjectDisposedException>(() => collector.Clear());
    }
 
    [Fact]
    public static void Snapshot()
    {
        const string CounterName = "MyCounter";
 
        var now = DateTimeOffset.Now;
 
        var timeProvider = new FakeTimeProvider(now);
        using var meter = new Meter(Guid.NewGuid().ToString());
        using var collector = new MetricCollector<long>(meter, CounterName, timeProvider);
        var counter = meter.CreateCounter<long>(CounterName);
 
        counter.Add(1);
        counter.Add(2);
        counter.Add(3);
 
        var snap = collector.GetMeasurementSnapshot();
        Assert.Equal(3, snap.Count);
        Assert.Equal(3, collector.GetMeasurementSnapshot().Count);
 
        Assert.Equal(1, snap[0].Value);
        Assert.Empty(snap[0].Tags);
        Assert.Equal(now, snap[0].Timestamp);
 
        Assert.Equal(2, snap[1].Value);
        Assert.Empty(snap[1].Tags);
        Assert.Equal(now, snap[1].Timestamp);
 
        Assert.Equal(3, snap[2].Value);
        Assert.Empty(snap[2].Tags);
        Assert.Equal(now, snap[2].Timestamp);
 
        snap = collector.GetMeasurementSnapshot(true);
        Assert.Equal(3, snap.Count);
        Assert.Empty(collector.GetMeasurementSnapshot());
    }
}