File: TelemetryRepositoryTests\LogTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using 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));
        }
    }
}