From 80bf3a44724c1a76811e95024449573114377d0d Mon Sep 17 00:00:00 2001 From: Tech Garage Date: Wed, 31 Jul 2024 00:35:16 +0330 Subject: [PATCH 01/14] Add Sqlite Provider --- Serilog.Ui.sln | 14 ++ .../SerilogUiOptionBuilderExtensions.cs | 47 ++++++ .../Serilog.Ui.SqliteDataProvider.csproj | 16 ++ .../Serilog.Ui.SqliteDataProvider.sln | 25 ++++ .../SqliteDataProvider.cs | 141 ++++++++++++++++++ .../SqlUtil/Costants.cs | 22 +++ .../DataProvider/DataProviderBaseTest.cs | 34 +++++ .../DataProviderPaginationTest.cs | 32 ++++ .../DataProvider/DataProviderSearchTest.cs | 41 +++++ .../SerilogUiOptionBuilderExtensionsTest.cs | 57 +++++++ .../Serilog.Ui.SqliteProvider.Tests.csproj | 24 +++ .../Util/SqliteTestProvider.cs | 58 +++++++ 12 files changed, 511 insertions(+) create mode 100644 src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs create mode 100644 src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj create mode 100644 src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln create mode 100644 src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs diff --git a/Serilog.Ui.sln b/Serilog.Ui.sln index 7ea6fb2e..226905d0 100644 --- a/Serilog.Ui.sln +++ b/Serilog.Ui.sln @@ -64,6 +64,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider.Tests", "tests\Serilog.Ui.RavenDbProvider.Tests\Serilog.Ui.RavenDbProvider.Tests.csproj", "{B785845B-D858-4562-B224-67468B4FEE41}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteDataProvider", "src\Serilog.Ui.SqliteDataProvider\Serilog.Ui.SqliteDataProvider.csproj", "{A23F4275-DB47-40C9-96CE-1116E20F5EB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteProvider.Tests", "tests\Serilog.Ui.SqliteProvider.Tests\Serilog.Ui.SqliteProvider.Tests.csproj", "{C9CBABEA-622C-4E11-9D68-816F685E8E0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -140,6 +144,14 @@ Global {B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.Build.0 = Debug|Any CPU {B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.ActiveCfg = Release|Any CPU {B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.Build.0 = Release|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.Build.0 = Release|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -164,6 +176,8 @@ Global {DCB452AD-2E0E-4D6A-B46D-72D0AF247381} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E} {8973E5F5-FD9B-41B1-B2D6-8B281754C443} = {ACA69857-2E3E-468C-B0B0-A86852E3492D} {B785845B-D858-4562-B224-67468B4FEE41} = {75F9223B-15F2-4465-B01D-2A5A49FCD000} + {A23F4275-DB47-40C9-96CE-1116E20F5EB7} = {ACA69857-2E3E-468C-B0B0-A86852E3492D} + {C9CBABEA-622C-4E11-9D68-816F685E8E0D} = {75F9223B-15F2-4465-B01D-2A5A49FCD000} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {88374732-FEAD-4375-9CF1-75331A37CF07} diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs new file mode 100644 index 00000000..f310c14f --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.DependencyInjection; +using Serilog.Ui.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serilog.Ui.SqliteDataProvider +{ + /// + /// Sqlite data provider specific extension methods for . + /// + public static class SerilogUiOptionBuilderExtensions + { + /// + /// Configures the SerilogUi to connect to a Sqlite database. + /// + /// The options builder. + /// The connection string. + /// Name of the table. + /// throw if connectionString is null + /// throw is tableName is null + public static void UseSqliteServer( + this SerilogUiOptionsBuilder optionsBuilder, + string connectionString, + string tableName + ) + { + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentNullException(nameof(connectionString)); + + if (string.IsNullOrWhiteSpace(tableName)) + throw new ArgumentNullException(nameof(tableName)); + + var relationProvider = new RelationalDbOptions + { + ConnectionString = connectionString, + TableName = tableName + }; + + ((ISerilogUiOptionsBuilder)optionsBuilder).Services + .AddScoped(p => ActivatorUtilities.CreateInstance(p, relationProvider)); + + } + } +} diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj new file mode 100644 index 00000000..7eec7b95 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln new file mode 100644 index 00000000..81b638c2 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34916.146 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteDataProvider", "Serilog.Ui.SqliteDataProvider.csproj", "{CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A770A0A3-5E95-42EE-BE36-7E8AACE8F2DC} + EndGlobalSection +EndGlobal diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs new file mode 100644 index 00000000..e6255fbe --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -0,0 +1,141 @@ +using Dapper; +using Microsoft.Data.Sqlite; +using Serilog.Ui.Core; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Serilog.Ui.SqliteDataProvider +{ + public class SqliteDataProvider : IDataProvider + { + private readonly RelationalDbOptions _options; + + public SqliteDataProvider(RelationalDbOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task<(IEnumerable, int)> FetchDataAsync( + int page, + int count, + string level = null, + string searchCriteria = null, + DateTime? startDate = null, + DateTime? endDate = null + ) + { + var logsTask = GetLogs(page - 1, count, level, searchCriteria, startDate, endDate); + var logCountTask = CountLogs(level, searchCriteria, startDate, endDate); + + await Task.WhenAll(logsTask, logCountTask); + + return (await logsTask, await logCountTask); + } + + public string Name => _options.ToDataProviderName("Sqlite"); + + private Task> GetLogs( + int page, + int count, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + var queryBuilder = new StringBuilder(); + queryBuilder.Append("SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM "); + queryBuilder.Append(_options.TableName); + queryBuilder.Append(" "); + + GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate); + + queryBuilder.Append("ORDER BY Id DESC LIMIT @Offset, @Count"); + + using (var connection = new SqliteConnection(_options.ConnectionString)) + { + var param = new + { + Offset = page * count, + Count = count, + Level = level, + Search = searchCriteria != null ? $"%{searchCriteria}%" : null, + StartDate = startDate, + EndDate = endDate + }; + var logs = connection.Query(queryBuilder.ToString(), param); + var index = 1; + foreach (var log in logs) + log.RowNo = (page * count) + index++; + + return Task.FromResult(logs); + } + } + + private Task CountLogs( + string level, + string searchCriteria, + DateTime? startDate = null, + DateTime? endDate = null) + { + var queryBuilder = new StringBuilder(); + queryBuilder.Append("SELECT COUNT(Id) FROM "); + queryBuilder.Append(_options.TableName); + queryBuilder.Append(" "); + + GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate); + + using (var connection = new SqliteConnection(_options.ConnectionString)) + { + return Task.FromResult(connection.QueryFirstOrDefault(queryBuilder.ToString(), + new + { + Level = level, + Search = searchCriteria != null ? "%" + searchCriteria + "%" : null, + StartDate = startDate, + EndDate = endDate + })); + } + } + + private void GenerateWhereClause( + StringBuilder queryBuilder, + string level, + string searchCriteria, + DateTime? startDate = null, + DateTime? endDate = null) + { + var whereIncluded = false; + + if (!string.IsNullOrEmpty(level)) + { + queryBuilder.Append("WHERE Level = @Level "); + whereIncluded = true; + } + + if (!string.IsNullOrEmpty(searchCriteria)) + { + queryBuilder.Append(whereIncluded + ? "AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) " + : "WHERE (RenderedMessage LIKE @Search OR Exception LIKE @Search) "); + whereIncluded = true; + } + + if (startDate != null) + { + queryBuilder.Append(whereIncluded + ? "AND Timestamp >= @StartDate " + : "WHERE Timestamp >= @StartDate "); + whereIncluded = true; + } + + if (endDate != null) + { + queryBuilder.Append(whereIncluded + ? "AND Timestamp <= @EndDate " + : "WHERE Timestamp <= @EndDate "); + } + } + } +} diff --git a/tests/Serilog.Ui.Common.Tests/SqlUtil/Costants.cs b/tests/Serilog.Ui.Common.Tests/SqlUtil/Costants.cs index 68ac973d..a87ff51e 100644 --- a/tests/Serilog.Ui.Common.Tests/SqlUtil/Costants.cs +++ b/tests/Serilog.Ui.Common.Tests/SqlUtil/Costants.cs @@ -70,5 +70,27 @@ public static class Costants $"@{nameof(LogModel.Exception)}," + $"@{nameof(LogModel.Properties)}" + ")"; + + // https://github.com/saleem-mirza/serilog-sinks-sqlite/blob/dev/src/Serilog.Sinks.SQLite/Sinks/SQLite/SQLiteSink.cs + public const string SqliteCreateTable = "CREATE TABLE IF NOT EXISTS Logs (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "Timestamp TEXT," + + "LogLevel VARCHAR(10)," + + "Exception TEXT," + + "Message TEXT," + + "Properties TEXT," + + "_ts TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now'))" + // SQLite equivalent for CURRENT_TIMESTAMP + ")"; + + public const string SqliteInsertFakeData = "INSERT INTO Logs" + + "(Timestamp, LogLevel, Exception, Message, Properties)" + + "VALUES (" + + $"@{nameof(LogModel.Timestamp)}," + + $"@{nameof(LogModel.Level)}," + + $"@{nameof(LogModel.Exception)}," + + $"@{nameof(LogModel.Message)}," + + $"@{nameof(LogModel.Properties)}" + + ")"; + } } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs new file mode 100644 index 00000000..4f4692ae --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using Serilog.Ui.Common.Tests.TestSuites; +using Serilog.Ui.SqliteDataProvider; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Sqlite.Tests.DataProvider +{ + [Trait("Unit-Base", "Sqlite")] + public class DataProviderBaseTest : IUnitBaseTests + { + [Fact] + public void It_throws_when_any_dependency_is_null() + { + var suts = new List> + { + () => new SqliteDataProvider(null), + }; + + suts.ForEach(sut => sut.Should().ThrowExactly()); + } + + [Fact] + public Task It_logs_and_throws_when_db_read_breaks_down() + { + var sut = new SqliteDataProvider(new() { ConnectionString = "connString", Schema = "dbo", TableName = "logs" }); + + var assert = () => sut.FetchDataAsync(1, 10); + return assert.Should().ThrowExactlyAsync(); + } + } +} diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs new file mode 100644 index 00000000..d7c3a585 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using Microsoft.Data.Sqlite; +using MySql.Tests.Util; +using Serilog.Ui.Common.Tests.TestSuites.Impl; +using Serilog.Ui.SqliteDataProvider; +using System.Threading.Tasks; +using Xunit; + +namespace Sqlite.Tests.DataProvider +{ + [Collection(nameof(SqliteDataProvider))] + [Trait("Integration-Pagination", "Sqlite")] + public class DataProviderPaginationTest : IntegrationPaginationTests + { + public DataProviderPaginationTest(SqliteTestProvider instance) : base(instance) + { + } + + public override Task It_fetches_with_limit() => base.It_fetches_with_limit(); + + public override Task It_fetches_with_limit_and_skip() => base.It_fetches_with_limit_and_skip(); + + public override Task It_fetches_with_skip() => base.It_fetches_with_skip(); + + [Fact] + public override Task It_throws_when_skip_is_zero() + { + var test = () => provider.FetchDataAsync(0, 1); + return test.Should().ThrowAsync(); + } + } +} diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs new file mode 100644 index 00000000..5d062675 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs @@ -0,0 +1,41 @@ +using MsSql.Tests.DataProvider; +using MySql.Tests.Util; +using Serilog.Ui.SqliteDataProvider; +using System.Threading.Tasks; +using Xunit; + +namespace Sqlite.Tests.DataProvider +{ + [Collection(nameof(SqliteDataProvider))] + [Trait("Integration-Search", "Sqlite")] + public class DataProviderSearchTest : IntegrationSearchTests + { + public DataProviderSearchTest(SqliteTestProvider instance) : base(instance) + { + } + + public override Task It_finds_all_data_with_default_search() + => base.It_finds_all_data_with_default_search(); + + public override Task It_finds_data_with_all_filters() + => base.It_finds_data_with_all_filters(); + + public override Task It_finds_only_data_emitted_after_date() + => base.It_finds_only_data_emitted_after_date(); + + public override Task It_finds_only_data_emitted_before_date() + => base.It_finds_only_data_emitted_before_date(); + + public override Task It_finds_only_data_emitted_in_dates_range() + => base.It_finds_only_data_emitted_in_dates_range(); + + public override Task It_finds_only_data_with_specific_level() + => base.It_finds_only_data_with_specific_level(); + + public override Task It_finds_only_data_with_specific_message_content() + => base.It_finds_only_data_with_specific_message_content(); + + public override Task It_finds_same_data_on_same_repeated_search() + => base.It_finds_same_data_on_same_repeated_search(); + } +} \ No newline at end of file diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs new file mode 100644 index 00000000..290a4903 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Serilog.Ui.Core; +using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.Web; +using System; +using System.Collections.Generic; +using Xunit; + +namespace MySql.Tests.Extensions +{ + [Trait("DI-DataProvider", "Sqlite")] + public class SerilogUiOptionBuilderExtensionsTest + { + private readonly ServiceCollection serviceCollection; + + public SerilogUiOptionBuilderExtensionsTest() + { + serviceCollection = new ServiceCollection(); + } + + [Fact] + public void It_registers_provider_and_dependencies() + { + serviceCollection.AddSerilogUi((builder) => + { + builder.UseSqliteServer("https://mysqlserver.example.com", "my-table"); + }); + var services = serviceCollection.BuildServiceProvider(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + + var provider = scope.ServiceProvider.GetService(); + provider.Should().NotBeNull().And.BeOfType(); + } + + [Fact] + public void It_throws_on_invalid_registration() + { + var nullables = new List> + { + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer(null, "name")), + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer(" ", "name")), + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("", "name")), + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("name", null)), + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("name", " ")), + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("name", "")), + }; + + foreach (var nullable in nullables) + { + nullable.Should().ThrowExactly(); + } + } + } +} diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj new file mode 100644 index 00000000..d505c210 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + Sqlite.Tests + Sqlite.Tests + false + + + + + %(Filename)%(Extension) + PreserveNewest + + + + + + + + + + diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs new file mode 100644 index 00000000..b12347ce --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -0,0 +1,58 @@ +using Ardalis.GuardClauses; +using Dapper; +using Microsoft.Data.Sqlite; +using Serilog.Ui.Common.Tests.DataSamples; +using Serilog.Ui.Common.Tests.SqlUtil; +using Serilog.Ui.Core; +using Serilog.Ui.SqliteDataProvider; +using System.Threading.Tasks; +using Xunit; + +namespace MySql.Tests.Util +{ + [CollectionDefinition(nameof(SqliteDataProvider))] + public class MySqlCollection : ICollectionFixture { } + + public sealed class SqliteTestProvider : DatabaseInstance + { + protected override string Name => "SqliteInMemory"; + + public SqliteTestProvider() : base() + { + // No need to set up a container for SQLite - using in-memory database + } + + public RelationalDbOptions DbOptions { get; set; } = new() + { + TableName = "Logs", + Schema = "dbo" + }; + + protected override async Task CheckDbReadinessAsync() + { + Guard.Against.Null(DbOptions); + + using var connection = new SqliteConnection("DataSource=:memory:"); + await connection.OpenAsync(); + + DbOptions.ConnectionString = connection.ConnectionString; + + await connection.ExecuteAsync("SELECT 1"); + } + + protected override async Task InitializeAdditionalAsync() + { + var logs = LogModelFaker.Logs(100); + Collector = new LogModelPropsCollector(logs); + + using var connection = new SqliteConnection(DbOptions.ConnectionString); + await connection.OpenAsync(); + + await connection.ExecuteAsync(Costants.SqliteCreateTable); + + await connection.ExecuteAsync(Costants.SqliteInsertFakeData, logs); + + Provider = new SqliteDataProvider(DbOptions); // Update this if needed for SQLite + } + } +} From d69eddd28027b84f1c1a801e2d556728ea17bc9c Mon Sep 17 00:00:00 2001 From: followynne Date: Mon, 12 Aug 2024 11:27:28 +0200 Subject: [PATCH 02/14] feat: align new provider with v3 --- .../SerilogUiOptionBuilderExtensions.cs | 41 ++--- .../Serilog.Ui.SqliteDataProvider.csproj | 6 + .../Serilog.Ui.SqliteDataProvider.sln | 25 --- .../SqliteDataProvider.cs | 161 ++++++++---------- 4 files changed, 89 insertions(+), 144 deletions(-) delete mode 100644 src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index f310c14f..86836193 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,47 +1,34 @@ using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; +using Serilog.Ui.Core.Interfaces; +using Serilog.Ui.Core.Models.Options; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Serilog.Ui.SqliteDataProvider +namespace Serilog.Ui.SqliteDataProvider.Extensions { /// - /// Sqlite data provider specific extension methods for . + /// SQLite data provider specific extension methods for . /// public static class SerilogUiOptionBuilderExtensions { - /// - /// Configures the SerilogUi to connect to a Sqlite database. - /// + /// Configures the SerilogUi to connect to a SQLite database. /// The options builder. - /// The connection string. - /// Name of the table. - /// throw if connectionString is null - /// throw is tableName is null - public static void UseSqliteServer( - this SerilogUiOptionsBuilder optionsBuilder, - string connectionString, - string tableName - ) + /// The SQLite options action. + public static ISerilogUiOptionsBuilder UseSqliteServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions) { - if (string.IsNullOrWhiteSpace(connectionString)) - throw new ArgumentNullException(nameof(connectionString)); + var dbOptions = new RelationalDbOptions(string.Empty); + setupOptions(dbOptions); + dbOptions.Validate(); - if (string.IsNullOrWhiteSpace(tableName)) - throw new ArgumentNullException(nameof(tableName)); - - var relationProvider = new RelationalDbOptions - { - ConnectionString = connectionString, - TableName = tableName - }; - - ((ISerilogUiOptionsBuilder)optionsBuilder).Services - .AddScoped(p => ActivatorUtilities.CreateInstance(p, relationProvider)); + optionsBuilder.Services.AddScoped(p => new SqliteDataProvider(dbOptions)); + return optionsBuilder; } } } diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj index 7eec7b95..b4f6fa19 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -1,7 +1,13 @@  + Serilog.UI.Sqlite netstandard2.0 + latest + 1.0.0-beta.1 + + SQLite data provider for Serilog UI. + serilog serilog-ui serilog.sinks.sqlite sqlite diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln deleted file mode 100644 index 81b638c2..00000000 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34916.146 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteDataProvider", "Serilog.Ui.SqliteDataProvider.csproj", "{CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB5BAEF0-7FC6-44A3-B36C-B8E04FA26745}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A770A0A3-5E95-42EE-BE36-7E8AACE8F2DC} - EndGlobalSection -EndGlobal diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs index e6255fbe..216169a1 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -1,141 +1,118 @@ -using Dapper; +using Ardalis.GuardClauses; +using Dapper; using Microsoft.Data.Sqlite; using Serilog.Ui.Core; +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.Models.Options; using System; using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; +using static Serilog.Ui.Core.Models.SearchOptions; namespace Serilog.Ui.SqliteDataProvider { - public class SqliteDataProvider : IDataProvider + public class SqliteDataProvider(RelationalDbOptions options) : IDataProvider { - private readonly RelationalDbOptions _options; + internal const string SqliteProviderName = "SQLite"; + private readonly RelationalDbOptions _options = Guard.Against.Null(options); - public SqliteDataProvider(RelationalDbOptions options) + public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public async Task<(IEnumerable, int)> FetchDataAsync( - int page, - int count, - string level = null, - string searchCriteria = null, - DateTime? startDate = null, - DateTime? endDate = null - ) - { - var logsTask = GetLogs(page - 1, count, level, searchCriteria, startDate, endDate); - var logCountTask = CountLogs(level, searchCriteria, startDate, endDate); + var logsTask = GetLogsAsync(queryParams); + var logCountTask = CountLogsAsync(queryParams); await Task.WhenAll(logsTask, logCountTask); return (await logsTask, await logCountTask); } - public string Name => _options.ToDataProviderName("Sqlite"); + public string Name => _options.GetProviderName(SqliteProviderName); - private Task> GetLogs( - int page, - int count, - string level, - string searchCriteria, - DateTime? startDate, - DateTime? endDate) + private async Task> GetLogsAsync(FetchLogsQuery queryParams) { var queryBuilder = new StringBuilder(); - queryBuilder.Append("SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM "); - queryBuilder.Append(_options.TableName); - queryBuilder.Append(" "); + queryBuilder.Append("SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties "); + queryBuilder.Append($"FROM {_options.TableName} "); + + GenerateWhereClause(queryBuilder, queryParams); - GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate); + GenerateSortClause(queryBuilder, queryParams.SortOn, queryParams.SortBy); - queryBuilder.Append("ORDER BY Id DESC LIMIT @Offset, @Count"); + queryBuilder.Append("LIMIT @Offset, @Count"); - using (var connection = new SqliteConnection(_options.ConnectionString)) + var rowNoStart = queryParams.Page * queryParams.Count; + + using var connection = new SqliteConnection(_options.ConnectionString); + var queryParameters = new { - var param = new - { - Offset = page * count, - Count = count, - Level = level, - Search = searchCriteria != null ? $"%{searchCriteria}%" : null, - StartDate = startDate, - EndDate = endDate - }; - var logs = connection.Query(queryBuilder.ToString(), param); - var index = 1; - foreach (var log in logs) - log.RowNo = (page * count) + index++; - - return Task.FromResult(logs); - } + Offset = rowNoStart, + queryParams.Count, + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + queryParams.StartDate, + queryParams.EndDate + }; + var logs = await connection.QueryAsync(queryBuilder.ToString(), queryParameters); + + return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); } - private Task CountLogs( - string level, - string searchCriteria, - DateTime? startDate = null, - DateTime? endDate = null) + private Task CountLogsAsync(FetchLogsQuery queryParams) { var queryBuilder = new StringBuilder(); - queryBuilder.Append("SELECT COUNT(Id) FROM "); - queryBuilder.Append(_options.TableName); - queryBuilder.Append(" "); + queryBuilder.Append($"SELECT COUNT(Id) FROM {_options.TableName} "); - GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate); + GenerateWhereClause(queryBuilder, queryParams); - using (var connection = new SqliteConnection(_options.ConnectionString)) - { - return Task.FromResult(connection.QueryFirstOrDefault(queryBuilder.ToString(), - new - { - Level = level, - Search = searchCriteria != null ? "%" + searchCriteria + "%" : null, - StartDate = startDate, - EndDate = endDate - })); - } + using var connection = new SqliteConnection(_options.ConnectionString); + + return connection.QueryFirstOrDefaultAsync( + queryBuilder.ToString(), + new + { + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + queryParams.StartDate, + queryParams.EndDate + }); } - private void GenerateWhereClause( - StringBuilder queryBuilder, - string level, - string searchCriteria, - DateTime? startDate = null, - DateTime? endDate = null) + private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams) { - var whereIncluded = false; + var conditionStart = "WHERE"; - if (!string.IsNullOrEmpty(level)) + if (!string.IsNullOrWhiteSpace(queryParams.Level)) { - queryBuilder.Append("WHERE Level = @Level "); - whereIncluded = true; + queryBuilder.Append($"{conditionStart} Level = @Level "); + conditionStart = "AND"; } - if (!string.IsNullOrEmpty(searchCriteria)) + if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria)) { - queryBuilder.Append(whereIncluded - ? "AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) " - : "WHERE (RenderedMessage LIKE @Search OR Exception LIKE @Search) "); - whereIncluded = true; + // TODO Exception as RemovableColumn? + queryBuilder.Append($"{conditionStart} (RenderedMessage LIKE @Search OR Exception LIKE @Search) "); + conditionStart = "AND"; } - if (startDate != null) + if (queryParams.StartDate != null) { - queryBuilder.Append(whereIncluded - ? "AND Timestamp >= @StartDate " - : "WHERE Timestamp >= @StartDate "); - whereIncluded = true; + queryBuilder.Append($"{conditionStart} Timestamp >= @StartDate "); + conditionStart = "AND"; } - if (endDate != null) + if (queryParams.EndDate != null) { - queryBuilder.Append(whereIncluded - ? "AND Timestamp <= @EndDate " - : "WHERE Timestamp <= @EndDate "); + queryBuilder.Append($"{conditionStart} Timestamp <= @EndDate "); } } + + private void GenerateSortClause(StringBuilder queryBuilder, SortProperty sortOn, SortDirection sortBy) + { + // TODO + queryBuilder.Append("ORDER BY Id DESC "); + } } } From 06e9534bb6f5dd786b74f57775241350d5c06d6c Mon Sep 17 00:00:00 2001 From: followynne Date: Mon, 12 Aug 2024 11:45:10 +0200 Subject: [PATCH 03/14] tmp --- .../DataProvider/DataProviderSearchTest.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs index 5d062675..e1fa1e69 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs @@ -1,4 +1,4 @@ -using MsSql.Tests.DataProvider; +using MsSql.Tests.DataProvider; // TODO nope using MySql.Tests.Util; using Serilog.Ui.SqliteDataProvider; using System.Threading.Tasks; @@ -10,6 +10,27 @@ namespace Sqlite.Tests.DataProvider [Trait("Integration-Search", "Sqlite")] public class DataProviderSearchTest : IntegrationSearchTests { + + // https://github.com/saleem-mirza/serilog-sinks-sqlite/blob/dev/src/Serilog.Sinks.SQLite/Sinks/SQLite/SQLiteSink.cs + public const string SqliteCreateTable = "CREATE TABLE IF NOT EXISTS Logs (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "Timestamp TEXT," + + "LogLevel VARCHAR(10)," + + "Exception TEXT," + + "Message TEXT," + + "Properties TEXT," + + "_ts TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now'))" + // SQLite equivalent for CURRENT_TIMESTAMP + ")"; + + public const string SqliteInsertFakeData = "INSERT INTO Logs" + + "(Timestamp, LogLevel, Exception, Message, Properties)" + + "VALUES (" + + $"@{nameof(LogModel.Timestamp)}," + + $"@{nameof(LogModel.Level)}," + + $"@{nameof(LogModel.Exception)}," + + $"@{nameof(LogModel.Message)}," + + $"@{nameof(LogModel.Properties)}" + + ")"; public DataProviderSearchTest(SqliteTestProvider instance) : base(instance) { } From e4f356b2d5e121fd09bf1791ef87fc9015f2a67b Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Thu, 3 Oct 2024 20:54:38 +0200 Subject: [PATCH 04/14] feat: convert sqllite to new models --- .../SerilogUiOptionBuilderExtensions.cs | 41 +++--- .../Extensions/SqliteDbOptions.cs | 10 ++ .../Models/SqliteSinkColumnNames.cs | 16 +++ .../SqliteDataProvider.cs | 130 ++++++------------ .../SqliteQueryBuilder.cs | 84 +++++++++++ 5 files changed, 168 insertions(+), 113 deletions(-) create mode 100644 src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs create mode 100644 src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs create mode 100644 src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 86836193..43b8b9ac 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -3,32 +3,27 @@ using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Core.Models.Options; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Serilog.Ui.SqliteDataProvider.Extensions +namespace Serilog.Ui.SqliteDataProvider.Extensions; + +/// +/// SQLite data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions { - /// - /// SQLite data provider specific extension methods for . - /// - public static class SerilogUiOptionBuilderExtensions + /// Configures the SerilogUi to connect to a SQLite database. + /// The options builder. + /// The SQLite options action. + public static ISerilogUiOptionsBuilder UseSqliteServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions) { - /// Configures the SerilogUi to connect to a SQLite database. - /// The options builder. - /// The SQLite options action. - public static ISerilogUiOptionsBuilder UseSqliteServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions) - { - var dbOptions = new RelationalDbOptions(string.Empty); - setupOptions(dbOptions); - dbOptions.Validate(); + var dbOptions = new SqliteDbOptions(string.Empty); + setupOptions(dbOptions); + dbOptions.Validate(); - optionsBuilder.Services.AddScoped(p => new SqliteDataProvider(dbOptions)); + optionsBuilder.Services.AddScoped(_ => new SqliteDataProvider(dbOptions, new SqliteQueryBuilder())); - return optionsBuilder; - } + return optionsBuilder; } -} +} \ No newline at end of file diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs new file mode 100644 index 00000000..58120eb8 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.SqliteDataProvider.Models; + +namespace Serilog.Ui.SqliteDataProvider.Extensions; + +public class SqliteDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) +{ + public SinkColumnNames ColumnNames { get; } = new SqliteSinkColumnNames(); +} diff --git a/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs b/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs new file mode 100644 index 00000000..46d68822 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.SqliteDataProvider.Models; + +internal class SqliteSinkColumnNames : SinkColumnNames +{ + public SqliteSinkColumnNames() + { + Exception = "Exception"; + Level = "Level"; + LogEventSerialized = "Properties"; + Message = "RenderedMessage"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs index 216169a1..1117960b 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -3,116 +3,66 @@ using Microsoft.Data.Sqlite; using Serilog.Ui.Core; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; -using System; +using Serilog.Ui.SqliteDataProvider.Extensions; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using static Serilog.Ui.Core.Models.SearchOptions; -namespace Serilog.Ui.SqliteDataProvider +namespace Serilog.Ui.SqliteDataProvider; + +public class SqliteDataProvider(SqliteDbOptions options, SqliteQueryBuilder queryBuilder) : IDataProvider { - public class SqliteDataProvider(RelationalDbOptions options) : IDataProvider - { - internal const string SqliteProviderName = "SQLite"; - private readonly RelationalDbOptions _options = Guard.Against.Null(options); + internal const string SqliteProviderName = "SQLite"; + private readonly SqliteDbOptions _options = Guard.Against.Null(options); - public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) - { - var logsTask = GetLogsAsync(queryParams); - var logCountTask = CountLogsAsync(queryParams); + public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) + { + var logsTask = GetLogsAsync(queryParams); + var logCountTask = CountLogsAsync(queryParams); - await Task.WhenAll(logsTask, logCountTask); + await Task.WhenAll(logsTask, logCountTask); - return (await logsTask, await logCountTask); - } + return (await logsTask, await logCountTask); + } - public string Name => _options.GetProviderName(SqliteProviderName); + public string Name => _options.GetProviderName(SqliteProviderName); - private async Task> GetLogsAsync(FetchLogsQuery queryParams) - { - var queryBuilder = new StringBuilder(); - queryBuilder.Append("SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties "); - queryBuilder.Append($"FROM {_options.TableName} "); + private async Task> GetLogsAsync(FetchLogsQuery queryParams) + { + var query = queryBuilder.BuildFetchLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); - GenerateWhereClause(queryBuilder, queryParams); + var rowNoStart = queryParams.Page * queryParams.Count; - GenerateSortClause(queryBuilder, queryParams.SortOn, queryParams.SortBy); + using var connection = new SqliteConnection(_options.ConnectionString); + var queryParameters = new + { + Offset = rowNoStart, + queryParams.Count, + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + queryParams.StartDate, + queryParams.EndDate + }; + var logs = await connection.QueryAsync(query.ToString(), queryParameters); + + return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); + } - queryBuilder.Append("LIMIT @Offset, @Count"); + private Task CountLogsAsync(FetchLogsQuery queryParams) + { + var query = queryBuilder.BuildCountLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); - var rowNoStart = queryParams.Page * queryParams.Count; + using var connection = new SqliteConnection(_options.ConnectionString); - using var connection = new SqliteConnection(_options.ConnectionString); - var queryParameters = new + return connection.QueryFirstOrDefaultAsync( + query.ToString(), + new { - Offset = rowNoStart, - queryParams.Count, queryParams.Level, Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, queryParams.StartDate, queryParams.EndDate - }; - var logs = await connection.QueryAsync(queryBuilder.ToString(), queryParameters); - - return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); - } - - private Task CountLogsAsync(FetchLogsQuery queryParams) - { - var queryBuilder = new StringBuilder(); - queryBuilder.Append($"SELECT COUNT(Id) FROM {_options.TableName} "); - - GenerateWhereClause(queryBuilder, queryParams); - - using var connection = new SqliteConnection(_options.ConnectionString); - - return connection.QueryFirstOrDefaultAsync( - queryBuilder.ToString(), - new - { - queryParams.Level, - Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, - queryParams.StartDate, - queryParams.EndDate - }); - } - - private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams) - { - var conditionStart = "WHERE"; - - if (!string.IsNullOrWhiteSpace(queryParams.Level)) - { - queryBuilder.Append($"{conditionStart} Level = @Level "); - conditionStart = "AND"; - } - - if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria)) - { - // TODO Exception as RemovableColumn? - queryBuilder.Append($"{conditionStart} (RenderedMessage LIKE @Search OR Exception LIKE @Search) "); - conditionStart = "AND"; - } - - if (queryParams.StartDate != null) - { - queryBuilder.Append($"{conditionStart} Timestamp >= @StartDate "); - conditionStart = "AND"; - } - - if (queryParams.EndDate != null) - { - queryBuilder.Append($"{conditionStart} Timestamp <= @EndDate "); - } - } - - private void GenerateSortClause(StringBuilder queryBuilder, SortProperty sortOn, SortDirection sortBy) - { - // TODO - queryBuilder.Append("ORDER BY Id DESC "); - } + }); } } diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs new file mode 100644 index 00000000..9219dd08 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs @@ -0,0 +1,84 @@ +using System; +using System.Text; +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.SqliteDataProvider; + +/// +/// Provides methods to build SQL queries specifically for Sqlite to fetch and count logs. +/// +/// The type of the log model. +public class SqliteQueryBuilder : SqlQueryBuilder +{ + /// + public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + GenerateSelectClause(queryStr, columns, schema, tableName); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Offset, @Count"); + + return queryStr.ToString(); + } + + /// + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append($"SELECT COUNT(Id) FROM {tableName}"); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + return queryStr.ToString(); + } + + protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) + => $"ORDER BY [{GetSortColumnName(columns, sortOn)}] {sortBy.ToString().ToUpper()}"; + + /// + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + { + queryBuilder.Append($"SELECT Id, {columns.Message} AS Message, {columns.Level}, {columns.Timestamp}, {columns.Exception}, {columns.Exception} "); + queryBuilder.Append($"FROM {tableName} "); + } + + /// + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + var conditionStart = "WHERE"; + + if (!string.IsNullOrWhiteSpace(level)) + { + queryBuilder.Append($"{conditionStart} {columns.Level} = @Level "); + conditionStart = "AND"; + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + queryBuilder.Append($"{conditionStart} ({columns.Message} LIKE @Search OR {columns.Exception} LIKE @Search) "); + conditionStart = "AND"; + } + + if (startDate != null) + { + queryBuilder.Append($"{conditionStart} {columns.Timestamp} >= @StartDate "); + conditionStart = "AND"; + } + + if (endDate != null) + { + queryBuilder.Append($"{conditionStart} {columns.Timestamp} <= @EndDate "); + } + } +} \ No newline at end of file From c3ccc648604f25031ba951d44d2de99868f1bd13 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Fri, 4 Oct 2024 09:24:49 +0200 Subject: [PATCH 05/14] fix tests --- .../Serilog.Ui.SqliteDataProvider.csproj | 1 + .../DataProvider/DataProviderBaseTest.cs | 15 ++++- .../DataProviderPaginationTest.cs | 36 ++++------- .../DataProvider/DataProviderSearchTest.cs | 64 ++----------------- .../Serilog.Ui.SqliteProvider.Tests.csproj | 8 ++- .../Util/SqliteTestProvider.cs | 58 +++++++++++------ 6 files changed, 76 insertions(+), 106 deletions(-) diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj index b4f6fa19..a9e629ff 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -17,6 +17,7 @@ + diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs index 4f4692ae..0d38bb4b 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -1,6 +1,10 @@ using FluentAssertions; +using Microsoft.Extensions.Primitives; using Serilog.Ui.Common.Tests.TestSuites; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.Core.Models; using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Extensions; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -16,7 +20,7 @@ public void It_throws_when_any_dependency_is_null() { var suts = new List> { - () => new SqliteDataProvider(null), + () => new SqliteDataProvider(null!, new SqliteQueryBuilder()), }; suts.ForEach(sut => sut.Should().ThrowExactly()); @@ -25,9 +29,14 @@ public void It_throws_when_any_dependency_is_null() [Fact] public Task It_logs_and_throws_when_db_read_breaks_down() { - var sut = new SqliteDataProvider(new() { ConnectionString = "connString", Schema = "dbo", TableName = "logs" }); + var sut = new SqliteDataProvider( + new SqliteDbOptions(string.Empty).WithConnectionString("connString").WithTable("Logs"), + new SqliteQueryBuilder() + ); - var assert = () => sut.FetchDataAsync(1, 10); + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; + + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); return assert.Should().ThrowExactlyAsync(); } } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs index d7c3a585..c3cebc59 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs @@ -1,32 +1,24 @@ using FluentAssertions; using Microsoft.Data.Sqlite; -using MySql.Tests.Util; +using Microsoft.Extensions.Primitives; using Serilog.Ui.Common.Tests.TestSuites.Impl; -using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.Core.Models; +using Sqlite.Tests.Util; +using System.Collections.Generic; using System.Threading.Tasks; using Xunit; -namespace Sqlite.Tests.DataProvider +namespace Sqlite.Tests.DataProvider; + +[Collection(nameof(SqliteTestProvider))] +[Trait("Integration-Pagination", "Sqlite")] +public class DataProviderPaginationTest(SqliteTestProvider instance) : IntegrationPaginationTests(instance) { - [Collection(nameof(SqliteDataProvider))] - [Trait("Integration-Pagination", "Sqlite")] - public class DataProviderPaginationTest : IntegrationPaginationTests + [Fact] + public override Task It_throws_when_skip_is_zero() { - public DataProviderPaginationTest(SqliteTestProvider instance) : base(instance) - { - } - - public override Task It_fetches_with_limit() => base.It_fetches_with_limit(); - - public override Task It_fetches_with_limit_and_skip() => base.It_fetches_with_limit_and_skip(); - - public override Task It_fetches_with_skip() => base.It_fetches_with_skip(); - - [Fact] - public override Task It_throws_when_skip_is_zero() - { - var test = () => provider.FetchDataAsync(0, 1); - return test.Should().ThrowAsync(); - } + var query = new Dictionary { ["page"] = "0", ["count"] = "1" }; + var test = () => Provider.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + return test.Should().ThrowAsync(); } } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs index e1fa1e69..e444264b 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs @@ -1,62 +1,10 @@ -using MsSql.Tests.DataProvider; // TODO nope -using MySql.Tests.Util; +using Serilog.Ui.Common.Tests.TestSuites.Impl; using Serilog.Ui.SqliteDataProvider; -using System.Threading.Tasks; +using Sqlite.Tests.Util; using Xunit; -namespace Sqlite.Tests.DataProvider -{ - [Collection(nameof(SqliteDataProvider))] - [Trait("Integration-Search", "Sqlite")] - public class DataProviderSearchTest : IntegrationSearchTests - { +namespace Sqlite.Tests.DataProvider; - // https://github.com/saleem-mirza/serilog-sinks-sqlite/blob/dev/src/Serilog.Sinks.SQLite/Sinks/SQLite/SQLiteSink.cs - public const string SqliteCreateTable = "CREATE TABLE IF NOT EXISTS Logs (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT," + - "Timestamp TEXT," + - "LogLevel VARCHAR(10)," + - "Exception TEXT," + - "Message TEXT," + - "Properties TEXT," + - "_ts TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now'))" + // SQLite equivalent for CURRENT_TIMESTAMP - ")"; - - public const string SqliteInsertFakeData = "INSERT INTO Logs" + - "(Timestamp, LogLevel, Exception, Message, Properties)" + - "VALUES (" + - $"@{nameof(LogModel.Timestamp)}," + - $"@{nameof(LogModel.Level)}," + - $"@{nameof(LogModel.Exception)}," + - $"@{nameof(LogModel.Message)}," + - $"@{nameof(LogModel.Properties)}" + - ")"; - public DataProviderSearchTest(SqliteTestProvider instance) : base(instance) - { - } - - public override Task It_finds_all_data_with_default_search() - => base.It_finds_all_data_with_default_search(); - - public override Task It_finds_data_with_all_filters() - => base.It_finds_data_with_all_filters(); - - public override Task It_finds_only_data_emitted_after_date() - => base.It_finds_only_data_emitted_after_date(); - - public override Task It_finds_only_data_emitted_before_date() - => base.It_finds_only_data_emitted_before_date(); - - public override Task It_finds_only_data_emitted_in_dates_range() - => base.It_finds_only_data_emitted_in_dates_range(); - - public override Task It_finds_only_data_with_specific_level() - => base.It_finds_only_data_with_specific_level(); - - public override Task It_finds_only_data_with_specific_message_content() - => base.It_finds_only_data_with_specific_message_content(); - - public override Task It_finds_same_data_on_same_repeated_search() - => base.It_finds_same_data_on_same_repeated_search(); - } -} \ No newline at end of file +[Collection(nameof(SqliteDataProvider))] +[Trait("Integration-Search", "Sqlite")] +public class DataProviderSearchTest(SqliteTestProvider instance) : IntegrationSearchTests(instance); diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj index d505c210..6f0e993a 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj @@ -1,13 +1,15 @@  - net7.0 - enable Sqlite.Tests Sqlite.Tests - false + + + + + %(Filename)%(Extension) diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs index b12347ce..1ff7d491 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -1,58 +1,76 @@ using Ardalis.GuardClauses; using Dapper; using Microsoft.Data.Sqlite; +using Serilog; using Serilog.Ui.Common.Tests.DataSamples; using Serilog.Ui.Common.Tests.SqlUtil; +using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core; +using Serilog.Ui.Core.Extensions; using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Extensions; using System.Threading.Tasks; using Xunit; -namespace MySql.Tests.Util +namespace Sqlite.Tests.Util { - [CollectionDefinition(nameof(SqliteDataProvider))] - public class MySqlCollection : ICollectionFixture { } + [CollectionDefinition(nameof(SqliteTestProvider))] + public class SqliteCollection : ICollectionFixture { } - public sealed class SqliteTestProvider : DatabaseInstance + public sealed class SqliteTestProvider : IIntegrationRunner { - protected override string Name => "SqliteInMemory"; + private LogModelPropsCollector? _collector; + + private SqliteDataProvider? _provider; public SqliteTestProvider() : base() { // No need to set up a container for SQLite - using in-memory database } - public RelationalDbOptions DbOptions { get; set; } = new() - { - TableName = "Logs", - Schema = "dbo" - }; + public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions(string.Empty).WithTable("Logs"); - protected override async Task CheckDbReadinessAsync() + private async Task CheckDbReadinessAsync() { Guard.Against.Null(DbOptions); using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); - DbOptions.ConnectionString = connection.ConnectionString; + DbOptions.WithConnectionString(connection.ConnectionString); await connection.ExecuteAsync("SELECT 1"); } - protected override async Task InitializeAdditionalAsync() + private void InitializeAdditional() { - var logs = LogModelFaker.Logs(100); - Collector = new LogModelPropsCollector(logs); + var serilog = new SerilogSinkSetup(logger => + logger + .WriteTo + .SQLite(DbOptions.ConnectionString)); + _collector = serilog.InitializeLogs(); - using var connection = new SqliteConnection(DbOptions.ConnectionString); - await connection.OpenAsync(); + _provider = new SqliteDataProvider(DbOptions, new SqliteQueryBuilder()); + } + + public IDataProvider GetDataProvider() => _provider!; + + public LogModelPropsCollector GetPropsCollector() => _collector!; - await connection.ExecuteAsync(Costants.SqliteCreateTable); + public async Task InitializeAsync() + { + await CheckDbReadinessAsync(); + InitializeAdditional(); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } - await connection.ExecuteAsync(Costants.SqliteInsertFakeData, logs); + public void Dispose() + { - Provider = new SqliteDataProvider(DbOptions); // Update this if needed for SQLite } } } From d2e4d58c77c47477ba11dbb169ba25e6c6fd10c8 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Fri, 4 Oct 2024 09:45:20 +0200 Subject: [PATCH 06/14] chore: review test definition --- .../SerilogUiOptionBuilderExtensions.cs | 2 +- .../Extensions/SqliteDbOptions.cs | 2 +- .../DataProvider/DataProviderBaseTest.cs | 2 +- .../DataProvider/DataProviderSearchTest.cs | 2 +- .../SerilogUiOptionBuilderExtensionsTest.cs | 59 +++++++++++++++---- .../Serilog.Ui.SqliteProvider.Tests.csproj | 13 +++- .../Util/SqliteTestProvider.cs | 2 +- 7 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 43b8b9ac..353dd552 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -18,7 +18,7 @@ public static ISerilogUiOptionsBuilder UseSqliteServer( this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) { - var dbOptions = new SqliteDbOptions(string.Empty); + var dbOptions = new SqliteDbOptions(); setupOptions(dbOptions); dbOptions.Validate(); diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs index 58120eb8..449a55b2 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs @@ -4,7 +4,7 @@ namespace Serilog.Ui.SqliteDataProvider.Extensions; -public class SqliteDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) +public class SqliteDbOptions() : RelationalDbOptions("ununsed") { public SinkColumnNames ColumnNames { get; } = new SqliteSinkColumnNames(); } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs index 0d38bb4b..a5239030 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -30,7 +30,7 @@ public void It_throws_when_any_dependency_is_null() public Task It_logs_and_throws_when_db_read_breaks_down() { var sut = new SqliteDataProvider( - new SqliteDbOptions(string.Empty).WithConnectionString("connString").WithTable("Logs"), + new SqliteDbOptions().WithConnectionString("connString").WithTable("Logs"), new SqliteQueryBuilder() ); diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs index e444264b..f0a709c8 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs @@ -5,6 +5,6 @@ namespace Sqlite.Tests.DataProvider; -[Collection(nameof(SqliteDataProvider))] +[Collection(nameof(SqliteTestProvider))] [Trait("Integration-Search", "Sqlite")] public class DataProviderSearchTest(SqliteTestProvider instance) : IntegrationSearchTests(instance); diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs index 290a4903..dba76668 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs @@ -1,13 +1,17 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.Core.Models.Options; using Serilog.Ui.SqliteDataProvider; -using Serilog.Ui.Web; +using Serilog.Ui.SqliteDataProvider.Extensions; +using Serilog.Ui.Web.Extensions; using System; using System.Collections.Generic; +using System.Linq; using Xunit; -namespace MySql.Tests.Extensions +namespace Sqlite.Tests.Extensions { [Trait("DI-DataProvider", "Sqlite")] public class SerilogUiOptionBuilderExtensionsTest @@ -22,9 +26,9 @@ public SerilogUiOptionBuilderExtensionsTest() [Fact] public void It_registers_provider_and_dependencies() { - serviceCollection.AddSerilogUi((builder) => + serviceCollection.AddSerilogUi(builder => { - builder.UseSqliteServer("https://mysqlserver.example.com", "my-table"); + builder.UseSqliteServer(opt => opt.WithConnectionString("https://sqliteserver.example.com").WithTable("my-table")); }); var services = serviceCollection.BuildServiceProvider(); @@ -35,17 +39,52 @@ public void It_registers_provider_and_dependencies() provider.Should().NotBeNull().And.BeOfType(); } + [Fact] + public void It_registers_multiple_providers() + { + serviceCollection.AddSerilogUi(builder => + { + builder.UseSqliteServer(opt => opt.WithConnectionString("https://sqliteserver.example.com").WithTable("my-table")); + builder.UseSqliteServer(opt => opt.WithConnectionString("https://sqliteserver2.example.com").WithTable("my-table2")); + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + + var providers = scope.ServiceProvider.GetServices().ToList(); + providers.Should().HaveCount(2).And.AllBeOfType(); + providers.Select(p => p.Name).Should().OnlyHaveUniqueItems(); + + var providersOptions = serviceProvider.GetRequiredService(); + providersOptions.DisabledSortProviderNames.Should().BeEmpty(); + providersOptions.ExceptionAsStringProviderNames.Should().BeEmpty(); + } + [Fact] public void It_throws_on_invalid_registration() { var nullables = new List> { - () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer(null, "name")), - () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer(" ", "name")), - () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("", "name")), - () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("name", null)), - () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("name", " ")), - () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer("name", "")), + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer(_ => {})), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString(null!).WithTable("my-table"))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString(" ").WithTable("my-table"))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString(string.Empty).WithTable("my-table"))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString("name").WithTable(null!))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString("name").WithTable(" "))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString("name").WithTable(string.Empty))), + // if user sets an invalid schema, default value will be overridden an validation should fail + () => serviceCollection.AddSerilogUi(builder => + builder.UseSqliteServer(opt => opt.WithConnectionString("conn").WithTable("ok").WithSchema(null!))), + () => serviceCollection.AddSerilogUi(builder => + builder.UseSqliteServer(opt => opt.WithConnectionString("conn").WithTable("ok").WithSchema(" "))), + () => serviceCollection.AddSerilogUi(builder => + builder.UseSqliteServer(opt => opt.WithConnectionString("conn").WithTable("ok").WithSchema(string.Empty))), }; foreach (var nullable in nullables) diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj index 6f0e993a..bf620eb6 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj @@ -7,7 +7,18 @@ - + + + + + + + + + + + + diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs index 1ff7d491..4a288ac3 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -28,7 +28,7 @@ public SqliteTestProvider() : base() // No need to set up a container for SQLite - using in-memory database } - public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions(string.Empty).WithTable("Logs"); + public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions().WithTable("Logs"); private async Task CheckDbReadinessAsync() { From 9df5ae36bdceeb7822f0db198b715256b1a14157 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Fri, 4 Oct 2024 19:19:26 +0200 Subject: [PATCH 07/14] fix base provider --- .../Serilog.Ui.SqliteDataProvider.csproj | 2 +- .../Serilog.Ui.SqliteProvider.Tests.csproj | 1 + .../Util/SqliteTestProvider.cs | 28 ++++++++----------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj index a9e629ff..fa900876 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj index bf620eb6..182a83e5 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs index 4a288ac3..1fa53ad6 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -3,7 +3,6 @@ using Microsoft.Data.Sqlite; using Serilog; using Serilog.Ui.Common.Tests.DataSamples; -using Serilog.Ui.Common.Tests.SqlUtil; using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core; using Serilog.Ui.Core.Extensions; @@ -28,18 +27,20 @@ public SqliteTestProvider() : base() // No need to set up a container for SQLite - using in-memory database } - public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions().WithTable("Logs"); + public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions() + .WithTable("Logs") + .WithConnectionString("Data Source=hello.db"); private async Task CheckDbReadinessAsync() { Guard.Against.Null(DbOptions); - using var connection = new SqliteConnection("DataSource=:memory:"); + using var connection = new SqliteConnection(DbOptions.ConnectionString); await connection.OpenAsync(); - DbOptions.WithConnectionString(connection.ConnectionString); - await connection.ExecuteAsync("SELECT 1"); + + InitializeAdditional(); } private void InitializeAdditional() @@ -47,7 +48,7 @@ private void InitializeAdditional() var serilog = new SerilogSinkSetup(logger => logger .WriteTo - .SQLite(DbOptions.ConnectionString)); + .SQLite(@"hello.db", batchSize: 1)); _collector = serilog.InitializeLogs(); _provider = new SqliteDataProvider(DbOptions, new SqliteQueryBuilder()); @@ -57,20 +58,13 @@ private void InitializeAdditional() public LogModelPropsCollector GetPropsCollector() => _collector!; - public async Task InitializeAsync() + public Task InitializeAsync() { - await CheckDbReadinessAsync(); - InitializeAdditional(); + return CheckDbReadinessAsync(); } - public Task DisposeAsync() - { - return Task.CompletedTask; - } - - public void Dispose() - { + public Task DisposeAsync() => Task.CompletedTask; - } + public void Dispose() { } } } From 21227351171df894aa104094c67722fc63fd13b1 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 10:46:10 +0200 Subject: [PATCH 08/14] fix: typo in provider --- src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs | 7 ++++++- src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs index 1117960b..8a864ac7 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -46,7 +46,12 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam }; var logs = await connection.QueryAsync(query.ToString(), queryParameters); - return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); + return logs.Select((item, i) => + { + item.PropertyType = "json"; + item.SetRowNo(rowNoStart, i); + return item; + }).ToList(); } private Task CountLogsAsync(FetchLogsQuery queryParams) diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs index 9219dd08..c352da93 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs @@ -43,7 +43,7 @@ protected override string GenerateSortClause(SinkColumnNames columns, SearchOpti /// private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) { - queryBuilder.Append($"SELECT Id, {columns.Message} AS Message, {columns.Level}, {columns.Timestamp}, {columns.Exception}, {columns.Exception} "); + queryBuilder.Append($"SELECT Id, {columns.Message} AS Message, {columns.Level}, {columns.Timestamp}, {columns.Exception}, {columns.LogEventSerialized} "); queryBuilder.Append($"FROM {tableName} "); } From 2ce8a7f179d7451c6643545e729096f8c56692f5 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 13:56:44 +0200 Subject: [PATCH 09/14] fix: typos in builder --- .../Extensions/SerilogUiOptionBuilderExtensions.cs | 2 ++ .../Serilog.Ui.SqliteDataProvider.csproj | 2 +- .../SqliteDataProvider.cs | 4 ++++ .../SqliteQueryBuilder.cs | 4 ++-- .../DataProvider/DataProviderPaginationTest.cs | 2 +- .../Util/SqliteTestProvider.cs | 14 +++++++++----- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 353dd552..906d551a 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -22,6 +22,8 @@ public static ISerilogUiOptionsBuilder UseSqliteServer( setupOptions(dbOptions); dbOptions.Validate(); + string providerName = dbOptions.GetProviderName(SqliteDataProvider.SqliteProviderName); + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); optionsBuilder.Services.AddScoped(_ => new SqliteDataProvider(dbOptions, new SqliteQueryBuilder())); return optionsBuilder; diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj index fa900876..87a1dcc2 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -4,7 +4,7 @@ Serilog.UI.Sqlite netstandard2.0 latest - 1.0.0-beta.1 + 1.0.0 SQLite data provider for Serilog UI. serilog serilog-ui serilog.sinks.sqlite sqlite diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs index 8a864ac7..8b0f84dc 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -4,6 +4,7 @@ using Serilog.Ui.Core; using Serilog.Ui.Core.Models; using Serilog.Ui.SqliteDataProvider.Extensions; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -49,6 +50,9 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam return logs.Select((item, i) => { item.PropertyType = "json"; + // both sinks save UTC but MariaDb is queried as Unspecified, MySql is queried as Local + var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Local : item.Timestamp.Kind); + item.Timestamp = ts.ToUniversalTime(); item.SetRowNo(rowNoStart, i); return item; }).ToList(); diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs index c352da93..54f7bf2c 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs @@ -30,7 +30,7 @@ public override string BuildCountLogsQuery(SinkColumnNames columns, string schem { StringBuilder queryStr = new(); - queryStr.Append($"SELECT COUNT(Id) FROM {tableName}"); + queryStr.Append($"SELECT COUNT(Id) FROM {tableName} "); GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); @@ -38,7 +38,7 @@ public override string BuildCountLogsQuery(SinkColumnNames columns, string schem } protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) - => $"ORDER BY [{GetSortColumnName(columns, sortOn)}] {sortBy.ToString().ToUpper()}"; + => $"ORDER BY {GetSortColumnName(columns, sortOn)} {sortBy.ToString().ToUpper()}"; /// private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs index c3cebc59..ecfba61d 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs @@ -19,6 +19,6 @@ public override Task It_throws_when_skip_is_zero() { var query = new Dictionary { ["page"] = "0", ["count"] = "1" }; var test = () => Provider.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - return test.Should().ThrowAsync(); + return test.Should().NotThrowAsync("because Sqlite catches the error"); } } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs index 1fa53ad6..d5310416 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -8,6 +8,7 @@ using Serilog.Ui.Core.Extensions; using Serilog.Ui.SqliteDataProvider; using Serilog.Ui.SqliteDataProvider.Extensions; +using System; using System.Threading.Tasks; using Xunit; @@ -18,6 +19,9 @@ public class SqliteCollection : ICollectionFixture { } public sealed class SqliteTestProvider : IIntegrationRunner { + private static string DbName() => $"integration-{DateTime.UtcNow:O}.db".Replace(':', '-'); + private string _dbInstanceName = string.Empty; + private LogModelPropsCollector? _collector; private SqliteDataProvider? _provider; @@ -27,14 +31,14 @@ public SqliteTestProvider() : base() // No need to set up a container for SQLite - using in-memory database } - public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions() - .WithTable("Logs") - .WithConnectionString("Data Source=hello.db"); - + public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions().WithTable("Logs"); private async Task CheckDbReadinessAsync() { Guard.Against.Null(DbOptions); + _dbInstanceName = DbName(); + DbOptions.WithConnectionString($"Data Source={_dbInstanceName}"); + using var connection = new SqliteConnection(DbOptions.ConnectionString); await connection.OpenAsync(); @@ -48,7 +52,7 @@ private void InitializeAdditional() var serilog = new SerilogSinkSetup(logger => logger .WriteTo - .SQLite(@"hello.db", batchSize: 1)); + .SQLite(_dbInstanceName, batchSize: 1, storeTimestampInUtc: true)); _collector = serilog.InitializeLogs(); _provider = new SqliteDataProvider(DbOptions, new SqliteQueryBuilder()); From 06defee2e030768b8453af48153335a55eb8fc4d Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 14:07:50 +0200 Subject: [PATCH 10/14] fix dataprovider DI check --- .../Extensions/SerilogUiOptionBuilderExtensionsTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs index dba76668..3284bfc3 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs @@ -57,7 +57,7 @@ public void It_registers_multiple_providers() var providersOptions = serviceProvider.GetRequiredService(); providersOptions.DisabledSortProviderNames.Should().BeEmpty(); - providersOptions.ExceptionAsStringProviderNames.Should().BeEmpty(); + providersOptions.ExceptionAsStringProviderNames.Should().HaveCount(2); } [Fact] @@ -89,7 +89,7 @@ public void It_throws_on_invalid_registration() foreach (var nullable in nullables) { - nullable.Should().ThrowExactly(); + nullable.Should().Throw(); } } } From 2da68e2cb91bb2fa3b437bf59ed29ded861a63bf Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 20:11:37 +0200 Subject: [PATCH 11/14] fix (sqllite-timestamp): manually parse to string dates queries --- .../Models/SqliteSinkColumnNames.cs | 2 +- .../SqliteDataProvider.cs | 17 +++++++++++------ .../Util/SqliteTestProvider.cs | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs b/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs index 46d68822..a86e515e 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs +++ b/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs @@ -11,6 +11,6 @@ public SqliteSinkColumnNames() LogEventSerialized = "Properties"; Message = "RenderedMessage"; MessageTemplate = ""; - Timestamp = "TimeStamp"; + Timestamp = "Timestamp"; } } \ No newline at end of file diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs index 8b0f84dc..c9bc3584 100644 --- a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -19,6 +19,8 @@ public class SqliteDataProvider(SqliteDbOptions options, SqliteQueryBuilder quer public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) { + queryParams.ToUtcDates(); // assuming data is saved in UTC, due to UTC predictability + var logsTask = GetLogsAsync(queryParams); var logCountTask = CountLogsAsync(queryParams); @@ -42,17 +44,18 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam queryParams.Count, queryParams.Level, Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, - queryParams.StartDate, - queryParams.EndDate + StartDate = StringifyDate(queryParams.StartDate), + EndDate = StringifyDate(queryParams.EndDate) }; var logs = await connection.QueryAsync(query.ToString(), queryParameters); return logs.Select((item, i) => { item.PropertyType = "json"; - // both sinks save UTC but MariaDb is queried as Unspecified, MySql is queried as Local - var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Local : item.Timestamp.Kind); + + var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind); item.Timestamp = ts.ToUniversalTime(); + item.SetRowNo(rowNoStart, i); return item; }).ToList(); @@ -70,8 +73,10 @@ private Task CountLogsAsync(FetchLogsQuery queryParams) { queryParams.Level, Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, - queryParams.StartDate, - queryParams.EndDate + StartDate = StringifyDate(queryParams.StartDate), + EndDate = StringifyDate(queryParams.EndDate) }); } + + private static string StringifyDate(DateTime? date) => date.HasValue ? date.Value.ToString("s") + ".999" : "null"; } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs index d5310416..7100e46c 100644 --- a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -52,7 +52,7 @@ private void InitializeAdditional() var serilog = new SerilogSinkSetup(logger => logger .WriteTo - .SQLite(_dbInstanceName, batchSize: 1, storeTimestampInUtc: true)); + .SQLite(_dbInstanceName, storeTimestampInUtc: true)); _collector = serilog.InitializeLogs(); _provider = new SqliteDataProvider(DbOptions, new SqliteQueryBuilder()); From 2af27b161e9da744188ddc94bcb2bf5a0386b01f Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 20:26:19 +0200 Subject: [PATCH 12/14] tests: sqlite query builder --- .../DataProvider/QueryBuilderTests.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs new file mode 100644 index 00000000..a2866fe4 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Serilog.Ui.Core.Models; +using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Models; +using Xunit; + +namespace Sqlite.Tests.DataProvider; + +[Trait("Unit-QueryBuilder", "Sqlite")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForSink_ReturnsCorrectQuery( + string schema, + string tableName, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate, + string expectedQuery) + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = level, + ["search"] = searchCriteria, + ["startDate"] = startDate?.ToString("O"), + ["endDate"] = endDate?.ToString("O") + }; + + SqliteSinkColumnNames sinkColumns = new(); + SqliteQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + string.Empty, "Logs", null!, null!, null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", null!, null!, null!, DateTime.Now, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Timestamp <= @EndDate ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Timestamp >= @StartDate AND Timestamp <= @EndDate ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", "Information", null!, null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Level = @Level ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", null!, "Test", null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE (RenderedMessage LIKE @Search OR Exception LIKE @Search) ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", "Information", "Test", null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Level = @Level AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", "Information", "Test", DateTime.UtcNow, DateTime.UtcNow, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Level = @Level AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) AND Timestamp >= @StartDate AND Timestamp <= @EndDate ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file From 2727b6f905b0f9ee712508f7cc977abb7ec03f8e Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 20:34:08 +0200 Subject: [PATCH 13/14] update readmes --- README.md | 4 +++- README_Nuget.md | 4 +++- .../Serilog.Ui.SqliteDataProvider.csproj | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fdf1eff1..61776209 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ A simple Serilog log viewer for the following sinks: - Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver)) -- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) +- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) - Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql)) and Serilog.Sinks.**Postgresql.Alternative** ([Nuget](https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative)) - Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb)) - Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch)) - Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb)) +- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/)) @@ -43,6 +44,7 @@ Install one or more of the available providers, based upon your sink(s): | **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` | | **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` | | **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` | +| **Serilog.UI.SQLiteProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.SQLiteProvider)] | `dotnet add package Serilog.UI.SQLiteProvider` | `Install-Package Serilog.UI.SQLiteProvider` | ### DI registration diff --git a/README_Nuget.md b/README_Nuget.md index a3f29621..cae30e94 100644 --- a/README_Nuget.md +++ b/README_Nuget.md @@ -3,11 +3,12 @@ A simple Serilog log viewer for the following sinks: - Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver)) -- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) +- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) - Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql)) and Serilog.Sinks.**Postgresql.Alternative** ([Nuget](https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative)) - Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb)) - Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch)) - Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb)) +- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/)) # Read the [Wiki](https://github.com/serilog-contrib/serilog-ui/wiki) @@ -35,6 +36,7 @@ Install one or more of the available providers, based upon your sink(s): | **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` | | **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` | | **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` | +| **Serilog.UI.SQLiteProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.SQLiteProvider)] | `dotnet add package Serilog.UI.SQLiteProvider` | `Install-Package Serilog.UI.SQLiteProvider` | ### DI registration diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj index 87a1dcc2..ecb195e8 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -1,7 +1,7 @@  - Serilog.UI.Sqlite + Serilog.UI.SqliteProvider netstandard2.0 latest 1.0.0 From a1958a2609eaab247a9c9de1e6a25ebab6cf6470 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sat, 5 Oct 2024 20:35:38 +0200 Subject: [PATCH 14/14] add authors field to package --- .../Serilog.Ui.SqliteDataProvider.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj index ecb195e8..f9302ea2 100644 --- a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -6,6 +6,7 @@ latest 1.0.0 + Tech Garage (team) SQLite data provider for Serilog UI. serilog serilog-ui serilog.sinks.sqlite sqlite