File: Mcp\AspireTelemetryMcpToolsTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Mcp;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Tests.Model;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Tests.Shared.DashboardModel;
using Aspire.Tests.Shared.Telemetry;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Logging.Abstractions;
using OpenTelemetry.Proto.Logs.V1;
using OpenTelemetry.Proto.Trace.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
 
namespace Aspire.Dashboard.Tests.Mcp;
 
public class AspireTelemetryMcpToolsTests
{
    private static readonly ResourcePropertyViewModel s_excludeFromMcpProperty = new ResourcePropertyViewModel(KnownProperties.Resource.ExcludeFromMcp, Value.ForBool(true), isValueSensitive: false, knownProperty: null, priority: 0);
    private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
    [Fact]
    public void ListTraces_NoResources_ReturnsEmptyResult()
    {
        // Arrange
        var repository = CreateRepository();
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListTraces(resourceName: null);
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# TRACES DATA", result);
    }
 
    [Fact]
    public void ListTraces_SingleResource_ReturnsTraces()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1");
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListTraces(resourceName: "app1");
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# TRACES DATA", result);
        Assert.Contains("app1", result);
    }
 
    [Fact]
    public void ListTraces_ResourceOptOut_FilterTraces()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1", "instance1");
        AddResource(repository, "app2", "instance1");
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "app1-instance1",
            displayName: "app1",
            properties: new Dictionary<string, ResourcePropertyViewModel> { [KnownProperties.Resource.ExcludeFromMcp] = s_excludeFromMcpProperty });
        var dashboardClient = new TestDashboardClient(isEnabled: true, initialResources: [resource]);
 
        var tools = CreateTools(repository, dashboardClient);
 
        // Act
        var result = tools.ListTraces();
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# TRACES DATA", result);
        Assert.DoesNotContain("app1", result);
        Assert.Contains("app2", result);
    }
 
    [Fact]
    public void ListTraces_MultipleResourcesWithSameName_HandlesGracefully()
    {
        // Arrange
        var repository = CreateRepository();
        // Add multiple resources with the same name but different instance IDs
        AddResource(repository, "app1", instanceId: "instance1");
        AddResource(repository, "app1", instanceId: "instance2");
        var tools = CreateTools(repository);
 
        // Act - This should not throw even though there are multiple matches
        var result = tools.ListTraces(resourceName: "app1");
 
        // Assert
        Assert.NotNull(result);
        // When there are multiple resources with the same name, the method should return an error message
        Assert.Contains("doesn't have any telemetry", result);
    }
 
    [Fact]
    public void ListTraces_ResourceNotFound_ReturnsErrorMessage()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1");
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListTraces(resourceName: "nonexistent");
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("doesn't have any telemetry", result);
    }
 
    [Fact]
    public void ListStructuredLogs_NoResources_ReturnsEmptyResult()
    {
        // Arrange
        var repository = CreateRepository();
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListStructuredLogs(resourceName: null);
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# STRUCTURED LOGS DATA", result);
    }
 
    [Fact]
    public void ListStructuredLogs_HasResource_ReturnsLogs()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1");
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListStructuredLogs();
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# STRUCTURED LOGS DATA", result);
        Assert.Contains("app1", result);
    }
 
    [Fact]
    public void ListStructuredLogs_ResourceOptOut_FiltersLogs()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1", "instance1");
        AddResource(repository, "app2", "instance1");
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "app1-instance1",
            displayName: "app1",
            properties: new Dictionary<string, ResourcePropertyViewModel> { [KnownProperties.Resource.ExcludeFromMcp] = s_excludeFromMcpProperty });
        var dashboardClient = new TestDashboardClient(isEnabled: true, initialResources: [resource]);
 
        var tools = CreateTools(repository, dashboardClient);
 
        // Act
        var result = tools.ListStructuredLogs();
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# STRUCTURED LOGS DATA", result);
        Assert.DoesNotContain("app1", result);
        Assert.Contains("app2", result);
    }
 
    [Fact]
    public void ListStructuredLogs_SingleResource_ReturnsLogs()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1");
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListStructuredLogs(resourceName: "app1");
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# STRUCTURED LOGS DATA", result);
        Assert.Contains("app1", result);
    }
 
    [Fact]
    public void ListStructuredLogs_MultipleResourcesWithSameName_HandlesGracefully()
    {
        // Arrange
        var repository = CreateRepository();
        // Add multiple resources with the same name but different instance IDs
        AddResource(repository, "app1", instanceId: "instance1");
        AddResource(repository, "app1", instanceId: "instance2");
        var tools = CreateTools(repository);
 
        // Act - This should not throw even though there are multiple matches
        var result = tools.ListStructuredLogs(resourceName: "app1");
 
        // Assert
        Assert.NotNull(result);
        // When there are multiple resources with the same name, the method should return an error message
        Assert.Contains("doesn't have any telemetry", result);
    }
 
    [Fact]
    public void ListTraceStructuredLogs_WithTraceId_ReturnsLogs()
    {
        // Arrange
        var repository = CreateRepository();
        AddResource(repository, "app1");
        var tools = CreateTools(repository);
 
        // Act
        var result = tools.ListTraceStructuredLogs(traceId: "test-trace-id");
 
        // Assert
        Assert.NotNull(result);
        Assert.Contains("# STRUCTURED LOGS DATA", result);
    }
 
    private static AspireTelemetryMcpTools CreateTools(TelemetryRepository repository, IDashboardClient? dashboardClient = null)
    {
        var options = new DashboardOptions();
        options.Frontend.EndpointUrls = "https://localhost:1234";
        options.Frontend.PublicUrl = "https://localhost:8080";
        Assert.True(options.Frontend.TryParseOptions(out _));
 
        return new AspireTelemetryMcpTools(
            repository,
            [],
            new TestOptionsMonitor<DashboardOptions>(options),
            dashboardClient ?? new TestDashboardClient(),
            NullLogger<AspireTelemetryMcpTools>.Instance);
    }
 
    private static TelemetryRepository CreateRepository()
    {
        return TelemetryTestHelpers.CreateRepository();
    }
 
    private static void AddResource(TelemetryRepository repository, string name, string? instanceId = null)
    {
        var idPrefix = instanceId != null ? $"{name}-{instanceId}" : name;
 
        var addContext = new AddContext();
        repository.AddTraces(addContext, new RepeatedField<ResourceSpans>()
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: name, instanceId: instanceId),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: idPrefix + "1", spanId: idPrefix + "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
                            CreateSpan(traceId: idPrefix + "1", spanId: idPrefix + "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: idPrefix + "1-1"),
                            CreateSpan(traceId: idPrefix + "2", spanId: idPrefix + "2-1", startTime: s_testTime.AddMinutes(6), endTime: s_testTime.AddMinutes(10))
                        }
                    }
                }
            }
        });
 
        Assert.Equal(0, addContext.FailureCount);
 
        repository.AddLogs(addContext, new RepeatedField<ResourceLogs>()
        {
            new ResourceLogs
            {
                Resource = CreateResource(name: name, instanceId: instanceId),
                ScopeLogs =
                {
                    new ScopeLogs
                    {
                        Scope = CreateScope(),
                        LogRecords =
                        {
                            CreateLogRecord(time: s_testTime, message: "Log entry!")
                        }
                    }
                }
            }
        });
 
        Assert.Equal(0, addContext.FailureCount);
    }
}