File: NativeDiagnosticsTests.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.Diagnostics;
using Aspire.Hosting.Native.Core.Tests.TestServices;
using Aspire.Hosting.Native.Diagnostics;
using Aspire.Hosting.Native.Model;
using Aspire.Hosting.Native.Sessions;
 
namespace Aspire.Hosting.Native.Core.Tests;
 
public class NativeDiagnosticsTests
{
    [Fact]
    public void ResourceOperationsCorrelateToTheCallerAndGeneration()
    {
        using var capture = new DiagnosticCapture(sampleActivities: true);
        using var model = new ApplicationModel();
        var resource = model.AddResource("cache", "example/Resource");
        model.Seal();
 
        var activities = capture.Activities.ToArray();
        Assert.Equal(["model.resource.add", "model.seal"], activities.Select(activity => activity.OperationName));
        Assert.All(activities, activity =>
        {
            Assert.Equal(capture.Root.TraceId, activity.TraceId);
            Assert.Equal(capture.Root.SpanId, activity.ParentSpanId);
            Assert.Equal(ActivityStatusCode.Ok, activity.Status);
            Assert.Equal(model.GenerationId, activity.GetTagItem("aspire.generation.id"));
        });
        Assert.Equal(resource.ResourceId, activities[0].GetTagItem("aspire.resource.id"));
        var events = capture.Events.ToArray();
        Assert.Equal([OperationOutcome.Started, OperationOutcome.Ok, OperationOutcome.Started, OperationOutcome.Ok], events.Select(value => value.Outcome));
        Assert.Equal(events[0].OperationId, events[1].OperationId);
        Assert.NotEqual(Guid.Empty, events[0].OperationId);
        Assert.NotEqual(events[0].OperationId, events[2].OperationId);
        Assert.Equal(resource.ResourceId, events[1].ResourceId);
        Assert.All(events, value => Assert.Equal(capture.Root.TraceId.ToString(), value.TraceId));
    }
 
    [Fact]
    public void ErrorsPreserveTheExceptionButDoNotExportItsMessage()
    {
        using var capture = new DiagnosticCapture(sampleActivities: true);
        var exception = new InvalidOperationException("sensitive-value-that-must-not-be-exported");
 
        var observed = Assert.Throws<InvalidOperationException>(() =>
            NativeDiagnostics.Execute("test.failure", null, Guid.NewGuid(), _ => throw exception));
 
        Assert.Same(exception, observed);
        var activity = Assert.Single(capture.Activities);
        Assert.Equal(ActivityStatusCode.Error, activity.Status);
        Assert.Null(activity.StatusDescription);
        Assert.Equal(typeof(InvalidOperationException).FullName, activity.GetTagItem("error.type"));
        Assert.Equal(["aspire.generation.id", "error.type"], activity.TagObjects.Select(tag => tag.Key));
        var failed = Assert.Single(capture.Events, value => value.Outcome == OperationOutcome.Error);
        Assert.Equal(typeof(InvalidOperationException).FullName, failed.ErrorType);
        Assert.Null(failed.ResourceId);
    }
 
    [Fact]
    public void ValidationAndOwnershipFailuresAreVisible()
    {
        using var capture = new DiagnosticCapture(sampleActivities: true);
        using var model = new ApplicationModel();
        var resource = model.AddResource("cache", "example/Resource");
        model.Seal();
 
        Assert.Throws<ArgumentException>(() => model.AddResource("invalid/name", "secret-input"));
        Assert.Throws<InvalidOperationException>(() => model.AddDependency(resource, resource));
 
        var failed = capture.Events.Where(value => value.Outcome == OperationOutcome.Error).ToArray();
        Assert.Equal(["model.resource.add", "model.dependency.add"], failed.Select(value => value.Operation));
        Assert.Equal([typeof(ArgumentException).FullName, typeof(InvalidOperationException).FullName],
            failed.Select(value => value.ErrorType));
        Assert.All(failed, value => Assert.Equal(model.GenerationId, value.GenerationId));
    }
 
    [Fact]
    public void StructuredDiagnosticsDoNotDependOnTraceSampling()
    {
        using var capture = new DiagnosticCapture(sampleActivities: false);
        using var model = new ApplicationModel();
        model.AddResource("cache", "example/Resource");
 
        Assert.Empty(capture.Activities);
        Assert.Equal([OperationOutcome.Started, OperationOutcome.Ok], capture.Events.Select(value => value.Outcome));
        Assert.All(capture.Events, value => Assert.Equal(capture.Root.TraceId.ToString(), value.TraceId));
        Assert.Equal(2, capture.Measurements.Count);
    }
 
    [Fact]
    public void MetricsHaveOnlyBoundedDimensions()
    {
        using var capture = new DiagnosticCapture(sampleActivities: true);
        using var model = new ApplicationModel();
        model.AddResource("cache", "example/Resource");
        Assert.Throws<InvalidOperationException>(() => model.AddResource("cache", "example/Resource"));
 
        var measurements = capture.Measurements.ToArray();
        Assert.Equal(4, measurements.Length);
        Assert.All(measurements, value =>
        {
            Assert.Equal(["operation", "outcome"], value.Tags.Select(tag => tag.Key));
            Assert.Equal("model.resource.add", value.Tags[0].Value);
            Assert.True(value.Value >= 0);
        });
        Assert.Equal(["ok", "error"], measurements.Where(value => value.Name == "aspire.native.operations")
            .Select(value => value.Tags[1].Value));
        Assert.All(measurements.Where(value => value.Name == "aspire.native.operations"),
            value => Assert.Equal(1, value.Value));
    }
 
    [Fact]
    public void SessionRetirementLinksToItsModelRetirement()
    {
        using var capture = new DiagnosticCapture(sampleActivities: true);
        using var session = new ApplicationSession();
        var model = session.StartGeneration();
        session.RetireGeneration(model.GenerationId);
 
        var sessionRetirement = Assert.Single(capture.Activities, activity => activity.OperationName == "session.generation.retire");
        var modelRetirement = Assert.Single(capture.Activities, activity => activity.OperationName == "model.retire");
        Assert.Equal(sessionRetirement.SpanId, modelRetirement.ParentSpanId);
        Assert.Equal(session.SessionId, sessionRetirement.GetTagItem("aspire.session.id"));
        Assert.Equal(model.GenerationId, modelRetirement.GetTagItem("aspire.generation.id"));
        var start = Assert.Single(capture.Events, value => value.Operation == "session.generation.start" && value.Outcome == OperationOutcome.Ok);
        Assert.Equal(session.SessionId, start.SessionId);
        Assert.Equal(model.GenerationId, start.GenerationId);
    }
}