File: TestServices\DiagnosticCapture.cs
Web Access
Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.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.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Aspire.Hosting.Native.Diagnostics;
 
namespace Aspire.Hosting.Native.Core.Tests.TestServices;
 
internal sealed class DiagnosticCapture : IDisposable
{
    private readonly ActivityListener _activities;
    private readonly MeterListener _meters;
    private readonly OperationEventSubscription _subscription;
    private readonly ConcurrentQueue<OperationEvent> _events = [];
 
    public DiagnosticCapture(bool sampleActivities, params string[] additionalSources)
    {
        Root = new Activity("native.diagnostics.test")
            .SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)
            .SetIdFormat(ActivityIdFormat.W3C).Start();
        _activities = new ActivityListener
        {
            ShouldListenTo = source => source.Name == NativeDiagnostics.SourceName || additionalSources.Contains(source.Name, StringComparer.Ordinal),
            Sample = (ref ActivityCreationOptions<ActivityContext> options) => sampleActivities && options.TraceId == Root.TraceId
                ? ActivitySamplingResult.AllDataAndRecorded : ActivitySamplingResult.None,
            ActivityStopped = activity =>
            {
                if (activity.TraceId == Root.TraceId)
                {
                    Activities.Enqueue(activity);
                }
            }
        };
        ActivitySource.AddActivityListener(_activities);
        _meters = new MeterListener
        {
            InstrumentPublished = (instrument, listener) =>
            {
                if (instrument.Meter.Name == NativeDiagnostics.SourceName)
                {
                    listener.EnableMeasurementEvents(instrument);
                }
            }
        };
        _meters.SetMeasurementEventCallback<long>((instrument, value, tags, _) => Record(instrument, value, tags));
        _meters.SetMeasurementEventCallback<double>((instrument, value, tags, _) => Record(instrument, value, tags));
        _meters.Start();
        // Captures drain at assertion boundaries rather than on a scheduler.
        // A value filter isolates parallel traces without installing a producer
        // callback. Retained capture history still has an explicit upper bound.
        _subscription = NativeDiagnostics.Events.Subscribe(capacity: 65536, traceId: Root.TraceId);
    }
 
    public Activity Root { get; }
    public ConcurrentQueue<Activity> Activities { get; } = [];
    public ConcurrentQueue<OperationEvent> Events
    {
        get
        {
            // Producer writes are complete when a synchronous operation returns.
            // Drain the channel directly for deterministic assertions, not a
            // background consumer that might not yet have been scheduled.
            while (_subscription.Reader.TryRead(out var value))
            {
                if (value.TraceId == Root.TraceId.ToString())
                {
                    _events.Enqueue(value);
                }
            }
            if (_subscription.Reader.Completion.IsCompleted)
            {
                _subscription.Reader.Completion.GetAwaiter().GetResult();
            }
 
            return _events;
        }
    }
    public ConcurrentQueue<Measurement> Measurements { get; } = [];
 
    public void Dispose()
    {
        _subscription.Dispose();
        _meters.Dispose();
        _activities.Dispose();
        Root.Dispose();
    }
 
    private void Record(Instrument instrument, double value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
    {
        if (Activity.Current?.TraceId == Root.TraceId)
        {
            Measurements.Enqueue(new Measurement(instrument.Name, value, tags.ToArray()));
        }
    }
}
 
internal sealed record Measurement(string Name, double Value, KeyValuePair<string, object?>[] Tags);