File: TelemetryApiServiceTests.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.Api;
using Aspire.Dashboard.Otlp.Model;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Logs.V1;
using OpenTelemetry.Proto.Trace.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
 
namespace Aspire.Dashboard.Tests;
 
public class TelemetryApiServiceTests
{
    private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
    [Fact]
    public async Task FollowSpansAsync_StreamsAllSpans()
    {
        // Arrange
        var repository = CreateRepository();
 
        // Add 5 spans
        for (var i = 1; i <= 5; i++)
        {
            repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
            {
                new ResourceSpans
                {
                    Resource = CreateResource(name: "service1", instanceId: "inst1"),
                    ScopeSpans =
                    {
                        new ScopeSpans
                        {
                            Scope = CreateScope(),
                            Spans =
                            {
                                CreateSpan(traceId: $"trace{i}", spanId: $"span{i}", startTime: s_testTime.AddMinutes(i), endTime: s_testTime.AddMinutes(i + 1))
                            }
                        }
                    }
                }
            });
        }
 
        var service = new TelemetryApiService(repository);
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
 
        // Act - stream spans
        var receivedItems = new List<string>();
        await foreach (var item in service.FollowSpansAsync(null, null, null, cts.Token))
        {
            receivedItems.Add(item);
            if (receivedItems.Count >= 5)
            {
                break;
            }
        }
 
        // Assert - should receive all 5 items
        Assert.Equal(5, receivedItems.Count);
    }
 
    [Fact]
    public async Task FollowLogsAsync_StreamsAllLogs()
    {
        // Arrange
        var repository = CreateRepository();
 
        // Add 5 logs
        for (var i = 1; i <= 5; i++)
        {
            repository.AddLogs(new AddContext(), new RepeatedField<ResourceLogs>
            {
                new ResourceLogs
                {
                    Resource = CreateResource(name: "service1", instanceId: "inst1"),
                    ScopeLogs =
                    {
                        new ScopeLogs
                        {
                            Scope = CreateScope("TestLogger"),
                            LogRecords =
                            {
                                CreateLogRecord(time: s_testTime.AddMinutes(i), message: $"log{i}", severity: SeverityNumber.Info)
                            }
                        }
                    }
                }
            });
        }
 
        var service = new TelemetryApiService(repository);
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
 
        // Act - stream logs
        var receivedItems = new List<string>();
        await foreach (var item in service.FollowLogsAsync(null, null, null, cts.Token))
        {
            receivedItems.Add(item);
            if (receivedItems.Count >= 5)
            {
                break;
            }
        }
 
        // Assert - should receive all 5 items
        Assert.Equal(5, receivedItems.Count);
    }
 
    [Fact]
    public void GetSpans_HasErrorFalse_ExcludesErrorSpans()
    {
        // Arrange
        var repository = CreateRepository();
 
        // Add spans - one with error, one without
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: "service1", instanceId: "inst1"),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: "trace1", spanId: "ok-span", startTime: s_testTime, endTime: s_testTime.AddMinutes(1), status: new Status { Code = Status.Types.StatusCode.Ok }),
                            CreateSpan(traceId: "trace2", spanId: "error-span", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), status: new Status { Code = Status.Types.StatusCode.Error })
                        }
                    }
                }
            }
        });
 
        var service = new TelemetryApiService(repository);
 
        // Act - get spans with hasError=false
        var result = service.GetSpans(resource: null, traceId: null, hasError: false, limit: null);
 
        // Assert - should only return the non-error span
        Assert.NotNull(result);
        Assert.Equal(1, result.ReturnedCount);
        
        // Serialize to check content
        var json = System.Text.Json.JsonSerializer.Serialize(result.Data);
        Assert.DoesNotContain("error-span", json);
        Assert.Contains("ok-span", json);
    }
 
    [Fact]
    public void GetSpans_HasErrorTrue_OnlyReturnsErrorSpans()
    {
        // Arrange
        var repository = CreateRepository();
 
        // Add spans - one with error, one without
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: "service1", instanceId: "inst1"),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: "trace1", spanId: "ok-span", startTime: s_testTime, endTime: s_testTime.AddMinutes(1), status: new Status { Code = Status.Types.StatusCode.Ok }),
                            CreateSpan(traceId: "trace2", spanId: "error-span", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), status: new Status { Code = Status.Types.StatusCode.Error })
                        }
                    }
                }
            }
        });
 
        var service = new TelemetryApiService(repository);
 
        // Act - get spans with hasError=true
        var result = service.GetSpans(resource: null, traceId: null, hasError: true, limit: null);
 
        // Assert - should only return the error span
        Assert.NotNull(result);
        Assert.Equal(1, result.ReturnedCount);
        
        var json = System.Text.Json.JsonSerializer.Serialize(result.Data);
        Assert.Contains("error-span", json);
        Assert.DoesNotContain("ok-span", json);
    }
 
    [Fact]
    public void GetTraces_HasErrorFalse_ExcludesTracesWithErrors()
    {
        // Arrange
        var repository = CreateRepository();
 
        // Add two traces - one with error span, one without
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: "service1", instanceId: "inst1"),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: "ok-trace", spanId: "span1", startTime: s_testTime, endTime: s_testTime.AddMinutes(1), status: new Status { Code = Status.Types.StatusCode.Ok })
                        }
                    }
                }
            }
        });
 
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: "service1", instanceId: "inst1"),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: "error-trace", spanId: "span2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), status: new Status { Code = Status.Types.StatusCode.Error })
                        }
                    }
                }
            }
        });
 
        var service = new TelemetryApiService(repository);
 
        // Act - get traces with hasError=false (no error, should exclude the error trace)
        var result = service.GetTraces(resource: null, hasError: false, limit: null);
 
        // Assert - should only return 1 trace (the one without errors)
        Assert.NotNull(result);
        Assert.Equal(1, result.ReturnedCount);
        
        // Verify with null filter returns both
        var allResult = service.GetTraces(resource: null, hasError: null, limit: null);
        Assert.NotNull(allResult);
        Assert.Equal(2, allResult.ReturnedCount);
    }
 
    [Fact]
    public void GetTraces_HasErrorTrue_OnlyReturnsTracesWithErrors()
    {
        // Arrange
        var repository = CreateRepository();
 
        // Add two traces - one with error span, one without
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: "service1", instanceId: "inst1"),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: "ok-trace", spanId: "span1", startTime: s_testTime, endTime: s_testTime.AddMinutes(1), status: new Status { Code = Status.Types.StatusCode.Ok })
                        }
                    }
                }
            }
        });
 
        repository.AddTraces(new AddContext(), new RepeatedField<ResourceSpans>
        {
            new ResourceSpans
            {
                Resource = CreateResource(name: "service1", instanceId: "inst1"),
                ScopeSpans =
                {
                    new ScopeSpans
                    {
                        Scope = CreateScope(),
                        Spans =
                        {
                            CreateSpan(traceId: "error-trace", spanId: "span2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), status: new Status { Code = Status.Types.StatusCode.Error })
                        }
                    }
                }
            }
        });
 
        var service = new TelemetryApiService(repository);
 
        // Act - get traces with hasError=true (error only)
        var result = service.GetTraces(resource: null, hasError: true, limit: null);
 
        // Assert - should only return 1 trace (the one with errors)
        Assert.NotNull(result);
        Assert.Equal(1, result.ReturnedCount);
        
        // Verify with null filter returns both
        var allResult = service.GetTraces(resource: null, hasError: null, limit: null);
        Assert.NotNull(allResult);
        Assert.Equal(2, allResult.ReturnedCount);
    }
}