// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Text;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Tests.Shared.DashboardModel;
using Google.Protobuf;
using Google.Protobuf.Collections;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Trace.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
namespace Aspire.Dashboard.Tests.TelemetryRepositoryTests;
public abstract class TraceTests : TelemetryRepositoryTestBase
{
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
[Theory]
[InlineData(OtlpSpanKind.Server, Span.Types.SpanKind.Server)]
[InlineData(OtlpSpanKind.Client, Span.Types.SpanKind.Client)]
[InlineData(OtlpSpanKind.Consumer, Span.Types.SpanKind.Consumer)]
[InlineData(OtlpSpanKind.Producer, Span.Types.SpanKind.Producer)]
[InlineData(OtlpSpanKind.Internal, Span.Types.SpanKind.Internal)]
[InlineData(OtlpSpanKind.Internal, Span.Types.SpanKind.Unspecified)]
[InlineData(OtlpSpanKind.Unspecified, (Span.Types.SpanKind)1000)]
public void ConvertSpanKind(OtlpSpanKind expected, Span.Types.SpanKind value)
{
var result = OtlpHelpers.ConvertSpanKind(value);
Assert.Equal(expected, result);
}
[Fact]
public async Task AddTraces()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
Assert.False(resource.UninstrumentedPeer);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
AssertId("1-1", trace.RootSpan!.SpanId);
Assert.Equal(2, trace.Spans.Count);
});
}
[Fact]
public async Task GetTraceSummaries_ReturnsPageData()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "frontend", instanceId: "frontend-1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime.AddMinutes(1),
endTime: s_testTime.AddMinutes(3),
attributes:
[
KeyValuePair.Create("custom", "match"),
KeyValuePair.Create("gen_ai.system", "test")
],
status: new Status { Code = Status.Types.StatusCode.Error }),
CreateSpan(
traceId: "2",
spanId: "2-1",
startTime: s_testTime.AddMinutes(2),
endTime: s_testTime.AddMinutes(4),
attributes: [KeyValuePair.Create("custom", "other")])
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "backend", instanceId: "backend-1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-1",
startTime: s_testTime.AddMinutes(2),
endTime: s_testTime.AddMinutes(6),
status: new Status { Code = Status.Types.StatusCode.Ok })
}
}
}
}
});
var request = new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
TraceNameFilterText = "frontend",
Filters =
[
new FieldTelemetryFilter
{
Field = "custom",
Condition = FilterCondition.Equals,
Value = "match"
}
]
};
var summaries = await repositoryContext.Repository.GetTraceSummariesAsync(request, cancellationToken: CancellationToken.None);
var traces = await repositoryContext.Repository.GetTracesAsync(request, cancellationToken: CancellationToken.None);
Assert.Equal(traces.PagedResult.TotalItemCount, summaries.PagedResult.TotalItemCount);
Assert.Equal(traces.MaxDuration, summaries.MaxDuration);
var summary = Assert.Single(summaries.PagedResult.Items);
AssertId("1", summary.TraceId);
Assert.Equal("frontend: Test span. Id: 1-1", summary.FullName);
Assert.Equal(s_testTime.AddMinutes(1), summary.StartTime);
Assert.Equal(TimeSpan.FromMinutes(5), summary.Duration);
Assert.Equal(new ResourceKey("frontend", "frontend-1"), summary.RootResource.ResourceKey);
Assert.True(summary.HasError);
Assert.True(summary.HasGenAI);
Assert.Collection(summary.Resources,
resource =>
{
Assert.Equal(new ResourceKey("frontend", "frontend-1"), resource.Resource.ResourceKey);
Assert.Equal(1, resource.TotalSpans);
Assert.Equal(1, resource.ErroredSpans);
},
resource =>
{
Assert.Equal(new ResourceKey("backend", "backend-1"), resource.Resource.ResourceKey);
Assert.Equal(1, resource.TotalSpans);
Assert.Equal(0, resource.ErroredSpans);
});
var latestRequest = new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = [],
LatestItemCount = 1
};
var latestSummaries = await repositoryContext.Repository.GetTraceSummariesAsync(latestRequest, cancellationToken: CancellationToken.None);
var latestTraces = await repositoryContext.Repository.GetTracesAsync(latestRequest, cancellationToken: CancellationToken.None);
Assert.Equal(2, latestSummaries.PagedResult.TotalItemCount);
var latestSummary = Assert.Single(latestSummaries.PagedResult.Items);
AssertId("2", latestSummary.TraceId);
Assert.Equal(s_testTime.AddMinutes(2), latestSummary.StartTime);
Assert.Equal(latestTraces.PagedResult.TotalItemCount, latestSummaries.PagedResult.TotalItemCount);
var latestTrace = Assert.Single(latestTraces.PagedResult.Items);
AssertId("2", latestTrace.TraceId);
Assert.Equal(s_testTime.AddMinutes(2), latestTrace.FirstSpan.StartTime);
var emptyPage = await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = request.ResourceKeys,
StartIndex = 10,
Count = request.Count,
TraceNameFilterText = request.TraceNameFilterText,
Filters = request.Filters
}, cancellationToken: CancellationToken.None);
Assert.Empty(emptyPage.PagedResult.Items);
Assert.Equal(1, emptyPage.PagedResult.TotalItemCount);
Assert.Equal(TimeSpan.FromMinutes(5), emptyPage.MaxDuration);
}
[Fact]
public async Task GetTraceSummaries_SpanTypeFiltersMatchGetTraces()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope("TestScope"),
Spans =
{
CreateSpan(traceId: "http", spanId: "1", startTime: s_testTime, endTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("http.request.method", "GET")]),
CreateSpan(traceId: "database", spanId: "2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(2), attributes: [KeyValuePair.Create("db.system", "sqlite")]),
CreateSpan(traceId: "messaging", spanId: "3", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), attributes: [KeyValuePair.Create("messaging.system", "kafka")]),
CreateSpan(traceId: "rpc", spanId: "4", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(4), attributes: [KeyValuePair.Create("rpc.system", "grpc")]),
CreateSpan(traceId: "genai", spanId: "5", startTime: s_testTime.AddMinutes(4), endTime: s_testTime.AddMinutes(5), attributes: [KeyValuePair.Create("gen_ai.provider.name", "test")])
}
},
new ScopeSpans
{
Scope = CreateScope("azure.messaging"),
Spans =
{
CreateSpan(traceId: "cloud-azure", spanId: "6", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(6))
}
},
new ScopeSpans
{
Scope = CreateScope("AWSSDK"),
Spans =
{
CreateSpan(traceId: "cloud-aws", spanId: "7", startTime: s_testTime.AddMinutes(6), endTime: s_testTime.AddMinutes(7))
}
},
new ScopeSpans
{
Scope = CreateScope("Azureish"),
Spans =
{
CreateSpan(traceId: "other", spanId: "8", startTime: s_testTime.AddMinutes(7), endTime: s_testTime.AddMinutes(8), attributes: [KeyValuePair.Create("http.request.method", string.Empty)])
}
}
}
}
]);
var cases = new (SpanType SpanType, string[] ExpectedTraceIds)[]
{
(SpanType.Http, ["http"]),
(SpanType.Database, ["database"]),
(SpanType.Messaging, ["messaging"]),
(SpanType.Rpc, ["rpc"]),
(SpanType.GenAI, ["genai"]),
(SpanType.Cloud, ["cloud-azure", "cloud-aws"]),
(SpanType.Other, ["other"])
};
foreach (var (spanType, expectedTraceIds) in cases)
{
var request = new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = [spanType.Filter]
};
var traces = (await repositoryContext.Repository.GetTracesAsync(request, cancellationToken: CancellationToken.None)).PagedResult.Items;
var summaries = (await repositoryContext.Repository.GetTraceSummariesAsync(request, cancellationToken: CancellationToken.None)).PagedResult.Items;
Assert.Equal(expectedTraceIds.Select(GetHexId), traces.Select(trace => trace.TraceId));
Assert.Equal(expectedTraceIds.Select(GetHexId), summaries.Select(summary => summary.TraceId));
}
}
[Fact]
public async Task GetTraceSummaries_LateParent_PreservesResourceOrder()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "z-child"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-1",
startTime: s_testTime.AddMinutes(1),
endTime: s_testTime.AddMinutes(10))
}
}
}
}
]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "a-parent"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime.AddMinutes(5),
endTime: s_testTime.AddMinutes(10))
}
}
}
}
]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
var trace = Assert.IsType<OtlpTrace>(repositoryContext.Repository.GetTrace(summary.TraceId));
Assert.Equal(
TraceHelpers.GetOrderedResources(trace).Select(resource => resource.Resource.ResourceKey),
summary.Resources.Select(resource => resource.Resource.ResourceKey));
Assert.Collection(summary.Resources,
resource => Assert.Equal("a-parent", resource.Resource.ResourceName),
resource => Assert.Equal("z-child", resource.Resource.ResourceName));
}
[Fact]
public async Task GetTraceSummaries_SameOrderTime_UninstrumentedPeerAfterInstrumentedResource()
{
var outgoingPeerResolver = new TestOutgoingPeerResolver(onResolve: _ => ("dashboard.db", null));
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "unknown_service:Aspire.Dashboard"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "dashboard.db")],
kind: Span.Types.SpanKind.Client)
}
}
}
}
]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(summary.Resources,
resource =>
{
Assert.Equal("unknown_service:Aspire.Dashboard", resource.Resource.ResourceName);
Assert.False(resource.Resource.UninstrumentedPeer);
},
resource =>
{
Assert.Equal("dashboard.db", resource.Resource.ResourceName);
Assert.True(resource.Resource.UninstrumentedPeer);
});
}
[Fact]
public async Task GetTraceSummaries_IncrementalAppend_UpdatesSummaryValues()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "frontend"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1))
}
}
}
}
]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "backend"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-1",
startTime: s_testTime.AddSeconds(1),
endTime: s_testTime.AddMinutes(2),
attributes: [KeyValuePair.Create("gen_ai.provider.name", "test")],
status: new Status { Code = Status.Types.StatusCode.Error })
}
}
}
}
]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.True(summary.HasError);
Assert.True(summary.HasGenAI);
Assert.Collection(summary.Resources,
resource =>
{
Assert.Equal("frontend", resource.Resource.ResourceName);
Assert.Equal(1, resource.TotalSpans);
Assert.Equal(0, resource.ErroredSpans);
},
resource =>
{
Assert.Equal("backend", resource.Resource.ResourceName);
Assert.Equal(1, resource.TotalSpans);
Assert.Equal(1, resource.ErroredSpans);
});
}
[Fact]
public async Task AddTraces_SelfParent_Reject()
{
// Arrange
var testSink = new TestSink();
var factory = LoggerFactory.Create(b => b.AddProvider(new TestLoggerProvider(testSink)));
using var repositoryContext = await CreateRepositoryAsync(loggerFactory: factory);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
// Assert
Assert.Equal(1, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Empty(traces.PagedResult.Items);
var write = Assert.Single(testSink.Writes);
Assert.Equal("Error adding span.", write.Message);
Assert.Equal("Circular loop detected for span '312d31' with parent '312d31'.", write.Exception!.Message);
}
[Fact]
public async Task AddTraces_MultipleSpansLoop_Reject()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-3"),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-2")
}
}
}
}
});
// Assert
Assert.Equal(1, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
Assert.Equal(2, trace.Spans.Count);
});
}
[Fact]
public async Task AddTraces_CircularReferenceAcrossIngestionCalls_Reject()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(4), parentSpanId: "1-2")
}
}
}
}
});
var context = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(context, new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(5), parentSpanId: "1-3")
}
}
}
}
});
Assert.Equal(1, context.FailureCount);
var trace = Assert.Single((await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(trace.Spans,
span => AssertId("1-2", span.SpanId),
span => AssertId("1-3", span.SpanId));
}
[Fact]
public async Task AddTraces_DuplicateTraceIds_Reject()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
}
}
}
}
});
// Assert
Assert.Equal(1, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
Assert.Equal(2, trace.Spans.Count);
});
}
[Fact]
public async Task AddTraces_Scope_Multiple()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope("scope1"),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
}
}
}
}
});
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope("scope2"),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
AssertId("1-1", trace.RootSpan!.SpanId);
Assert.Equal(2, trace.Spans.Count);
Assert.Collection(trace.Spans,
span => Assert.Equal("scope1", span.Scope.Name),
span => Assert.Equal("scope2", span.Scope.Name));
});
}
[Fact]
public async Task AddTraces_Traces_MultipleOutOrOrder()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext1 = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext1, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
Assert.Equal(0, addContext1.FailureCount);
var addContext2 = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext2, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
Assert.Equal(0, addContext2.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces1 = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces1.PagedResult.Items,
trace =>
{
AssertId("2", trace.TraceId);
AssertId("2-1", trace.FirstSpan.SpanId);
AssertId("2-1", trace.RootSpan!.SpanId);
},
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-2", trace.FirstSpan.SpanId);
Assert.Null(trace.RootSpan);
});
var addContext3 = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext3, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
Assert.Equal(0, addContext3.FailureCount);
var traces2 = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces2.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
Assert.Same(OtlpScope.Empty, trace.FirstSpan.Scope);
AssertId("1-1", trace.RootSpan!.SpanId);
},
trace =>
{
AssertId("2", trace.TraceId);
AssertId("2-1", trace.FirstSpan.SpanId);
Assert.Same(OtlpScope.Empty, trace.FirstSpan.Scope);
AssertId("2-1", trace.RootSpan!.SpanId);
});
}
[Fact]
public async Task AddTraces_Spans_MultipleOutOrOrder()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-5", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-4", startTime: s_testTime.AddMinutes(4), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
AssertId("1-1", trace.RootSpan!.SpanId);
Assert.Collection(trace.Spans,
s => AssertId("1-1", s.SpanId),
s => AssertId("1-2", s.SpanId),
s => AssertId("1-3", s.SpanId),
s => AssertId("1-4", s.SpanId),
s => AssertId("1-5", s.SpanId));
});
}
[Fact]
public async Task AddTraces_SpanEvents_ReturnData()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), events: new List<Span.Types.Event>
{
new Span.Types.Event
{
Name = "Event 2",
TimeUnixNano = 2,
Attributes =
{
new KeyValue { Key = "key2", Value = new AnyValue { StringValue = "Value!" } }
}
},
new Span.Types.Event
{
Name = "Event 1",
TimeUnixNano = 1,
Attributes =
{
new KeyValue { Key = "key1", Value = new AnyValue { StringValue = "Value!" } }
}
}
})
}
}
}
}
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
Assert.Collection(trace.FirstSpan.Events,
e =>
{
Assert.Equal("Event 1", e.Name);
Assert.Collection(e.Attributes,
a =>
{
Assert.Equal("key1", a.Key);
Assert.Equal("Value!", a.Value);
});
},
e =>
{
Assert.Equal("Event 2", e.Name);
});
});
}
[Fact]
public async Task AddTraces_SpanLinks_ReturnData()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), links: new List<Span.Types.Link>
{
new Span.Types.Link
{
TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("1")),
SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("1-1")),
Attributes =
{
new KeyValue { Key = "key2", Value = new AnyValue { StringValue = "Value!" } }
}
},
new Span.Types.Link
{
TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("2")),
SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("2-1")),
Attributes =
{
new KeyValue { Key = "key1", Value = new AnyValue { StringValue = "Value!" } }
}
}
})
}
}
}
}
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
Assert.Collection(trace.FirstSpan.Links,
l =>
{
AssertId("1", l.TraceId);
AssertId("1-1", l.SpanId);
Assert.Collection(l.Attributes,
a =>
{
Assert.Equal("key2", a.Key);
Assert.Equal("Value!", a.Value);
});
},
l =>
{
AssertId("2", l.TraceId);
AssertId("2-1", l.SpanId);
Assert.Collection(l.Attributes,
a =>
{
Assert.Equal("key1", a.Key);
Assert.Equal("Value!", a.Value);
});
});
});
}
[Fact]
public async Task GetTraces_ReturnCopies()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext1 = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext1, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
var traces1 = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces1.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
AssertId("1-1", trace.RootSpan!.SpanId);
});
var traces2 = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.NotSame(traces1.PagedResult.Items[0], traces2.PagedResult.Items[0]);
Assert.NotSame(traces1.PagedResult.Items[0].Spans[0].Trace, traces2.PagedResult.Items[0].Spans[0].Trace);
var trace1 = repositoryContext.Repository.GetTrace(GetHexId("1"))!;
var trace2 = repositoryContext.Repository.GetTrace(GetHexId("1"))!;
Assert.NotSame(trace1, trace2);
Assert.NotSame(trace1.Spans[0].Trace, trace2.Spans[0].Trace);
}
[Fact]
public async Task AddTraces_AttributeAndEventLimits_LimitsApplied()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync(maxAttributeCount: 5, maxAttributeLength: 16, maxSpanEventCount: 5);
var attributes = new List<KeyValuePair<string, string>>();
for (var i = 0; i < 10; i++)
{
var value = GetValue((i + 1) * 5);
attributes.Add(new KeyValuePair<string, string>($"Key{i}", value));
}
var events = new List<Span.Types.Event>();
for (var i = 0; i < 10; i++)
{
events.Add(CreateSpanEvent($"Event {i}", i, attributes));
}
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: attributes, events: events)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
var trace = Assert.Single(traces.PagedResult.Items);
AssertId("1", trace.TraceId);
AssertId("1-1", trace.FirstSpan.SpanId);
Assert.Collection(trace.FirstSpan.Attributes,
p =>
{
Assert.Equal("Key0", p.Key);
Assert.Equal("01234", p.Value);
},
p =>
{
Assert.Equal("Key1", p.Key);
Assert.Equal("0123456789", p.Value);
},
p =>
{
Assert.Equal("Key2", p.Key);
Assert.Equal("012345678901234", p.Value);
},
p =>
{
Assert.Equal("Key3", p.Key);
Assert.Equal("0123456789012345", p.Value);
},
p =>
{
Assert.Equal("Key4", p.Key);
Assert.Equal("0123456789012345", p.Value);
});
Assert.Equal(5, trace.FirstSpan.Events.Count);
Assert.Equal(5, trace.FirstSpan.Events[0].Attributes.Length);
}
[Fact]
public async Task AddTraces_Links_BacklinksPopulated()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
await AddTrace(repositoryContext.Repository, "1", s_testTime);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
var trace = Assert.Single(traces.PagedResult.Items);
Assert.Collection(trace.Spans,
s =>
{
var link = Assert.Single(s.Links);
AssertId("1-2", link.SpanId);
AssertId("1-1", link.SourceSpanId);
var backLink = Assert.Single(s.BackLinks);
AssertId("1-1", backLink.SpanId);
AssertId("1-2", backLink.SourceSpanId);
},
s =>
{
var link = Assert.Single(s.Links);
AssertId("1-1", link.SpanId);
AssertId("1-2", link.SourceSpanId);
var backLink = Assert.Single(s.BackLinks);
AssertId("1-2", backLink.SpanId);
AssertId("1-1", backLink.SourceSpanId);
});
}
[Fact]
public async Task AddTraces_ExceedLimit_OrderedByTimestampAndTraceId()
{
// Arrange
const int MaxTraceCount = 10;
using var repositoryContext = await CreateRepositoryAsync(maxTraceCount: MaxTraceCount);
var testTime = s_testTime.AddDays(1);
var expectedTraces = new List<(string TraceId, DateTime StartTime)>();
// Act
for (var i = 0; i < MaxTraceCount * 3; i++)
{
var traceNumber = i + 1;
var traceId = traceNumber.ToString(CultureInfo.InvariantCulture);
// Insert traces out of order to stress the circular buffer type.
var startTime = testTime.AddMinutes(i + (i % 2 == 0 ? -5 : 0));
expectedTraces.Add((GetHexId(traceId), startTime));
try
{
await AddTrace(repositoryContext.Repository, traceId, startTime);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error adding trace number {i}.", ex);
}
}
// Assert
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
var actualOrder = traces.PagedResult.Items.Select(t => t.TraceId).ToList();
var expectedOrder = expectedTraces
.OrderBy(trace => trace.StartTime)
.ThenBy(trace => trace.TraceId, StringComparer.Ordinal)
.TakeLast(MaxTraceCount)
.Select(trace => trace.TraceId)
.ToList();
Assert.Equal(expectedOrder, actualOrder);
Assert.Equal(MaxTraceCount * 2, traces.PagedResult.Items.SelectMany(t => t.Spans).SelectMany(s => s.Links).Count());
}
private static async Task AddTrace(ITelemetryRepository repository, string traceId, DateTime startTime)
{
var addContext = new AddContext();
var link1 = new Span.Types.Link
{
TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes(traceId)),
SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes($"{traceId}-2")),
Attributes =
{
new KeyValue { Key = "key2", Value = new AnyValue { StringValue = "Value!" } }
}
};
var link2 = new Span.Types.Link
{
TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes(traceId)),
SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes($"{traceId}-1")),
Attributes =
{
new KeyValue { Key = "key2", Value = new AnyValue { StringValue = "Value!" } }
}
};
await repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: traceId, spanId: $"{traceId}-2", startTime: startTime.AddMinutes(5), endTime: startTime.AddMinutes(1), parentSpanId: $"{traceId}-1", links: new List<Span.Types.Link>
{
link2
}),
CreateSpan(traceId: traceId, spanId: $"{traceId}-1", startTime: startTime.AddMinutes(1), endTime: startTime.AddMinutes(10), links: new List<Span.Types.Link>
{
link1
})
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
}
[Fact]
public async Task AddTraces_MultipleRootSpans_RootSpanIsEarliestWithoutParent()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(4), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
AssertId("1-2", trace.FirstSpan.SpanId); // First by time
AssertId("1-3", trace.RootSpan!.SpanId); // First by time and without a parent
Assert.Equal(3, trace.Spans.Count);
});
}
[Fact]
public async Task GetTraces_MultipleInstances()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create("key-1", "value-1")]) }
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create("key-2", "value-2")]) }
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "resource2"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10)) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resourceKey = new ResourceKey("resource1", InstanceId: null);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
},
trace =>
{
AssertId("2", trace.TraceId);
});
var propertyKeys = await repositoryContext.Repository.GetTracePropertyKeysAsync(resourceKey, TestContext.Current.CancellationToken);
Assert.Collection(propertyKeys,
s => Assert.Equal("key-1", s),
s => Assert.Equal("key-2", s));
}
[Fact]
public async Task AddTraces_MissingAndEmptyInstanceIdsAreDistinct()
{
using var repositoryContext = await CreateRepositoryAsync();
var missingInstanceIdResource = CreateResource(name: "resource", instanceId: "placeholder");
missingInstanceIdResource.Attributes.Remove(missingInstanceIdResource.Attributes.Single(attribute => attribute.Key == OtlpResource.SERVICE_INSTANCE_ID));
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = missingInstanceIdResource,
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(1)) }
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "resource", instanceId: string.Empty),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(1)) }
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Equal(2, resources.Count);
Assert.Contains(resources, resource => resource.ResourceKey == new ResourceKey("resource", InstanceId: null));
Assert.Contains(resources, resource => resource.ResourceKey == new ResourceKey("resource", InstanceId: string.Empty));
}
[Fact]
public async Task GetTraceFieldValues_AllFieldsMatchMaterializedTraces()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3), attributes: [KeyValuePair.Create("custom", "one")], kind: Span.Types.SpanKind.Client),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(5), parentSpanId: "1-1", attributes: [KeyValuePair.Create("custom", "two")], kind: Span.Types.SpanKind.Server)
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var traces = (await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resource.ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items;
foreach (var field in KnownTraceFields.AllFields.Append("custom"))
{
var expected = OtlpSpan.GetFieldValuesFromTraces(traces, field);
var actual = await repositoryContext.Repository.GetTraceFieldValuesAsync(field, TestContext.Current.CancellationToken);
Assert.Equal(expected.Count, actual.Count);
foreach (var (value, count) in expected)
{
Assert.Equal(count, actual[value]);
}
}
Assert.Empty(await repositoryContext.Repository.GetTraceFieldValuesAsync(KnownTraceFields.DurationField, TestContext.Current.CancellationToken));
Assert.Empty(await repositoryContext.Repository.GetTraceFieldValuesAsync(KnownTraceFields.TimestampField, TestContext.Current.CancellationToken));
}
[Fact]
public async Task GetTraces_AttributeFilters()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create("key1", "value1")]) }
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", attributes: [KeyValuePair.Create("key2", "value2")]) }
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var resourceKey = new ResourceKey("resource1", InstanceId: null);
// Act 1
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [
new FieldTelemetryFilter { Field = "key1", Condition = FilterCondition.Equals, Value = "value1" }
]
}, cancellationToken: CancellationToken.None);
// Assert 1
// Match first span.
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
});
// Act 2
traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [
new FieldTelemetryFilter { Field = "key2", Condition = FilterCondition.Equals, Value = "value2" }
]
}, cancellationToken: CancellationToken.None);
// Assert 2
// Match second span.
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
});
// Act 3
traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [
new FieldTelemetryFilter { Field = "key1", Condition = FilterCondition.Equals, Value = "value1" },
new FieldTelemetryFilter { Field = "key2", Condition = FilterCondition.Equals, Value = "value2" }
]
}, cancellationToken: CancellationToken.None);
// Assert 3
// Match neither span.
Assert.Empty(traces.PagedResult.Items);
}
[Theory]
[InlineData(KnownTraceFields.TraceIdField, "31")]
[InlineData(KnownTraceFields.SpanIdField, "312d31")]
[InlineData(KnownTraceFields.StatusField, "Unset")]
[InlineData(KnownTraceFields.KindField, "Client")]
[InlineData(KnownResourceFields.ServiceNameField, "resource1")]
[InlineData(KnownResourceFields.ServiceNameField, "TestPeer")]
[InlineData(KnownSourceFields.NameField, "TestScope")]
[InlineData(KnownTraceFields.DurationField, "540000")]
public async Task GetTraces_KnownFilters(string name, string value)
{
// Arrange
var outgoingPeerResolver = new TestOutgoingPeerResolver();
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create("key1", "value1"), KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")], kind: Span.Types.SpanKind.Client) }
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var resourceKey = new ResourceKey("resource1", InstanceId: null);
// Act 1
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [
new FieldTelemetryFilter { Field = name, Condition = FilterCondition.NotEqual, Value = value }
]
}, cancellationToken: CancellationToken.None);
// Assert 1
// Doesn't match filter.
Assert.Empty(traces.PagedResult.Items);
// Act 2
traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [
new FieldTelemetryFilter { Field = name, Condition = FilterCondition.Equals, Value = value }
]
}, cancellationToken: CancellationToken.None);
// Assert 2
// Matches filter.
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
});
}
[Fact]
public async Task GetTraces_FiltersPagingAndMaxDuration_ComputedFromAllMatchingTraces()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMilliseconds(1), endTime: s_testTime.AddMilliseconds(11), attributes: [KeyValuePair.Create("dynamic.filter", "match")]),
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMilliseconds(2), endTime: s_testTime.AddMilliseconds(22), attributes: [KeyValuePair.Create("dynamic.filter", "match")]),
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMilliseconds(3), endTime: s_testTime.AddMilliseconds(33), attributes: [KeyValuePair.Create("dynamic.filter", "other")]),
CreateSpan(traceId: "4", spanId: "4-1", startTime: s_testTime.AddMilliseconds(4), endTime: s_testTime.AddMilliseconds(44), attributes: [KeyValuePair.Create("dynamic.filter", "match")]),
CreateSpan(traceId: "5", spanId: "5-1", startTime: s_testTime.AddMilliseconds(5), endTime: s_testTime.AddMilliseconds(55), attributes: [KeyValuePair.Create("dynamic.filter", "match")])
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
// This pins the behavior expected from an optimized single-pass implementation:
// dynamic field filters, known duration filters, paging, total count, and max
// duration must all be computed from the same filtered trace set. MaxDuration
// intentionally comes from all matching traces, not just the returned page.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [new ResourceKey("resource1", InstanceId: null)],
StartIndex = 1,
Count = 1,
Filters =
[
new FieldTelemetryFilter { Field = "dynamic.filter", Condition = FilterCondition.Equals, Value = "match" },
new FieldTelemetryFilter { Field = KnownTraceFields.DurationField, Condition = FilterCondition.GreaterThanOrEqual, Value = "20" }
]
}, cancellationToken: CancellationToken.None);
Assert.Equal(3, traces.PagedResult.TotalItemCount);
Assert.Equal(TimeSpan.FromMilliseconds(50), traces.MaxDuration);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("4", trace.TraceId);
Assert.Equal(TimeSpan.FromMilliseconds(40), trace.Duration);
});
}
[Fact]
public async Task GetTraces_DurationFilter_AppliesTraceLevelDuration()
{
// Verifies that the duration filter uses the trace's overall duration (first span
// start to latest span end), not individual span durations. A trace with a 100ms
// root span containing a 5ms child span should match "> 50ms" (trace is 100ms)
// but NOT "< 10ms" (even though the child span is only 5ms).
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// Root span: 100ms duration
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMilliseconds(0), endTime: s_testTime.AddMilliseconds(100)),
// Child span: 5ms duration (well under any "short" threshold)
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMilliseconds(10), endTime: s_testTime.AddMilliseconds(15), parentSpanId: "1-1")
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var resourceKey = new ResourceKey("resource1", InstanceId: null);
// Duration filter "> 50ms" should match because trace duration is 100ms.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [new FieldTelemetryFilter { Field = KnownTraceFields.DurationField, Condition = FilterCondition.GreaterThan, Value = "50" }]
}, cancellationToken: CancellationToken.None);
Assert.Single(traces.PagedResult.Items);
// Duration filter "< 10ms" should NOT match because trace duration is 100ms,
// even though the child span is only 5ms.
traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = [new FieldTelemetryFilter { Field = KnownTraceFields.DurationField, Condition = FilterCondition.LessThan, Value = "10" }]
}, cancellationToken: CancellationToken.None);
Assert.Empty(traces.PagedResult.Items);
}
[Fact]
public async Task GetTraces_NotEqualFilter_NonMatchingValue_ReturnsTrace()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans = { CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create("key1", "value1")]) }
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
// Act - filter for key1 != "other_value" should return the trace since key1 is "value1"
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [new ResourceKey("resource1", InstanceId: null)],
StartIndex = 0,
Count = 10,
Filters = [
new FieldTelemetryFilter { Field = "key1", Condition = FilterCondition.NotEqual, Value = "other_value" }
]
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("1", trace.TraceId);
});
}
[Fact]
public async Task AddTraces_OutOfOrder_FullName()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var request = new GetTracesRequest
{
ResourceKeys = [new ResourceKey("TestService", "TestId")],
StartIndex = 0,
Count = 10,
Filters = []
};
// Act 1
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(10), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
// Assert 1
var trace = Assert.Single((await repositoryContext.Repository.GetTracesAsync(request, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Equal("TestService: Test span. Id: 1-3", trace.FullName);
// Act 2
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
// Assert 2
trace = Assert.Single((await repositoryContext.Repository.GetTracesAsync(request, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Equal("TestService: Test span. Id: 1-2", trace.FullName);
// Act 3
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(10), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
// Assert 3
trace = Assert.Single((await repositoryContext.Repository.GetTracesAsync(request, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Equal("TestService: Test span. Id: 1-1", trace.FullName);
// Act 4
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-4", startTime: s_testTime, endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
// Assert 4
trace = Assert.Single((await repositoryContext.Repository.GetTracesAsync(request, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Equal("TestService: Test span. Id: 1-1", trace.FullName);
}
[Fact]
public async Task AddTraces_SameResourceDifferentProperties_MultipleResourceViews()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(attributes: [KeyValuePair.Create("prop1", "value1")]),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10))
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(attributes: [KeyValuePair.Create("prop2", "value1"), KeyValuePair.Create("prop1", "value2")]),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(attributes: [KeyValuePair.Create("prop1", "value2"), KeyValuePair.Create("prop2", "value1")]),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(10), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
// Spans belong to the same resource
var resource = Assert.Single(repositoryContext.Repository.GetResources());
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
// Spans have different views
var views = resource.GetViews().OrderBy(v => v.Properties.Length).ToList();
Assert.Equal(3, views.Count);
Assert.Empty(views[0].Properties);
Assert.Collection(views.Where(v => v.Properties.Length > 0),
v =>
{
Assert.Collection(v.Properties,
p =>
{
Assert.Equal("prop1", p.Key);
Assert.Equal("value1", p.Value);
});
},
v =>
{
Assert.Collection(v.Properties,
p =>
{
Assert.Equal("prop1", p.Key);
Assert.Equal("value2", p.Value);
},
p =>
{
Assert.Equal("prop2", p.Key);
Assert.Equal("value1", p.Value);
});
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resource.ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
var trace = Assert.Single(traces.PagedResult.Items);
Assert.Collection(trace.Spans,
s =>
{
AssertId("1-1", s.SpanId);
Assert.Collection(s.Source.Properties,
p =>
{
Assert.Equal("prop1", p.Key);
Assert.Equal("value1", p.Value);
});
},
s =>
{
AssertId("1-2", s.SpanId);
Assert.Collection(s.Source.Properties,
p =>
{
Assert.Equal("prop1", p.Key);
Assert.Equal("value2", p.Value);
},
p =>
{
Assert.Equal("prop2", p.Key);
Assert.Equal("value1", p.Value);
});
},
s =>
{
AssertId("1-3", s.SpanId);
Assert.Collection(s.Source.Properties,
p =>
{
Assert.Equal("prop1", p.Key);
Assert.Equal("value2", p.Value);
},
p =>
{
Assert.Equal("prop2", p.Key);
Assert.Equal("value1", p.Value);
});
});
}
[Fact]
public async Task RemoveTraces_All()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource("resource1", "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource1", "456"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "2", spanId: "2-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "2-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource2", "789"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "3", spanId: "3-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "3-1")
}
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearTracesAsync();
// Assert
Assert.Equal(0, addContext.FailureCount);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.NotNull(traces?.PagedResult?.Items);
Assert.Empty(traces.PagedResult.Items);
Assert.Equal(0, traces.PagedResult.TotalItemCount);
}
[Fact]
public async Task RemoveTraces_SelectedResource()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource("resource1", "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource1", "456"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "2", spanId: "2-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "2-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource2", "789"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "3", spanId: "3-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "3-1")
}
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearTracesAsync(new ResourceKey("resource1", "123"));
// Assert
Assert.Equal(0, addContext.FailureCount);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.NotNull(traces?.PagedResult?.Items);
Assert.Equal(2, traces.PagedResult.TotalItemCount);
Assert.Collection(traces.PagedResult.Items,
trace =>
{
AssertId("2", trace.TraceId);
Assert.Collection(trace.Spans,
s =>
{
AssertId("2-1", s.SpanId);
},
s =>
{
AssertId("2-2", s.SpanId);
});
},
trace =>
{
AssertId("3", trace.TraceId);
Assert.Collection(trace.Spans,
s =>
{
AssertId("3-1", s.SpanId);
},
s =>
{
AssertId("3-2", s.SpanId);
});
});
}
[Fact]
public async Task RemoveTraces_MultipleSelectedResources()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource("resource1", "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource1", "456"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "2", spanId: "2-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "2-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource2", "789"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "3", spanId: "3-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "3-1"),
}
},
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearTracesAsync(new ResourceKey("resource1", null));
// Assert
Assert.Equal(0, addContext.FailureCount);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.NotNull(traces?.PagedResult?.Items);
var trace = Assert.Single(traces.PagedResult.Items);
AssertId("3", trace.TraceId);
Assert.Collection(trace.Spans,
s =>
{
AssertId("3-1", s.SpanId);
},
s =>
{
AssertId("3-2", s.SpanId);
});
}
[Fact]
public async Task RemoveTraces_SelectedResource_SpansFromDifferentTrace()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource("resource1", "123"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource1", "456"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "2-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource("resource2", "789"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "3", spanId: "3-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "3-1"),
// Spans on traces originating from other resources
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(6), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-2"),
CreateSpan(traceId: "2", spanId: "2-3", startTime: s_testTime.AddMinutes(6), endTime: s_testTime.AddMinutes(10), parentSpanId: "2-2")
}
},
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearTracesAsync(new ResourceKey("resource1", null));
// Assert
Assert.Equal(0, addContext.FailureCount);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.NotNull(traces?.PagedResult?.Items);
var trace = Assert.Single(traces.PagedResult.Items);
AssertId("3", trace.TraceId);
Assert.Collection(trace.Spans,
s =>
{
AssertId("3-1", s.SpanId);
},
s =>
{
AssertId("3-2", s.SpanId);
});
}
[Fact]
public async Task AddTraces_HaveUninstrumentedPeers()
{
// Arrange
var outgoingPeerResolver = new TestOutgoingPeerResolver();
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")], kind: Span.Types.SpanKind.Client),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-2")], kind: Span.Types.SpanKind.Client)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true);
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestPeer", resource.ResourceName);
Assert.Null(resource.InstanceId);
Assert.True(resource.UninstrumentedPeer);
},
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
Assert.False(resource.UninstrumentedPeer);
});
var uninstrumentedPeerApp = resources.Single(a => a.UninstrumentedPeer);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [uninstrumentedPeerApp.ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
var trace = Assert.Single(traces.PagedResult.Items);
Assert.Collection(trace.Spans,
s =>
{
AssertId("1-1", s.SpanId);
Assert.Null(s.UninstrumentedPeer);
},
s =>
{
AssertId("1-2", s.SpanId);
Assert.NotNull(s.UninstrumentedPeer);
Assert.Equal("TestPeer", s.UninstrumentedPeer.ResourceName);
});
var serviceNames = await repositoryContext.Repository.GetTraceFieldValuesAsync(KnownResourceFields.ServiceNameField, TestContext.Current.CancellationToken);
Assert.Equal(2, serviceNames["TestService"]);
Assert.Equal(1, serviceNames["TestPeer"]);
}
[Fact]
public async Task AddTraces_ChildAddedLater_UpdatesUninstrumentedPeers()
{
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [new TestOutgoingPeerResolver()]);
var writer = repositoryContext.Repository.AsWriter();
await writer.AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime.AddMinutes(1),
endTime: s_testTime.AddMinutes(10),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")],
kind: Span.Types.SpanKind.Client,
status: new Status { Code = Status.Types.StatusCode.Error })
}
}
}
}
});
var trace = Assert.IsType<OtlpTrace>(repositoryContext.Repository.GetTrace(GetHexId("1")));
Assert.NotNull(Assert.Single(trace.Spans).UninstrumentedPeer);
await writer.AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-1",
startTime: s_testTime.AddMinutes(5),
endTime: s_testTime.AddMinutes(10),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-2")],
kind: Span.Types.SpanKind.Client)
}
}
}
}
});
trace = Assert.IsType<OtlpTrace>(repositoryContext.Repository.GetTrace(GetHexId("1")));
Assert.Collection(
trace.Spans,
span => Assert.Null(span.UninstrumentedPeer),
span => Assert.Equal("TestPeer", span.UninstrumentedPeer?.ResourceName));
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(
summary.Resources,
resource =>
{
Assert.Equal("TestService", resource.Resource.ResourceName);
Assert.Equal(2, resource.TotalSpans);
Assert.Equal(1, resource.ErroredSpans);
},
resource =>
{
Assert.Equal("TestPeer", resource.Resource.ResourceName);
Assert.Equal(1, resource.TotalSpans);
Assert.Equal(0, resource.ErroredSpans);
});
}
[Fact]
public async Task ClearTraces_UninstrumentedPeer_RemovesOrphanedPeerResource()
{
var outgoingPeerResolver = new TestOutgoingPeerResolver();
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")], kind: Span.Types.SpanKind.Client)
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
Assert.Contains(repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true), resource => resource.UninstrumentedPeer);
await repositoryContext.Repository.AsWriter().ClearTracesAsync();
// The peer resource only ever existed because a span named it. Once the spans are gone nothing else
// deletes it, so without cleanup it lingers in the resource list for the life of the database.
Assert.DoesNotContain(repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true), resource => resource.UninstrumentedPeer);
}
[Fact]
public async Task ClearTraces_UninstrumentedPeer_RemovesAssociatedTrace()
{
var outgoingPeerResolver = new TestOutgoingPeerResolver();
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime.AddMinutes(1),
endTime: s_testTime.AddMinutes(10),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")],
kind: Span.Types.SpanKind.Client)
}
}
}
}
});
var peerResource = Assert.Single(
repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true),
resource => resource.UninstrumentedPeer);
await repositoryContext.Repository.AsWriter().ClearTracesAsync(peerResource.ResourceKey);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Empty(traces.PagedResult.Items);
Assert.DoesNotContain(repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true), resource => resource.UninstrumentedPeer);
}
[Fact]
public async Task AddTraces_OnPeerUpdated_HaveUninstrumentedPeers()
{
// Arrange
var matchPeer = false;
var outgoingPeerResolver = new TestOutgoingPeerResolver(onResolve: attributes =>
{
if (matchPeer)
{
var name = "TestPeer";
var matchedResourced = ModelTestHelpers.CreateResource(resourceName: "TestPeer");
return (name, matchedResourced);
}
else
{
return (null, null);
}
});
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")], kind: Span.Types.SpanKind.Client),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-2")], kind: Span.Types.SpanKind.Client)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true);
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
Assert.False(resource.UninstrumentedPeer);
});
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
var trace = Assert.Single(traces.PagedResult.Items);
Assert.Collection(trace.Spans,
s =>
{
AssertId("1-1", s.SpanId);
Assert.Null(s.UninstrumentedPeer);
},
s =>
{
AssertId("1-2", s.SpanId);
Assert.Null(s.UninstrumentedPeer);
});
matchPeer = true;
await outgoingPeerResolver.InvokePeerChanges();
resources = repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true);
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestPeer", resource.ResourceName);
Assert.Null(resource.InstanceId);
Assert.True(resource.UninstrumentedPeer);
},
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
Assert.False(resource.UninstrumentedPeer);
});
var uninstrumentedPeerApp = resources.Single(a => a.UninstrumentedPeer);
traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [uninstrumentedPeerApp.ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
trace = Assert.Single(traces.PagedResult.Items);
Assert.Collection(trace.Spans,
s =>
{
AssertId("1-1", s.SpanId);
Assert.Null(s.UninstrumentedPeer);
},
s =>
{
AssertId("1-2", s.SpanId);
Assert.NotNull(s.UninstrumentedPeer);
Assert.Equal("TestPeer", s.UninstrumentedPeer.ResourceName);
});
}
[Fact]
public async Task AddTraces_NameOnlyPeerResolver_PersistsUninstrumentedPeer()
{
var outgoingPeerResolver = new TestOutgoingPeerResolver(onResolve: _ => ("Browser Link", null));
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "localhost")],
kind: Span.Types.SpanKind.Client)
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var peerResource = Assert.Single(repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true), resource => resource.UninstrumentedPeer);
Assert.Equal("Browser Link", peerResource.ResourceName);
Assert.Null(peerResource.InstanceId);
var trace = Assert.IsType<OtlpTrace>(repositoryContext.Repository.GetTrace(GetHexId("1")));
var span = Assert.Single(trace.Spans);
Assert.Equal(peerResource.ResourceKey, span.UninstrumentedPeer?.ResourceKey);
}
[Fact]
public async Task AddTraces_NameOnlyPeerResolverChange_PersistsUninstrumentedPeer()
{
var matchPeer = false;
var outgoingPeerResolver = new TestOutgoingPeerResolver(onResolve: _ => matchPeer ? ("Browser Link", null) : (null, null));
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "localhost")],
kind: Span.Types.SpanKind.Client)
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
Assert.Null(Assert.Single(repositoryContext.Repository.GetTrace(GetHexId("1"))!.Spans).UninstrumentedPeer);
matchPeer = true;
await outgoingPeerResolver.InvokePeerChanges();
var peerResource = Assert.Single(repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true), resource => resource.UninstrumentedPeer);
Assert.Equal("Browser Link", peerResource.ResourceName);
var span = Assert.Single(repositoryContext.Repository.GetTrace(GetHexId("1"))!.Spans);
Assert.Equal(peerResource.ResourceKey, span.UninstrumentedPeer?.ResourceKey);
matchPeer = false;
await outgoingPeerResolver.InvokePeerChanges();
Assert.Null(Assert.Single(repositoryContext.Repository.GetTrace(GetHexId("1"))!.Spans).UninstrumentedPeer);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(summary.Resources, resource => Assert.Equal("TestService", resource.Resource.ResourceName));
Assert.Empty((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [peerResource.ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
}
[Fact]
public async Task AddTraces_UninstrumentedPeer_InstanceIdDashes_AppKeyResolvedCorrectly()
{
// Arrange
var resource = ModelTestHelpers.CreateResource(resourceName: "test-abc-def", displayName: "test");
var outgoingPeerResolver = new TestOutgoingPeerResolver(onResolve: attributes => (resource.Name, resource));
using var repositoryContext = await CreateRepositoryAsync(
outgoingPeerResolvers: [outgoingPeerResolver]);
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(addContext, new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "source", instanceId: "abc"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-1")], kind: Span.Types.SpanKind.Client),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "value-2")], kind: Span.Types.SpanKind.Client)
}
}
}
}
});
var resources = repositoryContext.Repository.GetResources(includeUninstrumentedPeers: true);
Assert.Collection(resources,
resource =>
{
Assert.Equal("source", resource.ResourceName);
Assert.Equal("abc", resource.InstanceId);
Assert.False(resource.UninstrumentedPeer);
},
resource =>
{
Assert.Equal("test", resource.ResourceName);
Assert.Equal("abc-def", resource.InstanceId);
Assert.True(resource.UninstrumentedPeer);
});
}
[Fact]
public async Task GetSpans_ReturnsAllSpans()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(8))
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(3, result.PagedResult.TotalItemCount);
Assert.Equal(3, result.PagedResult.Items.Count);
}
[Fact]
public async Task GetSpans_FilterByTraceId_ReturnsMatchingSpans()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(8))
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
TraceId = "31" // hex prefix of "1"
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(2, result.PagedResult.TotalItemCount);
Assert.All(result.PagedResult.Items, s => AssertId("1", s.TraceId));
}
[Fact]
public async Task GetSpans_FilterBySpanType_ReturnsMatchingSpans()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddSeconds(1), attributes: [KeyValuePair.Create("db.system.name", "postgresql")]),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddSeconds(1), endTime: s_testTime.AddSeconds(2))
}
}
}
}
});
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [new SpanHasAttributeTelemetryFilter(["db.system.name", "db.system"])]
}, CancellationToken.None);
var span = Assert.Single(result.PagedResult.Items);
AssertId("1-1", span.SpanId);
}
[Fact]
public async Task GetSpans_FilterByHasError_ReturnsErrorSpansOnly()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), status: new Status { Code = Status.Types.StatusCode.Error }),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", status: new Status { Code = Status.Types.StatusCode.Ok })
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
HasError = true
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId("1-1", result.PagedResult.Items[0].SpanId);
}
[Fact]
public async Task GetSpans_FilterByHasErrorFalse_ReturnsNonErrorSpansOnly()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), status: new Status { Code = Status.Types.StatusCode.Error }),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", status: new Status { Code = Status.Types.StatusCode.Ok })
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
HasError = false
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId("1-2", result.PagedResult.Items[0].SpanId);
}
[Theory]
[InlineData(FilterCondition.Equals, "1", "3")]
[InlineData(FilterCondition.NotEqual, "2")]
public async Task GetTraces_StatusFilter_ReturnsMatchingTraces(FilterCondition condition, params string[] expectedTraceIds)
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(2), status: new Status { Code = Status.Types.StatusCode.Error }),
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3), status: new Status { Code = Status.Types.StatusCode.Ok }),
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(4), status: new Status { Code = Status.Types.StatusCode.Error }),
CreateSpan(traceId: "3", spanId: "3-2", startTime: s_testTime.AddMinutes(4), endTime: s_testTime.AddMinutes(5), status: new Status { Code = Status.Types.StatusCode.Ok })
}
}
}
}
});
var result = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.StatusField,
Value = nameof(OtlpSpanStatusCode.Error),
Condition = condition
}
]
}, cancellationToken: CancellationToken.None);
Assert.Equal(expectedTraceIds.Length, result.PagedResult.TotalItemCount);
Assert.Equal(expectedTraceIds.Length, result.PagedResult.Items.Count);
for (var index = 0; index < expectedTraceIds.Length; index++)
{
AssertId(expectedTraceIds[index], result.PagedResult.Items[index].TraceId);
}
}
[Fact]
public async Task GetSpans_FilterByResource_ReturnsMatchingSpans()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(name: "service-a", instanceId: "a1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10))
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "service-b", instanceId: "b1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(8))
}
}
}
}
});
var resources = repositoryContext.Repository.GetResources();
var serviceA = resources.Single(r => r.ResourceName == "service-a");
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [serviceA.ResourceKey],
StartIndex = 0,
Count = int.MaxValue,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId("1-1", result.PagedResult.Items[0].SpanId);
}
[Theory]
[InlineData(FilterCondition.NotEqual, "TestService", 0)]
[InlineData(FilterCondition.NotEqual, "TestPeer", 0)]
[InlineData(FilterCondition.NotEqual, "other", 1)]
[InlineData(FilterCondition.NotContains, "Service", 0)]
[InlineData(FilterCondition.NotContains, "Peer", 0)]
[InlineData(FilterCondition.NotContains, "other", 1)]
public async Task GetSpans_NegativeServiceNameFilter_AppliesToSourceAndPeer(FilterCondition condition, string value, int expectedCount)
{
var outgoingPeerResolver = new TestOutgoingPeerResolver();
using var repositoryContext = await CreateRepositoryAsync(outgoingPeerResolvers: [outgoingPeerResolver]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1),
attributes: [KeyValuePair.Create(OtlpSpan.PeerServiceAttributeKey, "peer")],
kind: Span.Types.SpanKind.Client)
}
}
}
}
]);
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownResourceFields.ServiceNameField,
Condition = condition,
Value = value
}
]
}, cancellationToken: CancellationToken.None);
Assert.Equal(expectedCount, result.PagedResult.TotalItemCount);
Assert.Equal(expectedCount, result.PagedResult.Items.Count);
}
[Fact]
public async Task GetSpans_FilterByDuration_ReturnsMatchingSpans()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// 9 minutes = 540000ms
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
// 1 minute = 60000ms
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(6), parentSpanId: "1-1")
}
}
}
}
});
// Act - filter for spans with duration >= 100000ms (100s)
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.DurationField,
Condition = FilterCondition.GreaterThanOrEqual,
Value = "100000"
}
]
}, cancellationToken: CancellationToken.None);
// Assert - only the long span matches
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId("1-1", result.PagedResult.Items[0].SpanId);
}
[Fact]
public async Task GetSpans_FilterByTextFragments_ReturnsMatchingSpans()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), attributes: [KeyValuePair.Create("http.url", "https://example.com/api")]),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", attributes: [KeyValuePair.Create("db.system", "postgresql")])
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
TextFragments = ["example.com"]
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId("1-1", result.PagedResult.Items[0].SpanId);
}
[Fact]
public async Task GetSpans_FilterByTextFragments_MatchesDisplaySummaries()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1),
attributes:
[
KeyValuePair.Create("http.request.method", "GET"),
KeyValuePair.Create("http.response.status_code", "200")
],
kind: Span.Types.SpanKind.Client),
CreateSpan(
traceId: "1",
spanId: "1-2",
startTime: s_testTime.AddMinutes(1),
endTime: s_testTime.AddMinutes(2),
attributes: [KeyValuePair.Create("db.system", "postgresql")],
kind: Span.Types.SpanKind.Client),
CreateSpan(
traceId: "1",
spanId: "1-3",
startTime: s_testTime.AddMinutes(2),
endTime: s_testTime.AddMinutes(3),
attributes:
[
KeyValuePair.Create("rpc.system", "grpc"),
KeyValuePair.Create("rpc.service", "Greeter"),
KeyValuePair.Create("rpc.method", "SayHello"),
KeyValuePair.Create("rpc.grpc.status_code", "0")
],
kind: Span.Types.SpanKind.Client),
CreateSpan(
traceId: "1",
spanId: "1-4",
startTime: s_testTime.AddMinutes(3),
endTime: s_testTime.AddMinutes(4),
attributes:
[
KeyValuePair.Create("messaging.system", "kafka"),
KeyValuePair.Create("messaging.operation", "publish"),
KeyValuePair.Create("messaging.destination.name", "orders")
],
kind: Span.Types.SpanKind.Producer)
}
}
}
}
]);
var expectedMatches = new[]
{
(Text: "HTTP GET 200", SpanId: "1-1"),
(Text: "DATA postgresql Test span. Id: 1-2", SpanId: "1-2"),
(Text: "RPC Greeter/SayHello OK", SpanId: "1-3"),
(Text: "MSG kafka publish orders", SpanId: "1-4")
};
foreach (var expectedMatch in expectedMatches)
{
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
TextFragments = [expectedMatch.Text]
}, cancellationToken: CancellationToken.None);
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId(expectedMatch.SpanId, Assert.Single(result.PagedResult.Items).SpanId);
}
}
[Theory]
[InlineData(KnownTraceFields.KindField, "Client")]
[InlineData(KnownTraceFields.StatusField, "Error")]
public async Task GetSpans_FilterByKindOrStatusText_ReturnsMatchingSpans(string field, string value)
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: s_testTime.AddMinutes(1),
endTime: s_testTime.AddMinutes(2),
kind: Span.Types.SpanKind.Client,
status: new Status { Code = Status.Types.StatusCode.Error }),
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-1",
startTime: s_testTime.AddMinutes(2),
endTime: s_testTime.AddMinutes(3),
kind: Span.Types.SpanKind.Server,
status: new Status { Code = Status.Types.StatusCode.Ok })
}
}
}
}
]);
var fieldResult = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = field,
Condition = FilterCondition.Equals,
Value = value
}
]
}, cancellationToken: CancellationToken.None);
var textResult = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
TextFragments = [value]
}, cancellationToken: CancellationToken.None);
Assert.Equal(1, fieldResult.PagedResult.TotalItemCount);
AssertId("1-1", Assert.Single(fieldResult.PagedResult.Items).SpanId);
Assert.Equal(1, textResult.PagedResult.TotalItemCount);
AssertId("1-1", Assert.Single(textResult.PagedResult.Items).SpanId);
}
[Fact]
public async Task GetSpans_Pagination_ReturnsCorrectPage()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(3), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1")
}
}
}
}
});
// Act - get second page (skip 1, take 1)
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 1,
Count = 1,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(3, result.PagedResult.TotalItemCount);
Assert.Single(result.PagedResult.Items);
AssertId("1-2", result.PagedResult.Items[0].SpanId);
}
[Fact]
public async Task GetSpans_CombinedFilters_ReturnsMatchingSpans()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10), status: new Status { Code = Status.Types.StatusCode.Error }, attributes: [KeyValuePair.Create("http.url", "https://example.com")]),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(5), endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1", status: new Status { Code = Status.Types.StatusCode.Ok }, attributes: [KeyValuePair.Create("http.url", "https://example.com")]),
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(8), status: new Status { Code = Status.Types.StatusCode.Error }, attributes: [KeyValuePair.Create("db.system", "redis")])
}
}
}
}
});
// Act - filter for error spans with "example.com" text
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
HasError = true,
TextFragments = ["example.com"]
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(1, result.PagedResult.TotalItemCount);
AssertId("1-1", result.PagedResult.Items[0].SpanId);
}
[Fact]
public async Task GetSpans_EmptyRepository_ReturnsEmpty()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(0, result.PagedResult.TotalItemCount);
Assert.Empty(result.PagedResult.Items);
}
[Fact]
public async Task GetSpans_UnknownResource_ReturnsEmpty()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10))
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [ResourceKey.Create("nonexistent", "unknown")],
StartIndex = 0,
Count = int.MaxValue,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(0, result.PagedResult.TotalItemCount);
Assert.Empty(result.PagedResult.Items);
}
[Theory]
[InlineData("747261636531", 1)] // full hex trace ID — prefix match
[InlineData("7472616", 1)] // 7 chars — meets ShortenedIdLength, prefix match
[InlineData("747261", 0)] // 6 chars — below ShortenedIdLength, requires exact match
public async Task GetSpans_TraceIdPrefixLength_MatchesShortenedIds(string traceIdFilter, int expectedCount)
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Use a trace ID whose hex representation is "747261636531" (UTF-8 bytes of "trace1")
var traceId = Encoding.UTF8.GetString(Convert.FromHexString("747261636531"));
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>()
{
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: traceId, spanId: "1-1", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(10)),
CreateSpan(traceId: "other", spanId: "2-1", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(8))
}
}
}
}
});
// Act
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters = [],
TraceId = traceIdFilter
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(expectedCount, result.PagedResult.TotalItemCount);
}
[Fact]
public async Task GetSpans_DisabledFiltersAreIgnored()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "trace1", spanId: "span1", startTime: s_testTime, endTime: s_testTime.AddMinutes(1)),
CreateSpan(traceId: "trace1", spanId: "span2", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(3))
}
}
}
}
});
// Enabled filter matches span name containing "span1", disabled filter would exclude everything
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.NameField,
Value = "span1",
Condition = FilterCondition.Contains,
Enabled = true
},
new FieldTelemetryFilter
{
Field = KnownTraceFields.NameField,
Value = "IMPOSSIBLE",
Condition = FilterCondition.Contains,
Enabled = false
}
]
}, cancellationToken: CancellationToken.None);
// The disabled filter should be ignored — only the enabled "span1" filter applies
Assert.Equal(1, result.PagedResult.TotalItemCount);
Assert.Contains("span1", result.PagedResult.Items[0].Name);
}
[Fact]
public async Task GetTraces_NotContainsFilter_ExcludesTraceWhenAnySpanMatches()
{
// Verifies that a "not contains" filter on the trace Name field excludes the trace
// when ANY span's name contains the filtered text, even if other spans in the same
// trace do not contain it. This is the fix for https://github.com/microsoft/aspire/issues/18684.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// Root span whose name contains the filter text.
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5)),
// Child span whose name does NOT contain "1-1".
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3), parentSpanId: "1-1")
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
// Filter: Name not contains "1-1" — the root span's name is "Test span. Id: 1-1" which
// contains "1-1", so the trace should be excluded even though the child span doesn't match.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.NameField,
Condition = FilterCondition.NotContains,
Value = "1-1"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Empty(traces.PagedResult.Items);
}
[Fact]
public async Task GetTraces_NotContainsFilter_IncludesTraceWhenNoSpanMatches()
{
// Verifies that a "not contains" filter includes the trace when none of its spans'
// names contain the filtered text.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3), parentSpanId: "1-1")
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
// Filter: Name not contains "NONEXISTENT" — no span name contains this text,
// so the trace should be included.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.NameField,
Condition = FilterCondition.NotContains,
Value = "NONEXISTENT"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace => AssertId("1", trace.TraceId));
}
[Fact]
public async Task GetTraces_NotEqualFilter_ExcludesTraceWhenAnySpanMatches()
{
// Verifies that a "not equal" filter excludes the trace when ANY span's field value
// equals the filtered text.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5)),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3), parentSpanId: "1-1")
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
// Filter: Name != "Test span. Id: 1-1" — the root span matches exactly, so the trace
// should be excluded even though the child span doesn't match.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.NameField,
Condition = FilterCondition.NotEqual,
Value = "Test span. Id: 1-1"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Empty(traces.PagedResult.Items);
}
[Fact]
public async Task GetTraces_NotContainsWithPositiveFilter_CombinesCorrectly()
{
// Verifies that combining a positive filter with a negative filter works correctly:
// the trace must have at least one span matching the positive filter AND all spans
// must satisfy the negative filter.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5),
attributes: [KeyValuePair.Create("env", "prod")]),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3),
parentSpanId: "1-1", attributes: [KeyValuePair.Create("env", "prod")])
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(10), endTime: s_testTime.AddMinutes(15),
attributes: [KeyValuePair.Create("env", "staging")])
}
}
}
},
// Trace 3: satisfies both conditions — env=prod and no span name contains "1-1".
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "3", spanId: "3-1", startTime: s_testTime.AddMinutes(20), endTime: s_testTime.AddMinutes(25),
attributes: [KeyValuePair.Create("env", "prod")])
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
// Positive: attribute env contains "prod"
// Negative: name not contains "1-1" (excludes trace 1 because root span name matches)
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter { Field = "env", Condition = FilterCondition.Contains, Value = "prod" },
new FieldTelemetryFilter { Field = KnownTraceFields.NameField, Condition = FilterCondition.NotContains, Value = "1-1" }
]
}, cancellationToken: CancellationToken.None);
// Trace 1 is excluded (root span name contains "1-1" even though env=prod matches).
// Trace 2 doesn't match the positive filter (env=staging, not prod).
// Trace 3 satisfies both: env=prod AND no span name contains "1-1".
Assert.Collection(traces.PagedResult.Items,
trace => AssertId("3", trace.TraceId));
}
[Fact]
public async Task GetTraces_NotContainsFilter_AbsentAttributeDoesNotExcludeTrace()
{
// Verifies that a negative filter on an attribute field does NOT exclude a trace
// just because some spans lack the attribute. A span without the field trivially
// satisfies "not contains X" — it cannot contain the value.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// Span with http.method = POST (satisfies "not contains GET").
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5),
attributes: [KeyValuePair.Create("http.method", "POST")]),
// Span without http.method attribute at all — should NOT cause exclusion.
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3),
parentSpanId: "1-1")
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
// Filter: http.method not contains "GET" — span 1-1 has POST (passes), span 1-2
// has no http.method (trivially passes). The trace should be included.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = "http.method",
Condition = FilterCondition.NotContains,
Value = "GET"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace => AssertId("1", trace.TraceId));
}
[Fact]
public async Task GetTraces_NotContainsFilter_AbsentAttributeWithViolatingSpanExcludes()
{
// Verifies that a trace is excluded when one span has the attribute and violates
// the negative condition, even though another span lacks the attribute entirely.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// Span with http.method = GET (violates "not contains GET").
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5),
attributes: [KeyValuePair.Create("http.method", "GET")]),
// Span without http.method — trivially passes, but trace still excluded.
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3),
parentSpanId: "1-1")
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = "http.method",
Condition = FilterCondition.NotContains,
Value = "GET"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Empty(traces.PagedResult.Items);
}
[Fact]
public async Task GetSpans_NotContainsFilter_AbsentAttributeIncludesSpan()
{
// Verifies that span-level negative filtering correctly includes spans that lack the
// filtered attribute. A span without the field trivially satisfies "not contains X".
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5),
attributes: [KeyValuePair.Create("http.method", "GET")]),
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3),
parentSpanId: "1-1", attributes: [KeyValuePair.Create("http.method", "POST")]),
// Span without http.method attribute — should still be included.
CreateSpan(traceId: "1", spanId: "1-3", startTime: s_testTime.AddMinutes(2), endTime: s_testTime.AddMinutes(4),
parentSpanId: "1-1")
}
}
}
}
});
// Filter: http.method not contains "GET"
// Span 1-1 has GET → excluded
// Span 1-2 has POST → included (POST doesn't contain GET)
// Span 1-3 has no http.method → included (absent field trivially satisfies "not contains")
var result = await repositoryContext.Repository.GetSpansAsync(new GetSpansRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = "http.method",
Condition = FilterCondition.NotContains,
Value = "GET"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Equal(2, result.PagedResult.TotalItemCount);
Assert.Contains(result.PagedResult.Items, s => GetStringId(s.SpanId) == "1-2");
Assert.Contains(result.PagedResult.Items, s => GetStringId(s.SpanId) == "1-3");
}
[Fact]
public async Task GetTraces_NotEqualTimestampFilter_ExcludesTraceViaUnoptimizedPath()
{
// Verifies that the non-optimized MatchesFilters path (used for date/numeric fields)
// correctly applies ALL-span semantics for negative filters. A timestamp NotEqual
// filter excludes a trace when any span's timestamp matches the filter value.
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(), new RepeatedField<ResourceSpans>
{
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// Span 1: starts at s_testTime (violates "timestamp != s_testTime")
CreateSpan(traceId: "1", spanId: "1-1", startTime: s_testTime, endTime: s_testTime.AddMinutes(5)),
// Span 2: starts later (satisfies "timestamp != s_testTime")
CreateSpan(traceId: "1", spanId: "1-2", startTime: s_testTime.AddMinutes(1), endTime: s_testTime.AddMinutes(3),
parentSpanId: "1-1")
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "service1", instanceId: "inst1"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
// Trace 2: starts at a different time (satisfies the filter)
CreateSpan(traceId: "2", spanId: "2-1", startTime: s_testTime.AddMinutes(10), endTime: s_testTime.AddMinutes(15))
}
}
}
}
});
var resourceKey = new ResourceKey("service1", InstanceId: null);
// Timestamp is a date field, so StringFilter.TryCreate returns false and
// CreateOptimizedTraceFilters falls through to the non-optimized path.
// Filter: Timestamp != "1970-01-01T00:00:00Z" — span 1-1 violates this (its timestamp
// equals the filter value), so trace 1 is excluded. Trace 2 passes.
var traces = await repositoryContext.Repository.GetTracesAsync(new GetTracesRequest
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownTraceFields.TimestampField,
Condition = FilterCondition.NotEqual,
Value = "1970-01-01T00:00:00Z"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Collection(traces.PagedResult.Items,
trace => AssertId("2", trace.TraceId));
}
}
public sealed class SqliteTraceTests : TraceTests
{
[Fact]
public async Task GetTraceSummaries_MultipleRootResourcesInPayload_OrdersByReceipt()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
CreateRootResourceSpans("z-first", "1-1", testTime.AddMinutes(2)),
CreateRootResourceSpans("a-second", "1-2", testTime)
]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(
summary.Resources,
resource => Assert.Equal("z-first", resource.Resource.ResourceName),
resource => Assert.Equal("a-second", resource.Resource.ResourceName));
}
[Fact]
public async Task GetTraceSummaries_MultipleRootResourcesAcrossCalls_OrdersByReceipt()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateRootResourceSpans("z-first", "1-1", testTime.AddMinutes(2))]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateRootResourceSpans("a-second", "1-2", testTime)]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(
summary.Resources,
resource => Assert.Equal("z-first", resource.Resource.ResourceName),
resource => Assert.Equal("a-second", resource.Resource.ResourceName));
}
[Fact]
public async Task GetTraceSummaries_PeerRecalculationPreservesRootReceiptOrder()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var outgoingPeerResolver = new TestOutgoingPeerResolver(onResolve: _ => (null, null));
using var repositoryContext = await CreateRepositoryAsync(
timeProvider: new FixedTimeProvider(DateTimeOffset.MaxValue.AddTicks(-100)),
outgoingPeerResolvers: [outgoingPeerResolver]);
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
await repository.AsWriter().AddTracesAsync(new AddContext(),
[
CreateRootResourceSpans("first", "1-1", testTime),
CreateRootResourceSpans("z-second", "1-2", testTime)
]);
await outgoingPeerResolver.InvokePeerChanges();
await repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateRootResourceSpans("a-third", "1-3", testTime)]);
var summary = Assert.Single((await repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(
summary.Resources,
resource => Assert.Equal("first", resource.Resource.ResourceName),
resource => Assert.Equal("z-second", resource.Resource.ResourceName),
resource => Assert.Equal("a-third", resource.Resource.ResourceName));
}
[Fact]
public async Task GetTraceSummaries_NonRootResourceEarlierSpanAddedLater_UpdatesOrder()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
CreateResourceSpans("root", "1-1", testTime, parentSpanId: null),
CreateResourceSpans("z-updated", "1-2", testTime.AddMinutes(5), parentSpanId: "1-1"),
CreateResourceSpans("middle", "1-3", testTime.AddMinutes(4), parentSpanId: "1-1")
]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateResourceSpans("z-updated", "1-4", testTime.AddMinutes(3), parentSpanId: "1-1")]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(
summary.Resources,
resource => Assert.Equal("root", resource.Resource.ResourceName),
resource =>
{
Assert.Equal("z-updated", resource.Resource.ResourceName);
Assert.Equal(2, resource.TotalSpans);
},
resource => Assert.Equal("middle", resource.Resource.ResourceName));
}
[Fact]
public async Task GetTraceSummaries_ResourceSeenAsRootLater_PromotesOrderByReceipt()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
CreateResourceSpans("first-root", "1-1", testTime, parentSpanId: null),
CreateResourceSpans("promoted", "1-2", testTime.AddMinutes(2), parentSpanId: "1-1"),
CreateResourceSpans("non-root", "1-3", testTime.AddMinutes(1), parentSpanId: "1-1")
]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateResourceSpans("promoted", "1-4", testTime.AddMinutes(4), parentSpanId: null)]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateResourceSpans("later-root", "1-5", testTime.AddMinutes(3), parentSpanId: null)]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(
new AddContext(),
[CreateResourceSpans("promoted", "1-6", testTime.AddMinutes(5), parentSpanId: null)]);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(
summary.Resources,
resource => Assert.Equal("first-root", resource.Resource.ResourceName),
resource =>
{
Assert.Equal("promoted", resource.Resource.ResourceName);
Assert.Equal(3, resource.TotalSpans);
},
resource => Assert.Equal("later-root", resource.Resource.ResourceName),
resource => Assert.Equal("non-root", resource.Resource.ResourceName));
}
private static ResourceSpans CreateRootResourceSpans(string resourceName, string spanId, DateTime startTime)
=> CreateResourceSpans(resourceName, spanId, startTime, parentSpanId: null);
private static ResourceSpans CreateResourceSpans(string resourceName, string spanId, DateTime startTime, string? parentSpanId)
{
return new ResourceSpans
{
Resource = CreateResource(name: resourceName),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: spanId,
startTime: startTime,
endTime: startTime.AddMinutes(1),
parentSpanId: parentSpanId)
}
}
}
};
}
[Fact]
public async Task AddTraces_CircularReferenceAcrossPersistedAndIncomingSpans_Reject()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
parentSpanId: "1-2",
startTime: testTime.AddMinutes(1),
endTime: testTime.AddMinutes(2))
}
}
}
}
]);
var context = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(context,
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-3",
startTime: testTime.AddMinutes(2),
endTime: testTime.AddMinutes(3)),
CreateSpan(
traceId: "1",
spanId: "1-3",
parentSpanId: "1-1",
startTime: testTime.AddMinutes(3),
endTime: testTime.AddMinutes(4))
}
}
}
}
]);
Assert.Equal(1, context.SuccessCount);
Assert.Equal(1, context.FailureCount);
var trace = Assert.IsType<OtlpTrace>(repositoryContext.Repository.GetTrace(GetHexId("1")));
Assert.Collection(trace.Spans,
span => AssertId("1-1", span.SpanId),
span => AssertId("1-2", span.SpanId));
}
[Fact]
public async Task GetTraceSummaries_EqualStartTime_MatchesMaterializedTrace()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "first"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-1", startTime: testTime, endTime: testTime.AddMinutes(1))
}
}
}
}
]);
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "second"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "1-2", startTime: testTime, endTime: testTime.AddMinutes(1))
}
}
}
}
]);
var trace = Assert.IsType<OtlpTrace>(repositoryContext.Repository.GetTrace(GetHexId("1")));
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Equal(trace.FullName, summary.FullName);
Assert.Equal(trace.RootOrFirstSpan.Source.Resource.ResourceKey, summary.RootResource.ResourceKey);
}
[Fact]
public async Task AddTraces_LargeAppend_DoesNotExceedSqliteParameterLimit()
{
const int appendedSpanCount = 8_192;
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(traceId: "1", spanId: "root", startTime: testTime, endTime: testTime.AddMinutes(1))
}
}
}
}
]);
var resourceSpans = new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope()
}
}
};
for (var index = 0; index < appendedSpanCount; index++)
{
resourceSpans.ScopeSpans[0].Spans.Add(CreateSpan(
traceId: "1",
spanId: $"child-{index}",
parentSpanId: $"missing-parent-{index}",
startTime: testTime.AddSeconds(1),
endTime: testTime.AddMinutes(1)));
}
var context = new AddContext();
await repositoryContext.Repository.AsWriter().AddTracesAsync(context, [resourceSpans]);
Assert.Equal(appendedSpanCount, context.SuccessCount);
Assert.Equal(0, context.FailureCount);
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
Assert.Collection(summary.Resources, resource => Assert.Equal(appendedSpanCount + 1, resource.TotalSpans));
}
[Fact]
public async Task GetTraceSummaries_AfterResourceDeletion_DeletesAffectedTraces()
{
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddTracesAsync(new AddContext(),
[
new ResourceSpans
{
Resource = CreateResource(name: "frontend"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-1",
startTime: testTime,
endTime: testTime.AddMinutes(5),
attributes: [KeyValuePair.Create("gen_ai.system", "test")],
status: new Status { Code = Status.Types.StatusCode.Error })
}
}
}
},
new ResourceSpans
{
Resource = CreateResource(name: "backend"),
ScopeSpans =
{
new ScopeSpans
{
Scope = CreateScope(),
Spans =
{
CreateSpan(
traceId: "1",
spanId: "1-2",
parentSpanId: "1-1",
startTime: testTime.AddMinutes(1),
endTime: testTime.AddMinutes(2)),
CreateSpan(
traceId: "2",
spanId: "2-1",
startTime: testTime.AddMinutes(3),
endTime: testTime.AddMinutes(4))
}
}
}
}
]);
await repositoryContext.Repository.AsWriter().ClearSelectedSignalsAsync(new Dictionary<string, HashSet<AspireDataType>>
{
[new ResourceKey("frontend", "TestId").GetCompositeName()] = [AspireDataType.Resource]
});
Assert.Null(repositoryContext.Repository.GetTrace(GetHexId("1")));
Assert.NotNull(repositoryContext.Repository.GetTrace(GetHexId("2")));
var summary = Assert.Single((await repositoryContext.Repository.GetTraceSummariesAsync(new GetTracesRequest
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).PagedResult.Items);
AssertId("2", summary.TraceId);
Assert.Equal("backend: Test span. Id: 2-1", summary.FullName);
Assert.Equal(testTime.AddMinutes(3), summary.StartTime);
Assert.Equal(TimeSpan.FromMinutes(1), summary.Duration);
Assert.False(summary.HasError);
Assert.False(summary.HasGenAI);
Assert.Collection(summary.Resources, resource =>
{
Assert.Equal("backend", resource.Resource.ResourceName);
Assert.Equal(1, resource.TotalSpans);
Assert.Equal(0, resource.ErroredSpans);
});
}
[Fact]
public async Task AddTraces_LargeSpanAndAttributeBatchesRoundTrip()
{
const int spanCount = 101;
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
var resourceSpans = new ResourceSpans
{
Resource = CreateResource(),
ScopeSpans =
{
new ScopeSpans { Scope = CreateScope() }
}
};
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
for (var index = 0; index < spanCount; index++)
{
resourceSpans.ScopeSpans[0].Spans.Add(CreateSpan(
traceId: "trace",
spanId: $"span-{index}",
startTime: testTime.AddTicks(index),
endTime: testTime.AddTicks(index + 1),
attributes: [KeyValuePair.Create("index", index.ToString(CultureInfo.InvariantCulture))]));
}
var context = new AddContext();
await repository.AsWriter().AddTracesAsync(context, [resourceSpans]);
Assert.Equal(spanCount, context.SuccessCount);
Assert.Equal(0, context.FailureCount);
var trace = Assert.IsType<OtlpTrace>(repository.GetTrace(GetHexId("trace")));
Assert.Equal(spanCount, trace.Spans.Count);
for (var index = 0; index < spanCount; index++)
{
var span = trace.Spans[index];
AssertId($"span-{index}", span.SpanId);
Assert.Equal(KeyValuePair.Create("index", index.ToString(CultureInfo.InvariantCulture)), Assert.Single(span.Attributes));
}
}
private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => utcNow;
}
}