// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Threading.Channels;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Tests.Integration;
using Aspire.Dashboard.Utils;
using Google.Protobuf.Collections;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Proto.Logs.V1;
using OpenTelemetry.Proto.Trace.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
namespace Aspire.Dashboard.Tests.TelemetryRepositoryTests;
public abstract class LogTests : TelemetryRepositoryTestBase
{
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly ITestOutputHelper _testOutputHelper;
public LogTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Fact]
public async Task AddLogs()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
// 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 logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("546573745370616e4964", resource.SpanId);
Assert.Equal("5465737454726163654964", resource.TraceId);
Assert.Equal("Test {Log}", resource.OriginalFormat);
Assert.Equal("Test Value!", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
Assert.Collection(resource.Attributes,
p =>
{
Assert.Equal("Log", p.Key);
Assert.Equal("Value!", p.Value);
});
});
var propertyKeys = await repositoryContext.Repository.GetLogPropertyKeysAsync(resources[0].ResourceKey, TestContext.Current.CancellationToken);
Assert.Collection(propertyKeys,
s => Assert.Equal("Log", s));
}
[Fact]
public async Task GetLogSummaries_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: "trace",
spanId: "span",
startTime: s_testTime,
endTime: s_testTime.AddMinutes(1),
attributes: [KeyValuePair.Create("gen_ai.provider.name", "test")])
}
}
}
}
});
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(name: "frontend", instanceId: "frontend-1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(
time: s_testTime.AddMinutes(1),
message: "direct",
severity: SeverityNumber.Warn,
traceId: "direct-trace",
spanId: "direct-span",
attributes:
[
KeyValuePair.Create("custom", "match"),
KeyValuePair.Create("exception.stacktrace", "stack trace"),
KeyValuePair.Create("exception.message", "ignored message"),
KeyValuePair.Create("gen_ai.system", "test")
]),
CreateLogRecord(
time: s_testTime.AddMinutes(2),
message: "linked",
severity: SeverityNumber.Error,
traceId: "trace",
spanId: "span",
attributes:
[
KeyValuePair.Create("custom", "other"),
KeyValuePair.Create("exception.type", "TestException"),
KeyValuePair.Create("exception.message", "test message")
]),
CreateLogRecord(
time: s_testTime.AddMinutes(3),
message: "ordinary",
traceId: "ordinary-trace",
spanId: "ordinary-span",
attributes:
[
KeyValuePair.Create("custom", "other"),
KeyValuePair.Create("gen_ai.system", string.Empty),
KeyValuePair.Create("gen_ai.provider.name", "ignored fallback")
])
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var context = new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
};
var summaries = await repositoryContext.Repository.GetLogSummariesAsync(context, cancellationToken: CancellationToken.None);
var logs = await repositoryContext.Repository.GetLogsAsync(context, cancellationToken: CancellationToken.None);
Assert.Equal(logs.TotalItemCount, summaries.TotalItemCount);
Assert.Equal(logs.IsFull, summaries.IsFull);
Assert.Collection(summaries.Items,
summary =>
{
var log = logs.Items[0];
Assert.Equal(log.InternalId, summary.InternalId);
Assert.Equal(log.TimeStamp, summary.TimeStamp);
Assert.Equal(log.Severity, summary.Severity);
Assert.Equal(log.Message, summary.Message);
Assert.Equal(log.TraceId, summary.TraceId);
Assert.Equal(log.SpanId, summary.SpanId);
Assert.Equal(new ResourceKey("frontend", "frontend-1"), summary.Resource.ResourceKey);
Assert.Equal("stack trace", summary.ExceptionText);
Assert.True(summary.HasGenAI);
},
summary =>
{
Assert.Equal("linked", summary.Message);
Assert.Equal("TestException: test message", summary.ExceptionText);
Assert.True(summary.HasGenAI);
},
summary =>
{
Assert.Equal("ordinary", summary.Message);
Assert.Null(summary.ExceptionText);
Assert.False(summary.HasGenAI);
});
var latestContext = new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = [],
LatestItemCount = 2
};
var latestSummaries = await repositoryContext.Repository.GetLogSummariesAsync(latestContext, cancellationToken: CancellationToken.None);
var latestLogs = await repositoryContext.Repository.GetLogsAsync(latestContext, cancellationToken: CancellationToken.None);
Assert.Equal(3, latestSummaries.TotalItemCount);
Assert.Equal(latestLogs.TotalItemCount, latestSummaries.TotalItemCount);
Assert.Collection(latestSummaries.Items,
summary =>
{
Assert.Equal("linked", summary.Message);
Assert.Equal(s_testTime.AddMinutes(2), summary.TimeStamp);
},
summary =>
{
Assert.Equal("ordinary", summary.Message);
Assert.Equal(s_testTime.AddMinutes(3), summary.TimeStamp);
});
Assert.Collection(latestLogs.Items,
log =>
{
Assert.Equal("linked", log.Message);
Assert.Equal(s_testTime.AddMinutes(2), log.TimeStamp);
},
log =>
{
Assert.Equal("ordinary", log.Message);
Assert.Equal(s_testTime.AddMinutes(3), log.TimeStamp);
});
var filtered = await repositoryContext.Repository.GetLogSummariesAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters =
[
new FieldTelemetryFilter
{
Field = "custom",
Condition = FilterCondition.Equals,
Value = "match"
}
]
}, cancellationToken: CancellationToken.None);
Assert.Equal("direct", Assert.Single(filtered.Items).Message);
Assert.Equal(1, filtered.TotalItemCount);
var emptyPage = await repositoryContext.Repository.GetLogSummariesAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 10,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Empty(emptyPage.Items);
Assert.Equal(3, emptyPage.TotalItemCount);
var emptyLogsPage = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 10,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Empty(emptyLogsPage.Items);
Assert.Equal(3, emptyLogsPage.TotalItemCount);
}
[Fact]
public async Task GetLogsFieldValues_AllFieldsMatchMaterializedLogs()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime, message: "Message", attributes: [KeyValuePair.Create("custom", "Value")], severity: SeverityNumber.Info, eventName: "Event"),
CreateLogRecord(time: s_testTime, message: "message", attributes: [KeyValuePair.Create("custom", "value")], severity: SeverityNumber.Info2)
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var logs = (await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).Items;
foreach (var field in KnownStructuredLogFields.AllFields
.Except([KnownStructuredLogFields.TimestampField])
.Append(KnownStructuredLogFields.LevelField)
.Append("custom"))
{
var expected = logs
.Select(log => OtlpLogEntry.GetFieldValue(log, field))
.Where(value => value is not null)
.GroupBy(value => value!, StringComparers.OtlpAttribute)
.ToDictionary(group => group.Key, group => group.Count(), StringComparers.OtlpAttribute);
var actual = await repositoryContext.Repository.GetLogsFieldValuesAsync(field, TestContext.Current.CancellationToken);
Assert.True(expected.Count == actual.Count, $"Field '{field}' expected {expected.Count} values but found {actual.Count}.");
foreach (var (value, count) in expected)
{
Assert.True(actual.TryGetValue(value, out var actualCount), $"Field '{field}' is missing value '{value}'.");
Assert.True(count == actualCount, $"Field '{field}' value '{value}' expected count {count} but found {actualCount}.");
}
}
Assert.Empty(await repositoryContext.Repository.GetLogsFieldValuesAsync(KnownStructuredLogFields.TimestampField, TestContext.Current.CancellationToken));
}
[Fact]
public async Task AddLogs_NoBody_EmptyMessage()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(skipBody: true) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("", resource.Message);
});
}
[Fact]
public async Task AddLogs_MultipleOutOfOrder()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1"),
CreateLogRecord(time: s_testTime.AddMinutes(2), message: "2"),
CreateLogRecord(time: s_testTime.AddMinutes(3), message: "3"),
CreateLogRecord(time: s_testTime.AddMinutes(10), message: "10"),
CreateLogRecord(time: s_testTime.AddMinutes(9), message: "9"),
CreateLogRecord(time: s_testTime.AddMinutes(4), message: "4"),
CreateLogRecord(time: s_testTime.AddMinutes(5), message: "5"),
CreateLogRecord(time: s_testTime.AddMinutes(7), message: "7"),
CreateLogRecord(time: s_testTime.AddMinutes(6), message: "6"),
CreateLogRecord(time: s_testTime.AddMinutes(8), message: "8"),
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
l =>
{
Assert.Equal("1", l.Message);
Assert.Same(OtlpScope.Empty, l.Scope);
},
l => Assert.Equal("2", l.Message),
l => Assert.Equal("3", l.Message),
l => Assert.Equal("4", l.Message),
l => Assert.Equal("5", l.Message),
l => Assert.Equal("6", l.Message),
l => Assert.Equal("7", l.Message),
l => Assert.Equal("8", l.Message),
l => Assert.Equal("9", l.Message),
l => Assert.Equal("10", l.Message));
}
[Fact]
public async Task AddLogs_Error_UnviewedCount()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Trace),
CreateLogRecord(time: s_testTime.AddMinutes(2), message: "2", severity: SeverityNumber.Debug),
CreateLogRecord(time: s_testTime.AddMinutes(3), message: "3", severity: SeverityNumber.Info),
CreateLogRecord(time: s_testTime.AddMinutes(4), message: "4", severity: SeverityNumber.Warn),
CreateLogRecord(time: s_testTime.AddMinutes(5), message: "5", severity: SeverityNumber.Error),
CreateLogRecord(time: s_testTime.AddMinutes(6), message: "6", severity: SeverityNumber.Fatal)
}
}
}
},
new ResourceLogs
{
Resource = CreateResource(instanceId: "2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Fatal)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var unviewedCounts1 = repositoryContext.Repository.GetResourceUnviewedErrorLogsCount();
Assert.True(unviewedCounts1.TryGetValue(new ResourceKey("TestService", "1"), out var unviewedCount1));
Assert.Equal(2, unviewedCount1);
Assert.True(unviewedCounts1.TryGetValue(new ResourceKey("TestService", "2"), out var unviewedCount2));
Assert.Equal(1, unviewedCount2);
repositoryContext.Repository.MarkViewedErrorLogs(new ResourceKey("TestService", "1"));
var unviewedCounts2 = repositoryContext.Repository.GetResourceUnviewedErrorLogsCount();
Assert.False(unviewedCounts2.TryGetValue(new ResourceKey("TestService", "1"), out _));
Assert.True(unviewedCounts2.TryGetValue(new ResourceKey("TestService", "2"), out unviewedCount2));
Assert.Equal(1, unviewedCount2);
repositoryContext.Repository.MarkViewedErrorLogs(null);
var unviewedCounts3 = repositoryContext.Repository.GetResourceUnviewedErrorLogsCount();
Assert.False(unviewedCounts3.TryGetValue(new ResourceKey("TestService", "1"), out _));
Assert.False(unviewedCounts3.TryGetValue(new ResourceKey("TestService", "2"), out _));
}
[Fact]
public async Task AddLogs_Error_UnviewedCount_WithReadSubscriptionAll()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
using var subscription = repositoryContext.Repository.OnNewLogs(resourceKey: null, SubscriptionType.Read, () => Task.CompletedTask);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Error),
}
}
}
},
new ResourceLogs
{
Resource = CreateResource(instanceId: "2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Fatal)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var unviewedCounts = repositoryContext.Repository.GetResourceUnviewedErrorLogsCount();
Assert.False(unviewedCounts.TryGetValue(new ResourceKey("TestService", "1"), out _));
Assert.False(unviewedCounts.TryGetValue(new ResourceKey("TestService", "2"), out _));
}
[Fact]
public async Task AddLogs_Error_UnviewedCount_WithReadSubscriptionOneApp()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
using var subscription = repositoryContext.Repository.OnNewLogs(resourceKey: new ResourceKey("TestService", "1"), SubscriptionType.Read, () => Task.CompletedTask);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Error),
}
}
}
},
new ResourceLogs
{
Resource = CreateResource(instanceId: "2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Fatal)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var unviewedCounts = repositoryContext.Repository.GetResourceUnviewedErrorLogsCount();
Assert.False(unviewedCounts.TryGetValue(new ResourceKey("TestService", "1"), out _));
Assert.True(unviewedCounts.TryGetValue(new ResourceKey("TestService", "2"), out var unviewedCount));
Assert.Equal(1, unviewedCount);
}
[Fact]
public async Task AddLogs_Error_UnviewedCount_WithNonReadSubscription()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
using var subscription = repositoryContext.Repository.OnNewLogs(resourceKey: null, SubscriptionType.Other, () => Task.CompletedTask);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "1", severity: SeverityNumber.Error),
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var unviewedCounts = repositoryContext.Repository.GetResourceUnviewedErrorLogsCount();
Assert.True(unviewedCounts.TryGetValue(new ResourceKey("TestService", "1"), out var unviewedCount));
Assert.Equal(1, unviewedCount);
}
[Fact]
public async Task GetLogs_UnknownResource()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [new ResourceKey("TestService", "UnknownResource")],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
// Assert
Assert.Empty(logs.Items);
}
[Fact]
public async Task GetLogPropertyKeys_UnknownResource()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var propertyKeys = await repositoryContext.Repository.GetLogPropertyKeysAsync(new ResourceKey("TestService", "UnknownResource"), TestContext.Current.CancellationToken);
// Assert
Assert.Empty(propertyKeys);
}
[Fact]
public async Task Subscriptions_AddLog()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var newResourcesTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
repositoryContext.Repository.OnNewResources(() =>
{
newResourcesTcs.TrySetResult();
return Task.CompletedTask;
});
// Act 1
var addContext1 = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext1, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
// Assert 1
Assert.Equal(0, addContext1.FailureCount);
await newResourcesTcs.Task.DefaultTimeout();
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
// Act 2
var newLogsTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
repositoryContext.Repository.OnNewLogs(resources[0].ResourceKey, SubscriptionType.Read, () =>
{
newLogsTcs.TrySetResult();
return Task.CompletedTask;
});
var addContext2 = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext2, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
await newLogsTcs.Task.DefaultTimeout();
// Assert 2
Assert.Equal(0, addContext2.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 1,
Filters = []
}, cancellationToken: CancellationToken.None)!;
Assert.Single(logs.Items);
Assert.Equal(2, logs.TotalItemCount);
}
[Fact]
public async Task Unsubscribe()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var onNewResourcesCalled = false;
var subscription = repositoryContext.Repository.OnNewResources(() =>
{
onNewResourcesCalled = true;
return Task.CompletedTask;
});
subscription.Dispose();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
Assert.False(onNewResourcesCalled, "Callback shouldn't have been called because subscription was disposed.");
}
[Fact]
public async Task Subscription_RaisedFromDifferentContext_InitialContextPreserved()
{
// Arrange
var asyncLocal = new AsyncLocal<string>();
asyncLocal.Value = "CustomValue";
using var repositoryContext = await CreateRepositoryAsync();
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var subscription = repositoryContext.Repository.OnNewResources(() =>
{
tcs.SetResult(asyncLocal.Value);
return Task.CompletedTask;
});
// Act
Task task;
using (ExecutionContext.SuppressFlow())
{
task = Task.Run(async () =>
{
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
});
}
await task.DefaultTimeout();
// Assert
var callbackValue = await tcs.Task.DefaultTimeout();
Assert.Equal("CustomValue", callbackValue);
}
[Fact]
public async Task AddLogs_AttributeLimits_LimitsApplied()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync(maxAttributeCount: 5, maxAttributeLength: 16);
// Act
var attributes = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("{OriginalFormat}", "Test {Log}")
};
for (var i = 0; i < 10; i++)
{
var value = GetValue((i + 1) * 5);
attributes.Add(new KeyValuePair<string, string>($"Key{i}", value));
}
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(message: GetValue(50), attributes: attributes) }
}
}
}
});
// 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 logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("Test {Log}", resource.OriginalFormat);
Assert.Equal("0123456789012345", resource.Message);
Assert.Collection(resource.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);
});
});
}
[Fact]
public async Task Subscription_MultipleUpdates_MinExecuteIntervalApplied()
{
// Arrange
var minExecuteInterval = CallbackThrottler.DefaultMinExecuteInterval;
var loggerFactory = IntegrationTestHelpers.CreateLoggerFactory(_testOutputHelper);
var logger = loggerFactory.CreateLogger(nameof(LogTests));
using var repositoryContext = await CreateRepositoryAsync(subscriptionMinExecuteInterval: minExecuteInterval, loggerFactory: loggerFactory);
var stopwatch = new Stopwatch();
var callCount = 0;
var resultChannel = Channel.CreateUnbounded<int>();
var subscription = repositoryContext.Repository.OnNewLogs(resourceKey: null, SubscriptionType.Read, async () =>
{
if (!stopwatch.IsRunning)
{
stopwatch.Start();
}
else
{
stopwatch.Stop();
}
++callCount;
resultChannel.Writer.TryWrite(callCount);
await Task.Delay(20);
});
// Act
var addContext = new AddContext();
logger.LogInformation("Writing log 1");
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
// Assert
var read1 = await resultChannel.Reader.ReadAsync().DefaultTimeout();
Assert.Equal(1, read1);
logger.LogInformation("Received log 1 callback");
logger.LogInformation("Writing log 2");
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
var read2 = await resultChannel.Reader.ReadAsync().DefaultTimeout();
Assert.Equal(2, read2);
logger.LogInformation("Received log 2 callback");
var elapsed = stopwatch.Elapsed;
logger.LogInformation("Elapsed time: {Elapsed}", elapsed);
CustomAssert.AssertExceedsMinInterval(elapsed, minExecuteInterval);
}
[Fact]
public async Task FilterLogs_With_Message_Returns_CorrectLog()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "test_message", severity: SeverityNumber.Error),
}
}
}
}
});
var resourceKey = repositoryContext.Repository.GetResources().First().ResourceKey;
// Assert
Assert.Empty((await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 1,
Filters = [new FieldTelemetryFilter { Condition = FilterCondition.Contains, Field = nameof(OtlpLogEntry.Message), Value = "does_not_contain" }]
}, cancellationToken: CancellationToken.None)).Items);
Assert.Single((await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 1,
Filters = [new FieldTelemetryFilter { Condition = FilterCondition.Contains, Field = nameof(OtlpLogEntry.Message), Value = "MESSAGE" }]
}, cancellationToken: CancellationToken.None)).Items);
}
[Theory]
[InlineData("%")]
[InlineData("_")]
[InlineData("!")]
public async Task FilterLogs_WithLikeMetacharacter_TreatsValueAsLiteral(string fragment)
{
using var repositoryContext = await CreateRepositoryAsync();
var expectedMessage = $"matches-{fragment}-literal";
await repositoryContext.Repository.AsWriter().AddLogsAsync(new AddContext(), new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: expectedMessage),
CreateLogRecord(time: s_testTime.AddMinutes(2), message: "matches-x-literal")
}
}
}
}
});
var result = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [repositoryContext.Repository.GetResources().Single().ResourceKey],
StartIndex = 0,
Count = int.MaxValue,
Filters = [new FieldTelemetryFilter { Condition = FilterCondition.Contains, Field = nameof(OtlpLogEntry.Message), Value = fragment }]
}, cancellationToken: CancellationToken.None);
var log = Assert.Single(result.Items);
Assert.Equal(expectedMessage, log.Message);
}
[Fact]
public async Task FilterLogs_With_EventName_Returns_CorrectLog()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(instanceId: "1"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime.AddMinutes(1), message: "test_message", severity: SeverityNumber.Error, eventName: "MyEventName"),
}
}
}
}
});
var resourceKey = repositoryContext.Repository.GetResources().First().ResourceKey;
// Assert
Assert.Empty((await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 1,
Filters = [new FieldTelemetryFilter { Condition = FilterCondition.Contains, Field = KnownStructuredLogFields.EventNameField, Value = "does_not_contain" }]
}, cancellationToken: CancellationToken.None)).Items);
Assert.Single((await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 1,
Filters = [new FieldTelemetryFilter { Condition = FilterCondition.Contains, Field = KnownStructuredLogFields.EventNameField, Value = "MyEvent" }]
}, cancellationToken: CancellationToken.None)).Items);
}
[Fact]
public async Task AddLogs_MultipleResources_SameInstanceId_CreateMultipleResources()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(name: "App1", instanceId: "computer-name"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "App2", instanceId: "computer-name"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("App1", resource.ResourceName);
Assert.Equal("computer-name", resource.InstanceId);
},
resource =>
{
Assert.Equal("App2", resource.ResourceName);
Assert.Equal("computer-name", resource.InstanceId);
});
var logs1 = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resources[0].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs1.Items,
resource =>
{
Assert.Equal("546573745370616e4964", resource.SpanId);
Assert.Equal("5465737454726163654964", resource.TraceId);
Assert.Equal("Test {Log}", resource.OriginalFormat);
Assert.Equal("Test Value!", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
Assert.Collection(resource.Attributes,
p =>
{
Assert.Equal("Log", p.Key);
Assert.Equal("Value!", p.Value);
});
});
var logs2 = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resources[1].ResourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs2.Items,
resource =>
{
Assert.Equal("546573745370616e4964", resource.SpanId);
Assert.Equal("5465737454726163654964", resource.TraceId);
Assert.Equal("Test {Log}", resource.OriginalFormat);
Assert.Equal("Test Value!", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
Assert.Collection(resource.Attributes,
p =>
{
Assert.Equal("Log", p.Key);
Assert.Equal("Value!", p.Value);
});
});
}
[Fact]
public async Task GetLogs_MultipleInstances()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(1), message: "message-1", attributes: [KeyValuePair.Create("key-1", "value-1")]) }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(2), message: "message-2", attributes: [KeyValuePair.Create("key-2", "value-2")]) }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(3)) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resourceKey = new ResourceKey("resource1", InstanceId: null);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [resourceKey],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("message-1", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
Assert.Collection(resource.Attributes,
p =>
{
Assert.Equal("key-1", p.Key);
Assert.Equal("value-1", p.Value);
});
},
resource =>
{
Assert.Equal("message-2", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
Assert.Collection(resource.Attributes,
p =>
{
Assert.Equal("key-2", p.Key);
Assert.Equal("value-2", p.Value);
});
});
var propertyKeys = await repositoryContext.Repository.GetLogPropertyKeysAsync(resourceKey, TestContext.Current.CancellationToken);
Assert.Collection(propertyKeys,
s => Assert.Equal("key-1", s),
s => Assert.Equal("key-2", s));
}
[Fact]
public async Task RemoveLogs_All()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(1), message: "message-1") }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(2), message: "message-2") }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(3)) }
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearStructuredLogsAsync();
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.NotNull(logs);
Assert.Empty(logs.Items);
Assert.Equal(0, logs.TotalItemCount);
}
[Fact]
public async Task RemoveLogs_SelectedResource()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(1), message: "message-1") }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(2), message: "message-2") }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(3), message: "message-3") }
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearStructuredLogsAsync(new ResourceKey("resource1", "123"));
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Equal(2, logs.TotalItemCount);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("message-2", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
},
resource =>
{
Assert.Equal("message-3", resource.Message);
Assert.Equal("TestLogger", resource.Scope.Name);
});
}
[Fact]
public async Task RemoveLogs_MultipleSelectedResources()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(1), message: "message-1") }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(2), message: "message-2") }
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "resource2"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: s_testTime.AddMinutes(3), message: "message-3") }
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearStructuredLogsAsync(new ResourceKey("resource1", null));
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Equal(1, logs.TotalItemCount);
var log = Assert.Single(logs.Items);
Assert.Equal("message-3", log.Message);
Assert.Equal("TestLogger", log.Scope.Name);
}
[Fact]
public async Task AddLogs_ObservedUnixTimeNanos()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(time: DateTime.UnixEpoch, observedTime: s_testTime.AddMinutes(1)) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal(s_testTime.AddMinutes(1), resource.TimeStamp);
});
}
[Fact]
public async Task AddLogs_EventName_FromLogRecordField()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(eventName: "TestEvent") }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("TestEvent", resource.EventName);
});
}
[Fact]
public async Task AddLogs_EventName_FromLegacyAttribute()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(attributes: [new KeyValuePair<string, string>("logrecord.event.name", "LegacyEvent")]) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Equal("LegacyEvent", resource.EventName);
// Legacy attribute should be filtered out
Assert.DoesNotContain(resource.Attributes, a => a.Key == "logrecord.event.name");
});
}
[Fact]
public async Task AddLogs_EventName_FieldTakesPrecedenceOverAttribute()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(eventName: "FieldEvent", attributes: [new KeyValuePair<string, string>("logrecord.event.name", "AttributeEvent")]) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
// Field should take precedence over attribute
Assert.Equal("FieldEvent", resource.EventName);
});
}
[Fact]
public async Task AddLogs_EventName_NullWhenNotSet()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddLogsAsync(addContext, new RepeatedField<ResourceLogs>()
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { CreateLogRecord(attributes: []) }
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None);
Assert.Collection(logs.Items,
resource =>
{
Assert.Null(resource.EventName);
});
}
[Fact]
public async Task GetLogs_DisabledFiltersAreIgnored()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddLogsAsync(new AddContext(), new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(time: s_testTime, message: "matching log", severity: SeverityNumber.Info),
CreateLogRecord(time: s_testTime.AddSeconds(1), message: "other log", severity: SeverityNumber.Info)
}
}
}
}
});
// Enabled filter matches "matching", disabled filter would exclude everything
var filters = new List<TelemetryFilter>
{
new FieldTelemetryFilter
{
Field = nameof(OtlpLogEntry.Message),
Value = "matching",
Condition = FilterCondition.Contains,
Enabled = true
},
new FieldTelemetryFilter
{
Field = nameof(OtlpLogEntry.Message),
Value = "IMPOSSIBLE",
Condition = FilterCondition.Contains,
Enabled = false
}
};
var logs = await repositoryContext.Repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = filters
}, cancellationToken: CancellationToken.None);
// The disabled filter should be ignored — only the enabled "matching" filter applies
Assert.Single(logs.Items);
Assert.Equal("matching log", logs.Items[0].Message);
}
}
public sealed class SqliteLogTests(ITestOutputHelper testOutputHelper) : LogTests(testOutputHelper)
{
[Fact]
public async Task GetLogSummaries_MoreThanSqliteVariableLimit_ReturnsTraceDisplayData()
{
const int logCount = 1_100;
var testTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
var logRecords = new RepeatedField<LogRecord>();
for (var index = 0; index < logCount; index++)
{
logRecords.Add(CreateLogRecord(
time: testTime.AddTicks(index + 1),
message: $"Message {index}",
severity: index == 0 ? SeverityNumber.Error : SeverityNumber.Info,
attributes: [],
traceId: "large-trace",
spanId: "large-span",
eventName: $"Event {index}"));
}
var context = new AddContext();
await repository.AsWriter().AddLogsAsync(context, new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords = { logRecords }
}
}
}
});
Assert.Equal(0, context.FailureCount);
var logs = (await repository.GetLogSummariesAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = int.MaxValue,
Filters =
[
new FieldTelemetryFilter
{
Field = KnownStructuredLogFields.TraceIdField,
Condition = FilterCondition.Equals,
Value = logRecords[0].TraceId.ToHexString()
}
]
}, cancellationToken: CancellationToken.None)).Items;
Assert.Equal(logCount, logs.Count);
var first = logs[0];
Assert.Equal(testTime.AddTicks(1), first.TimeStamp);
Assert.Equal(LogLevel.Error, first.Severity);
Assert.Equal("Message 0", first.Message);
Assert.Equal(logRecords[0].SpanId.ToHexString(), first.SpanId);
Assert.Equal("TestLogger", first.ScopeName);
Assert.Equal("Event 0", first.EventName);
Assert.True(first.IsError);
Assert.Equal("Message 1099", logs[^1].Message);
}
[Fact]
public async Task AddLogs_LargeAttributeBatchesRoundTripAcrossResources()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
var context = new AddContext();
var attributes = Enumerable.Range(0, 128)
.Select(index => KeyValuePair.Create($"key-{index}", $"value-{index}"))
.ToArray();
await repository.AsWriter().AddLogsAsync(context, new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(name: "app-one"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(message: "one", attributes: attributes),
CreateLogRecord(message: "two", attributes: attributes)
}
}
}
},
new ResourceLogs
{
Resource = CreateResource(name: "app-two"),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("TestLogger"),
LogRecords =
{
CreateLogRecord(message: "three", attributes: attributes),
CreateLogRecord(message: "four", attributes: attributes)
}
}
}
}
});
Assert.Equal(4, context.SuccessCount);
Assert.Equal(0, context.FailureCount);
await AssertResourceLogsAsync("app-one", ["one", "two"]);
await AssertResourceLogsAsync("app-two", ["three", "four"]);
async Task AssertResourceLogsAsync(string resourceName, string[] expectedMessages)
{
var logs = (await repository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [new ResourceKey(resourceName, null)],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).Items;
Assert.Equal(expectedMessages.Order(), logs.Select(log => log.Message).Order());
Assert.All(logs, log => Assert.Equal(attributes, log.Attributes));
}
}
}