From 2782f32b49b49c7f2a4b54c27ff36265bf4d4aea Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 22 Sep 2024 09:54:16 +0200 Subject: [PATCH 01/13] Add SQL query builder base class. --- .../QueryBuilder/Sql/SinkColumnNames.cs | 37 +++++++++++++ .../QueryBuilder/Sql/SqlQueryBuilder.cs | 52 +++++++++++++++++++ src/Serilog.Ui.Core/Serilog.Ui.Core.csproj | 2 +- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs create mode 100644 src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs new file mode 100644 index 00000000..786c1d1a --- /dev/null +++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs @@ -0,0 +1,37 @@ +namespace Serilog.Ui.Core.QueryBuilder.Sql; + +/// +/// Represents the column names used in the SQL-based sink for logging. +/// +public abstract class SinkColumnNames +{ + /// + /// Gets or sets the message of the log entry. + /// + public string Message { get; set; } = string.Empty; + + /// + /// Gets or sets the message template of the log entry. + /// + public string MessageTemplate { get; set; } = string.Empty; + + /// + /// Gets or sets the level of the log entry. + /// + public string Level { get; set; } = string.Empty; + + /// + /// Gets or sets the timestamp of the log entry. + /// + public string Timestamp { get; set; } = string.Empty; + + /// + /// Gets or sets the exception of the log entry. + /// + public string Exception { get; set; } = string.Empty; + + /// + /// Gets or sets the serialized log event like properties. + /// + public string LogEventSerialized { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs new file mode 100644 index 00000000..6603fc92 --- /dev/null +++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs @@ -0,0 +1,52 @@ +using Serilog.Ui.Core.Models; +using static Serilog.Ui.Core.Models.SearchOptions; + +namespace Serilog.Ui.Core.QueryBuilder.Sql; + +/// +/// Abstract class that provides methods to build SQL queries for fetching and counting logs. +/// +public abstract class SqlQueryBuilder +{ + /// + /// Builds a SQL query to fetch logs from the specified table. + /// + /// The type of the log model. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for fetching logs. + /// A SQL query string to fetch logs. + public abstract string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) where T : LogModel; + + /// + /// Builds a SQL query to count logs in the specified table. + /// + /// The type of the log model. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public abstract string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) where T : LogModel; + + /// + /// Generates a SQL sort clause based on the specified sort property and direction. + /// + /// The column names used in the sink for logging. + /// The property to sort on. + /// The direction to sort by. + /// A SQL sort clause string. + protected static string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy) + { + var sortPropertyName = sortOn switch + { + SortProperty.Timestamp => columns.Timestamp, + SortProperty.Level => columns.Level, + SortProperty.Message => columns.Message, + _ => columns.Timestamp + }; + + return $"\"{sortPropertyName}\" {sortBy.ToString().ToUpper()}"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj b/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj index ec5eee72..0740033d 100644 --- a/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj +++ b/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj @@ -9,6 +9,6 @@ - + \ No newline at end of file From 88837cfbe201b40be4b6e2bed5e3a50ecae600cf Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 22 Sep 2024 10:15:03 +0200 Subject: [PATCH 02/13] Refactor postgres query builder. --- Serilog.Ui.sln.DotSettings | 3 +- .../Extensions/PostgreSqlDbOptions.cs | 6 +- .../SerilogUiOptionBuilderExtensions.cs | 8 +- .../Models/PostgreLogModel.cs | 10 +- .../PostgreSqlAlternativeSinkColumnNames.cs | 6 +- .../Models/PostgreSqlSinkColumnNames.cs | 12 +- .../Models/SinkColumnNames.cs | 16 -- .../PostgreDataProvider.cs | 22 +-- .../PostgresQueryBuilder.cs | 155 ++++++++++++++++++ .../QueryBuilder.cs | 111 ------------- .../Serilog.Ui.PostgreSqlProvider.csproj | 2 - src/Serilog.Ui.Web/Serilog.Ui.Web.csproj | 8 +- .../DataProvider/DataProviderBaseTest.cs | 68 ++++---- .../DataProvider/QueryBuilderTests.cs | 10 +- .../Util/PostgresTestProvider.cs | 5 +- 15 files changed, 245 insertions(+), 197 deletions(-) delete mode 100644 src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs create mode 100644 src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs delete mode 100644 src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs diff --git a/Serilog.Ui.sln.DotSettings b/Serilog.Ui.sln.DotSettings index 4d35b8be..e62a5d60 100644 --- a/Serilog.Ui.sln.DotSettings +++ b/Serilog.Ui.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs index 06cea7c4..c53687bd 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs @@ -1,4 +1,5 @@ using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; using Serilog.Ui.PostgreSqlProvider.Models; namespace Serilog.Ui.PostgreSqlProvider.Extensions; @@ -9,10 +10,9 @@ public class PostgreSqlDbOptions : RelationalDbOptions /// public PostgreSqlDbOptions(string defaultSchemaName) : base(defaultSchemaName) { + ColumnNames = new PostgreSqlAlternativeSinkColumnNames(); } - internal SinkColumnNames ColumnNames = new PostgreSqlAlternativeSinkColumnNames(); - /// /// It gets or sets SinkType. /// The sink that used to store logs in the PostgreSQL database. This data provider supports @@ -33,4 +33,6 @@ public PostgreSqlDbOptions WithSinkType(PostgreSqlSinkType sinkType) : new PostgreSqlSinkColumnNames(); return this; } + + internal SinkColumnNames ColumnNames { get; set; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 9f217bcb..302412f1 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,8 +1,8 @@ -using System; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.PostgreSqlProvider.Models; +using System; namespace Serilog.Ui.PostgreSqlProvider.Extensions { @@ -44,12 +44,12 @@ Action setupOptions if (customModel) { optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions)); + optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); return optionsBuilder; } - optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions)); + optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); return optionsBuilder; } } diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs index d0149d7f..546204d3 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs @@ -17,23 +17,23 @@ public class PostgresLogModel : LogModel private string _level = string.Empty; /// - public sealed override int RowNo => base.RowNo; + public override sealed int RowNo => base.RowNo; /// - public sealed override string? Message { get; set; } + public override sealed string? Message { get; set; } /// - public sealed override DateTime Timestamp { get; set; } + public override sealed DateTime Timestamp { get; set; } /// - public sealed override string? Level + public override sealed string? Level { get => _level; set => _level = LogLevelConverter.GetLevelName(value); } /// - /// It get or sets LogEventSerialized. + /// It gets or sets LogEventSerialized. /// [JsonIgnore] public string LogEvent { get; set; } = string.Empty; diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs index 8f72b6e4..b1ecae3c 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs @@ -1,4 +1,6 @@ -namespace Serilog.Ui.PostgreSqlProvider.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.PostgreSqlProvider.Models; internal class PostgreSqlAlternativeSinkColumnNames : SinkColumnNames { @@ -7,8 +9,8 @@ public PostgreSqlAlternativeSinkColumnNames() Exception = "Exception"; Level = "Level"; LogEventSerialized = "LogEvent"; + Message = "Message"; MessageTemplate = "MessageTemplate"; - RenderedMessage = "Message"; Timestamp = "Timestamp"; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs index a2d56427..01153b9d 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs @@ -1,14 +1,16 @@ -namespace Serilog.Ui.PostgreSqlProvider.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.PostgreSqlProvider.Models; internal class PostgreSqlSinkColumnNames : SinkColumnNames { public PostgreSqlSinkColumnNames() { - RenderedMessage = "message"; - MessageTemplate = "message_template"; - Level = "level"; - Timestamp = "timestamp"; Exception = "exception"; + Level = "level"; LogEventSerialized = "log_event"; + Message = "message"; + MessageTemplate = "message_template"; + Timestamp = "timestamp"; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs deleted file mode 100644 index 296c5d29..00000000 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Serilog.Ui.PostgreSqlProvider.Models; - -internal abstract class SinkColumnNames -{ - public string RenderedMessage { get; set; } = string.Empty; - - public string MessageTemplate { get; set; } = string.Empty; - - public string Level { get; set; } = string.Empty; - - public string Timestamp { get; set; } = string.Empty; - - public string Exception { get; set; } = string.Empty; - - public string LogEventSerialized { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs index fc76c115..108052ca 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs @@ -1,26 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; +using Dapper; using Npgsql; using Serilog.Ui.Core; using Serilog.Ui.Core.Models; using Serilog.Ui.PostgreSqlProvider.Extensions; using Serilog.Ui.PostgreSqlProvider.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Serilog.Ui.PostgreSqlProvider; /// -public class PostgresDataProvider(PostgreSqlDbOptions options) : PostgresDataProvider(options); +public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) + : PostgresDataProvider(options, queryBuilder); /// -public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvider +public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) : IDataProvider where T : PostgresLogModel { internal const string ProviderName = "NPGSQL"; - private readonly PostgreSqlDbOptions _options = options ?? throw new ArgumentNullException(nameof(options)); /// @@ -40,7 +40,7 @@ public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvide private async Task> GetLogsAsync(FetchLogsQuery queryParams) { - var query = options.ColumnNames.BuildFetchLogsQuery(_options.Schema, _options.TableName, queryParams); + var query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, _options.Schema, _options.TableName, queryParams); var rowNoStart = queryParams.Page * queryParams.Count; await using var connection = new NpgsqlConnection(_options.ConnectionString); @@ -68,7 +68,7 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam private async Task CountLogsAsync(FetchLogsQuery queryParams) { - var query = options.ColumnNames.BuildCountLogsQuery(_options.Schema, _options.TableName, queryParams); + var query = queryBuilder.BuildCountLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); await using var connection = new NpgsqlConnection(_options.ConnectionString); diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs new file mode 100644 index 00000000..39f5efa3 --- /dev/null +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs @@ -0,0 +1,155 @@ +using Serilog.Ui.Core.Attributes; +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.PostgreSqlProvider.Models; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Serilog.Ui.PostgreSqlProvider; + +/// +/// Provides methods to build SQL queries specifically for PostgreSQL to fetch and count logs. +/// +public class PostgresQueryBuilder : SqlQueryBuilder +{ + /// + /// Builds a SQL query to fetch logs from the specified PostgreSQL table. + /// + /// The type of the log model. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for fetching logs. + /// A SQL query string to fetch logs. + 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); + + string sortClause = GenerateSortClause(columns, query.SortOn, query.SortBy); + + queryStr.Append($" ORDER BY {sortClause} LIMIT @Count OFFSET @Offset"); + + return queryStr.ToString(); + } + + /// + /// Builds a SQL query to count logs in the specified PostgreSQL table. + /// + /// The type of the log model. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append($"SELECT COUNT(\"{columns.Message}\") ") + .Append($"FROM \"{schema}\".\"{tableName}\""); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + return queryStr.ToString(); + } + + /// + /// Generates the SELECT clause for the SQL query. + /// + /// The type of the log model. + /// The StringBuilder to append the SELECT clause to. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + where T : LogModel + { + if (typeof(T) != typeof(PostgresLogModel)) + { + queryBuilder.Append("SELECT *"); + } + else + { + queryBuilder.Append($"SELECT \"{columns.Message}\", ") + .Append($"\"{columns.MessageTemplate}\", ") + .Append($"\"{columns.Level}\", ") + .Append($"\"{columns.Timestamp}\", ") + .Append($"\"{columns.Exception}\", ") + .Append($"\"{columns.LogEventSerialized}\" AS \"Properties\""); + } + + queryBuilder.Append($" FROM \"{schema}\".\"{tableName}\""); + } + + /// + /// Generates the WHERE clause for the SQL query. + /// + /// The type of the log model. + /// The StringBuilder to append the WHERE clause to. + /// The column names used in the sink for logging. + /// The log level to filter by. + /// The search criteria to filter by. + /// The start date to filter by. + /// The end date to filter by. + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + where T : LogModel + { + List conditions = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions.Add($"\"{columns.Level}\" = @Level"); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + string exceptionCondition = AddExceptionToWhereClause() ? $"OR \"{columns.Exception}\" LIKE @Search" : string.Empty; + conditions.Add($"(\"{columns.Message}\" LIKE @Search {exceptionCondition})"); + } + + if (startDate.HasValue) + { + conditions.Add($"\"{columns.Timestamp}\" >= @StartDate"); + } + + if (endDate.HasValue) + { + conditions.Add($"\"{columns.Timestamp}\" <= @EndDate"); + } + + if (conditions.Count <= 0) + { + return; + } + + queryBuilder + .Append(" WHERE TRUE AND ") + .Append(string.Join(" AND ", conditions)); + } + + /// + /// Determines whether to add the exception column to the WHERE clause based on the log model type. + /// + /// The type of the log model. + /// True if the exception column should be added; otherwise, false. + private static bool AddExceptionToWhereClause() + where T : LogModel + { + PropertyInfo? exceptionProperty = typeof(T).GetProperty(nameof(PostgresLogModel.Exception)); + RemovedColumnAttribute? att = exceptionProperty?.GetCustomAttribute(); + + return att is null; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs deleted file mode 100644 index 49d8171a..00000000 --- a/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; -using Serilog.Ui.PostgreSqlProvider.Models; -using static Serilog.Ui.Core.Models.SearchOptions; - -namespace Serilog.Ui.PostgreSqlProvider; - -internal static class QueryBuilder -{ - internal static string BuildFetchLogsQuery(this SinkColumnNames _columns, string schema, string tableName, FetchLogsQuery query) - where T : PostgresLogModel - { - var sortClause = _columns.GenerateSortClause(query.SortOn, query.SortBy); - - return new StringBuilder() - .GenerateSelectClause(_columns) - .Append($" FROM \"{schema}\".\"{tableName}\"") - .GenerateWhereClause(_columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate) - .Append($" ORDER BY {sortClause} LIMIT @Count OFFSET @Offset") - .ToString(); - } - - internal static string BuildCountLogsQuery(this SinkColumnNames _columns, string schema, string tableName, FetchLogsQuery query) - where T : PostgresLogModel - { - return new StringBuilder() - .Append($"SELECT COUNT(\"{_columns.RenderedMessage}\") ") - .Append($"FROM \"{schema}\".\"{tableName}\"") - .GenerateWhereClause(_columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate) - .ToString(); - } - - private static StringBuilder GenerateSelectClause(this StringBuilder queryBuilder, SinkColumnNames _columns) - where T : PostgresLogModel - { - if (typeof(T) != typeof(PostgresLogModel)) - { - return queryBuilder.Append("SELECT *"); - } - - return queryBuilder.Append($"SELECT \"{_columns.RenderedMessage}\", ") - .Append($"\"{_columns.MessageTemplate}\", ") - .Append($"\"{_columns.Level}\", ") - .Append($"\"{_columns.Timestamp}\", ") - .Append($"\"{_columns.Exception}\", ") - .Append($"\"{_columns.LogEventSerialized}\" AS \"Properties\""); - } - - private static StringBuilder GenerateWhereClause(this StringBuilder queryBuilder, - SinkColumnNames _columns, - string? level, - string? searchCriteria, - DateTime? startDate, - DateTime? endDate) - where T : PostgresLogModel - { - var conditions = new List(); - - if (!string.IsNullOrWhiteSpace(level)) - { - conditions.Add($"\"{_columns.Level}\" = @Level"); - } - - if (!string.IsNullOrWhiteSpace(searchCriteria)) - { - var exceptionCondition = AddExceptionToWhereClause() ? $"OR \"{_columns.Exception}\" LIKE @Search" : string.Empty; - conditions.Add($"(\"{_columns.RenderedMessage}\" LIKE @Search {exceptionCondition})"); - } - - if (startDate.HasValue) - { - conditions.Add($"\"{_columns.Timestamp}\" >= @StartDate"); - } - - if (endDate.HasValue) - { - conditions.Add($"\"{_columns.Timestamp}\" <= @EndDate"); - } - - if (conditions.Count <= 0) return queryBuilder; - - return queryBuilder - .Append(" WHERE TRUE AND ") - .Append(string.Join(" AND ", conditions)); - } - - private static bool AddExceptionToWhereClause() - where T : PostgresLogModel - { - var exceptionProperty = typeof(T).GetProperty(nameof(PostgresLogModel.Exception)); - var att = exceptionProperty?.GetCustomAttribute(); - return att is null; - } - - private static string GenerateSortClause(this SinkColumnNames _columns, SortProperty sortOn, SortDirection sortBy) - { - var sortPropertyName = sortOn switch - { - SortProperty.Timestamp => _columns.Timestamp, - SortProperty.Level => _columns.Level, - SortProperty.Message => _columns.RenderedMessage, - _ => _columns.Timestamp, - }; - - return $"\"{sortPropertyName}\" {sortBy.ToString().ToUpper()}"; - } -} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj index 2c60d4f3..fa032bc8 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj +++ b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj @@ -5,9 +5,7 @@ netstandard2.0 latest 3.1.0 - True - PostgreSQL data provider for Serilog UI. serilog serilog-ui serilog.sinks.postgresql postgresql diff --git a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj index 4bbfa663..14593355 100644 --- a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj +++ b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj @@ -7,12 +7,12 @@ 3.1.0 - - - + + + - + diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs index cd2b0bce..c940992a 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -11,39 +11,49 @@ using Serilog.Ui.PostgreSqlProvider.Extensions; using Xunit; -namespace Postgres.Tests.DataProvider +namespace Postgres.Tests.DataProvider; + +[Trait("Unit-Base", "Postgres")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "Postgres")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact] + public void It_throws_when_any_dependency_is_null() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var sut = new List + // Arrange + var sut = new List { - () => { _ = new PostgresDataProvider(null!); }, - () => { _ = new PostgresDataProvider(null!); }, + () => { _ = new PostgresDataProvider(null!, null!); }, + () => { _ = new PostgresDataProvider(null!, null !); }, }; - sut.ForEach(s => s.Should().ThrowExactly()); - } - - [Fact] - public async Task It_logs_and_throws_when_db_read_breaks_down() - { - var sut = new PostgresDataProvider(new PostgreSqlDbOptions("dbo") - .WithConnectionString("connString") - .WithTable("logs")); - var sutWithCols = new PostgresDataProvider(new PostgreSqlDbOptions("dbo") - .WithConnectionString("connString") - .WithTable("logs")); - var query = new Dictionary { ["page"] = "1", ["count"] = "10" }; - - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - - await assert.Should().ThrowExactlyAsync(); - await assertWithCols.Should().ThrowExactlyAsync(); - } + // Act + + // Assert + sut.ForEach(s => s.Should().ThrowExactly()); + } + + [Fact] + public async Task It_logs_and_throws_when_db_read_breaks_down() + { + // Arrange + PostgresQueryBuilder queryBuilder = new(); + + var sut = new PostgresDataProvider( + new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + queryBuilder); + + var sutWithCols = new PostgresDataProvider( + new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + queryBuilder); + + var query = new Dictionary { ["page"] = "1", ["count"] = "10" }; + + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + + // Assert + await assert.Should().ThrowExactlyAsync(); + await assertWithCols.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs index 52d954f7..791f6dd7 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -14,7 +14,7 @@ namespace Postgres.Tests.DataProvider; [Trait("Unit-QueryBuilder", "Postgres")] public class QueryBuilderTests { - private readonly PostgreSqlAlternativeSinkColumnNames _sut = new(); + private readonly PostgresQueryBuilder _sut = new(); [Theory] [ClassData(typeof(QueryBuilderTestData))] @@ -36,8 +36,10 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( ["endDate"] = endDate?.ToString("O") }; + PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + // Act - var query = _sut.BuildFetchLogsQuery(schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + var query = _sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); // Assert query.Should().Be(expectedQuery); @@ -53,8 +55,10 @@ public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() ["search"] = "criteria" }; + PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + // Act - var query = _sut.BuildFetchLogsQuery("test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + var query = _sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); // Assert query.ToLowerInvariant().Should().StartWith("select *"); diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs index 6bd8c3da..2e3ab2ff 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs @@ -31,7 +31,7 @@ protected PostgresTestProvider() Container = new PostgreSqlBuilder().Build(); } - private PostgreSqlDbOptions DbOptions { get; set; } = new PostgreSqlDbOptions("public") + private PostgreSqlDbOptions DbOptions { get; } = new PostgreSqlDbOptions("public") .WithTable("logs") .WithSinkType(PostgreSqlSinkType.SerilogSinksPostgreSQLAlternative); @@ -66,7 +66,8 @@ protected override Task InitializeAdditionalAsync() Collector = serilog.InitializeLogs(); var custom = typeof(T) != typeof(PostgresLogModel); - Provider = custom ? new PostgresDataProvider(DbOptions) : new PostgresDataProvider(DbOptions); + PostgresQueryBuilder queryBuilder = new(); + Provider = custom ? new PostgresDataProvider(DbOptions, queryBuilder) : new PostgresDataProvider(DbOptions, queryBuilder); return Task.CompletedTask; } From 9127ebffd9d61c7344c13155094e262b51015a46 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 22 Sep 2024 10:16:28 +0200 Subject: [PATCH 03/13] Rename PostgreDataProvider file. --- .../{PostgreDataProvider.cs => PostgresDataProvider.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Serilog.Ui.PostgreSqlProvider/{PostgreDataProvider.cs => PostgresDataProvider.cs} (100%) diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs similarity index 100% rename from src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs rename to src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs From 3120c2bdb67ac629b2effd53a9f04fccdb4f659c Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 22 Sep 2024 22:15:59 +0200 Subject: [PATCH 04/13] Implement SqlServer query builder. --- .../QueryBuilder/Sql/SqlQueryBuilder.cs | 45 ++-- .../SerilogUiOptionBuilderExtensions.cs | 104 ++++----- .../Extensions/SqlServerDbOptions.cs | 10 + .../Models/SqlServerSinkColumnNames.cs | 16 ++ .../Serilog.Ui.MsSqlServerProvider.csproj | 3 +- .../SqlServerDataProvider.cs | 200 +++++------------- .../SqlServerQueryBuilder.cs | 126 +++++++++++ .../Extensions/PostgreSqlDbOptions.cs | 1 + .../SerilogUiOptionBuilderExtensions.cs | 77 ++++--- .../PostgresDataProvider.cs | 24 +-- .../PostgresQueryBuilder.cs | 91 +++----- .../DataProvider/DataProviderBaseTest.cs | 52 +++-- .../DataProvider/QueryBuilderTests.cs | 106 ++++++++++ .../Util/MsSqlServerTestProvider.cs | 9 +- .../DataProvider/DataProviderBaseTest.cs | 29 +-- .../DataProvider/QueryBuilderTests.cs | 14 +- .../Util/PostgresTestProvider.cs | 5 +- 17 files changed, 520 insertions(+), 392 deletions(-) create mode 100644 src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs create mode 100644 src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs create mode 100644 src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs create mode 100644 tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs index 6603fc92..b9b112b3 100644 --- a/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs +++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs @@ -1,4 +1,6 @@ -using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.Attributes; +using Serilog.Ui.Core.Models; +using System.Reflection; using static Serilog.Ui.Core.Models.SearchOptions; namespace Serilog.Ui.Core.QueryBuilder.Sql; @@ -6,29 +8,27 @@ namespace Serilog.Ui.Core.QueryBuilder.Sql; /// /// Abstract class that provides methods to build SQL queries for fetching and counting logs. /// -public abstract class SqlQueryBuilder +public abstract class SqlQueryBuilder where TModel : LogModel { /// /// Builds a SQL query to fetch logs from the specified table. /// - /// The type of the log model. /// The column names used in the sink for logging. /// The schema of the table. /// The name of the table. /// The query parameters for fetching logs. /// A SQL query string to fetch logs. - public abstract string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) where T : LogModel; + public abstract string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query); /// /// Builds a SQL query to count logs in the specified table. /// - /// The type of the log model. /// The column names used in the sink for logging. /// The schema of the table. /// The name of the table. /// The query parameters for counting logs. /// A SQL query string to count logs. - public abstract string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) where T : LogModel; + public abstract string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query); /// /// Generates a SQL sort clause based on the specified sort property and direction. @@ -37,16 +37,31 @@ public abstract class SqlQueryBuilder /// The property to sort on. /// The direction to sort by. /// A SQL sort clause string. - protected static string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy) + protected abstract string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy); + + /// + /// Generates a SQL sort clause based on the specified sort property and direction. + /// + /// The column names used in the sink for logging. + /// The property to sort on. + /// A SQL sort clause string. + protected static string GetSortColumnName(SinkColumnNames columns, SortProperty sortOn) => sortOn switch + { + SortProperty.Timestamp => columns.Timestamp, + SortProperty.Level => columns.Level, + SortProperty.Message => columns.Message, + _ => columns.Timestamp + }; + + /// + /// Determines whether to add the exception column to the WHERE clause based on the presence of the RemovedColumnAttribute. + /// + /// True if the exception column should be added to the WHERE clause; otherwise, false. + protected static bool AddExceptionToWhereClause() { - var sortPropertyName = sortOn switch - { - SortProperty.Timestamp => columns.Timestamp, - SortProperty.Level => columns.Level, - SortProperty.Message => columns.Message, - _ => columns.Timestamp - }; + PropertyInfo? exceptionProperty = typeof(TModel).GetProperty("Exception"); + RemovedColumnAttribute? att = exceptionProperty?.GetCustomAttribute(); - return $"\"{sortPropertyName}\" {sortBy.ToString().ToUpper()}"; + return att is null; } } \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index a1d67b01..30f36029 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,64 +1,64 @@ -using System; -using Dapper; +using Dapper; using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Core.Models.Options; +using System; -namespace Serilog.Ui.MsSqlServerProvider.Extensions -{ - /// - /// SQL Server data provider specific extension methods for . - /// - public static class SerilogUiOptionBuilderExtensions - { - /// Configures the SerilogUi to connect to a SQL Server database. - /// The options builder. - /// The Ms Sql options action. - /// - /// Delegate to customize the DateTime parsing. - /// It throws if the return DateTime isn't UTC kind. - /// - public static ISerilogUiOptionsBuilder UseSqlServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions, - Func? dateTimeCustomParsing = null - ) => optionsBuilder.UseSqlServer(setupOptions, dateTimeCustomParsing); - - /// Configures the SerilogUi to connect to a SQL Server database. - /// The log model, containing any additional columns. It must inherit . - /// The options builder. - /// The Ms Sql options action. - /// - /// Delegate to customize the DateTime parsing. - /// It throws if the return DateTime isn't UTC kind. - /// - public static ISerilogUiOptionsBuilder UseSqlServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions, - Func? dateTimeCustomParsing = null - ) where T : SqlServerLogModel - { - var dbOptions = new RelationalDbOptions("dbo"); - setupOptions(dbOptions); - dbOptions.Validate(); - - var providerName = dbOptions.GetProviderName(SqlServerDataProvider.MsSqlProviderName); +namespace Serilog.Ui.MsSqlServerProvider.Extensions; - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - SqlMapper.AddTypeHandler(new DapperDateTimeHandler(dateTimeCustomParsing)); +/// +/// SQL Server data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions +{ + /// Configures the SerilogUi to connect to a SQL Server database. + /// The options builder. + /// The Ms Sql options action. + /// + /// Delegate to customize the DateTime parsing. + /// It throws if the return DateTime isn't UTC kind. + /// + public static ISerilogUiOptionsBuilder UseSqlServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions, + Func? dateTimeCustomParsing = null + ) => optionsBuilder.UseSqlServer(setupOptions, dateTimeCustomParsing); - var customModel = typeof(T) != typeof(SqlServerLogModel); - if (customModel) - { - optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions)); + /// Configures the SerilogUi to connect to a SQL Server database. + /// The log model, containing any additional columns. It must inherit . + /// The options builder. + /// The Ms Sql options action. + /// + /// Delegate to customize the DateTime parsing. + /// It throws if the return DateTime isn't UTC kind. + /// + public static ISerilogUiOptionsBuilder UseSqlServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions, + Func? dateTimeCustomParsing = null + ) where T : SqlServerLogModel + { + SqlServerDbOptions dbOptions = new("dbo"); + setupOptions(dbOptions); + dbOptions.Validate(); - return optionsBuilder; - } + string providerName = dbOptions.GetProviderName(SqlServerDataProvider.MsSqlProviderName); + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + SqlMapper.AddTypeHandler(new DapperDateTimeHandler(dateTimeCustomParsing)); - optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions)); - return optionsBuilder; + bool customModel = typeof(T) != typeof(SqlServerLogModel); + if (customModel) + { + optionsBuilder.RegisterColumnsInfo(providerName); + optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions, new SqlServerQueryBuilder())); } + else + { + optionsBuilder.Services.AddScoped(_ => + new SqlServerDataProvider(dbOptions, new SqlServerQueryBuilder())); + } + + return optionsBuilder; } } \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs new file mode 100644 index 00000000..34ae5166 --- /dev/null +++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.MsSqlServerProvider.Models; + +namespace Serilog.Ui.MsSqlServerProvider.Extensions; + +public class SqlServerDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) +{ + internal SinkColumnNames ColumnNames { get; } = new SqlServerSinkColumnNames(); +} \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs new file mode 100644 index 00000000..3806f527 --- /dev/null +++ b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.MsSqlServerProvider.Models; + +internal class SqlServerSinkColumnNames : SinkColumnNames +{ + public SqlServerSinkColumnNames() + { + Exception = "Exception"; + Level = "Level"; + LogEventSerialized = "Properties"; + Message = "Message"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj index a47a2017..3c3ca3e4 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj +++ b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj @@ -13,10 +13,11 @@ - + + \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs index 15100abe..93b57651 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs +++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs @@ -1,168 +1,76 @@ -using System.Collections.Generic; +using Dapper; +using Microsoft.Data.SqlClient; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MsSqlServerProvider.Extensions; +using System.Collections.Generic; using System.Data; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Ardalis.GuardClauses; -using Dapper; -using Microsoft.Data.SqlClient; -using Serilog.Ui.Core; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; -using static Serilog.Ui.Core.Models.SearchOptions; - -namespace Serilog.Ui.MsSqlServerProvider -{ - public class SqlServerDataProvider(RelationalDbOptions options) : SqlServerDataProvider(options) - { - protected override string SearchCriteriaWhereQuery() => "OR [Exception] LIKE @Search"; - - protected override string SelectQuery() - { - const string level = $"[{ColumnLevelName}]"; - const string message = $"[{ColumnMessageName}]"; - const string timestamp = $"[{ColumnTimestampName}]"; - - return $"SELECT [Id], {message}, {level}, {timestamp}, [Exception], [Properties] "; - } - } - - public class SqlServerDataProvider(RelationalDbOptions options) : IDataProvider - where T : SqlServerLogModel - { - internal const string MsSqlProviderName = "MsSQL"; - - private protected const string ColumnTimestampName = "TimeStamp"; - - private protected const string ColumnLevelName = "Level"; - - private protected const string ColumnMessageName = "Message"; - - private readonly RelationalDbOptions _options = Guard.Against.Null(options); - - public string Name => _options.GetProviderName(MsSqlProviderName); - - protected virtual string SelectQuery() => "SELECT * "; - public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) - { - // since sink stores dates in local time, we query by local time - queryParams.ToLocalDates(); +namespace Serilog.Ui.MsSqlServerProvider; - var logsTask = GetLogsAsync(queryParams); - var logCountTask = CountLogsAsync(queryParams); +/// +public class SqlServerDataProvider(SqlServerDbOptions options, SqlServerQueryBuilder queryBuilder) + : SqlServerDataProvider(options, queryBuilder); - await Task.WhenAll(logsTask, logCountTask); - - return (await logsTask, await logCountTask); - } - - private async Task> GetLogsAsync(FetchLogsQuery queryParams) - { - var queryBuilder = new StringBuilder(); - - queryBuilder.Append(SelectQuery()); - queryBuilder.Append($"FROM [{_options.Schema}].[{_options.TableName}] "); - - GenerateWhereClause(queryBuilder, queryParams); - - GenerateSortClause(queryBuilder, queryParams.SortOn, queryParams.SortBy); - - queryBuilder.Append("OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY"); - - var rowNoStart = queryParams.Page * queryParams.Count; - - using IDbConnection connection = new SqlConnection(_options.ConnectionString); - var logs = await connection.QueryAsync(queryBuilder.ToString(), - new - { - Offset = rowNoStart, - queryParams.Count, - queryParams.Level, - Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, - queryParams.StartDate, - queryParams.EndDate - }); - - return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); - } - - private async Task CountLogsAsync(FetchLogsQuery queryParams) - { - var queryBuilder = new StringBuilder(); - queryBuilder.Append($"SELECT COUNT(Id) FROM [{_options.Schema}].[{_options.TableName}]"); +/// +public class SqlServerDataProvider(SqlServerDbOptions options, SqlServerQueryBuilder queryBuilder) : IDataProvider + where T : SqlServerLogModel +{ + internal const string MsSqlProviderName = "MsSQL"; - GenerateWhereClause(queryBuilder, queryParams); + /// + public string Name => options.GetProviderName(MsSqlProviderName); - using IDbConnection connection = new SqlConnection(_options.ConnectionString); - return await connection.ExecuteScalarAsync(queryBuilder.ToString(), - new - { - queryParams.Level, - Search = queryParams.SearchCriteria != null ? "%" + queryParams.SearchCriteria + "%" : null, - queryParams.StartDate, - queryParams.EndDate - }); - } + public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) + { + // since sink stores dates in local time, we query by local time + queryParams.ToLocalDates(); - /// - /// If Exception property is flagged with , - /// it removes the Where query part on the Exception field. - /// - /// - protected virtual string SearchCriteriaWhereQuery() - { - var exceptionProperty = typeof(T).GetProperty(nameof(SqlServerLogModel.Exception)); - var att = exceptionProperty?.GetCustomAttribute(); - return att is null ? "OR [Exception] LIKE @Search" : string.Empty; - } + var logsTask = GetLogsAsync(queryParams); + var logCountTask = CountLogsAsync(queryParams); - private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams) - { - var conditionStart = "WHERE"; + await Task.WhenAll(logsTask, logCountTask); - if (!string.IsNullOrWhiteSpace(queryParams.Level)) - { - queryBuilder.Append($"{conditionStart} [{ColumnLevelName}] = @Level "); - conditionStart = "AND"; - } + return (await logsTask, await logCountTask); + } - if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria)) - { - queryBuilder.Append($"{conditionStart} [{ColumnMessageName}] LIKE @Search {SearchCriteriaWhereQuery()} "); - conditionStart = "AND"; - } + private async Task> GetLogsAsync(FetchLogsQuery queryParams) + { + string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); + int rowNoStart = queryParams.Page * queryParams.Count; - if (queryParams.StartDate != null) - { - queryBuilder.Append($"{conditionStart} [{ColumnTimestampName}] >= @StartDate "); - conditionStart = "AND"; - } + using IDbConnection connection = new SqlConnection(options.ConnectionString); - if (queryParams.EndDate != null) + IEnumerable logs = await connection.QueryAsync(query, + new { - queryBuilder.Append($"{conditionStart} [{ColumnTimestampName}] <= @EndDate "); - } - } + Offset = rowNoStart, + queryParams.Count, + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + queryParams.StartDate, + queryParams.EndDate + }); + + return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); + } - private static void GenerateSortClause(StringBuilder queryBuilder, SortProperty sortOn, SortDirection sortBy) - { - var sortOnCol = GetColumnName(sortOn); - var sortByCol = sortBy.ToString().ToUpper(); + private async Task CountLogsAsync(FetchLogsQuery queryParams) + { + string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); - queryBuilder.Append($"ORDER BY [{sortOnCol}] {sortByCol} "); - } + using IDbConnection connection = new SqlConnection(options.ConnectionString); - private static string GetColumnName(SortProperty sortOn) - => sortOn switch + return await connection.ExecuteScalarAsync(query, + new { - SortProperty.Level => ColumnLevelName, - SortProperty.Message => ColumnMessageName, - SortProperty.Timestamp => ColumnTimestampName, - _ => ColumnTimestampName - }; + queryParams.Level, + Search = queryParams.SearchCriteria != null ? "%" + queryParams.SearchCriteria + "%" : null, + queryParams.StartDate, + queryParams.EndDate + }); } } \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs new file mode 100644 index 00000000..061180b2 --- /dev/null +++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs @@ -0,0 +1,126 @@ +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using System; +using System.Text; + +namespace Serilog.Ui.MsSqlServerProvider; + +/// +/// Provides methods to build SQL queries specifically for SQL Server to fetch and count logs. +/// +/// The type of the log model. +public class SqlServerQueryBuilder : SqlQueryBuilder where TModel : LogModel +{ + /// + 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 @Count OFFSET @Offset"); + + return queryStr.ToString(); + } + + /// + /// Builds a SQL query to count logs in the specified table. + /// + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append("SELECT COUNT([Id]) ") + .Append($"FROM [{schema}].[{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()}"; + + /// + /// Generates the SELECT clause for the SQL query. + /// + /// The StringBuilder to append the SELECT clause to. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + { + if (typeof(TModel) != typeof(SqlServerLogModel)) + { + queryBuilder.Append("SELECT * "); + } + else + { + queryBuilder.Append("SELECT [Id], ") + .Append($"[{columns.Message}], ") + .Append($"[{columns.Level}], ") + .Append($"[{columns.Timestamp}], ") + .Append($"[{columns.Exception}], ") + .Append($"[{columns.LogEventSerialized}] "); + } + + queryBuilder.Append($"FROM [{schema}].[{tableName}] "); + } + + /// + /// Generates the WHERE clause for the SQL query. + /// + /// The StringBuilder to append the WHERE clause to. + /// The column names used in the sink for logging. + /// The log level to filter by. + /// The search criteria to filter by. + /// The start date to filter by. + /// The end date to filter by. + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + StringBuilder conditions2 = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions2.Append($"AND [{columns.Level}] = @Level "); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + conditions2.Append($"AND ([{columns.Message}] LIKE @Search "); + conditions2.Append(AddExceptionToWhereClause() ? $"OR [{columns.Exception}] LIKE @Search) " : ") "); + } + + if (startDate.HasValue) + { + conditions2.Append($"AND [{columns.Timestamp}] >= @StartDate "); + } + + if (endDate.HasValue) + { + conditions2.Append($"AND [{columns.Timestamp}] <= @EndDate "); + } + + if (conditions2.Length <= 0) + { + return; + } + + queryBuilder + .Append("WHERE TRUE ") + .Append(conditions2); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs index c53687bd..0ff48ee9 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs @@ -31,6 +31,7 @@ public PostgreSqlDbOptions WithSinkType(PostgreSqlSinkType sinkType) ColumnNames = sinkType == PostgreSqlSinkType.SerilogSinksPostgreSQLAlternative ? new PostgreSqlAlternativeSinkColumnNames() : new PostgreSqlSinkColumnNames(); + return this; } diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 302412f1..8ba23502 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -4,53 +4,48 @@ using Serilog.Ui.PostgreSqlProvider.Models; using System; -namespace Serilog.Ui.PostgreSqlProvider.Extensions +namespace Serilog.Ui.PostgreSqlProvider.Extensions; + +/// +/// PostgreSQL data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions { /// - /// PostgreSQL data provider specific extension methods for . + /// Configures the SerilogUi to connect to a PostgreSQL database. /// - public static class SerilogUiOptionBuilderExtensions - { - /// - /// Configures the SerilogUi to connect to a PostgreSQL database. - /// - /// The Serilog UI option builder. - /// The Postgres Sql options action. - public static ISerilogUiOptionsBuilder UseNpgSql( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) => optionsBuilder.UseNpgSql(setupOptions); - - /// - /// Configures the SerilogUi to connect to a PostgreSQL database. - /// - /// The log model, containing any additional columns. It must inherit . - /// The Serilog UI option builder. - /// The Postgres Sql options action. - public static ISerilogUiOptionsBuilder UseNpgSql( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) where T : PostgresLogModel - { - var dbOptions = new PostgreSqlDbOptions("public"); - setupOptions(dbOptions); - dbOptions.Validate(); + /// The Serilog UI option builder. + /// The Postgres Sql options action. + public static ISerilogUiOptionsBuilder UseNpgSql(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + => optionsBuilder.UseNpgSql(setupOptions); - var providerName = dbOptions.GetProviderName(PostgresDataProvider.ProviderName); - - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - - var customModel = typeof(T) != typeof(PostgresLogModel); - if (customModel) - { - optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); + /// + /// Configures the SerilogUi to connect to a PostgreSQL database. + /// + /// The log model, containing any additional columns. It must inherit . + /// The Serilog UI option builder. + /// The Postgres Sql options action. + public static ISerilogUiOptionsBuilder UseNpgSql(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + where T : PostgresLogModel + { + PostgreSqlDbOptions dbOptions = new("public"); + setupOptions(dbOptions); + dbOptions.Validate(); - return optionsBuilder; - } + string providerName = dbOptions.GetProviderName(PostgresDataProvider.ProviderName); + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); - return optionsBuilder; + bool customModel = typeof(T) != typeof(PostgresLogModel); + if (customModel) + { + optionsBuilder.RegisterColumnsInfo(providerName); + optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); } + else + { + optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); + } + + return optionsBuilder; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs index 108052ca..3a2d44be 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs @@ -4,7 +4,6 @@ using Serilog.Ui.Core.Models; using Serilog.Ui.PostgreSqlProvider.Extensions; using Serilog.Ui.PostgreSqlProvider.Models; -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -13,26 +12,25 @@ namespace Serilog.Ui.PostgreSqlProvider; /// -public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) +public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) : PostgresDataProvider(options, queryBuilder); /// -public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) : IDataProvider +public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) : IDataProvider where T : PostgresLogModel { internal const string ProviderName = "NPGSQL"; - private readonly PostgreSqlDbOptions _options = options ?? throw new ArgumentNullException(nameof(options)); /// - public string Name => _options.GetProviderName(ProviderName); + public string Name => options.GetProviderName(ProviderName); /// public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) { queryParams.ToUtcDates(); - var logsTask = GetLogsAsync(queryParams); - var logCountTask = CountLogsAsync(queryParams); + Task> logsTask = GetLogsAsync(queryParams); + Task logCountTask = CountLogsAsync(queryParams); await Task.WhenAll(logsTask, logCountTask); return (await logsTask, await logCountTask); @@ -40,12 +38,12 @@ public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryB private async Task> GetLogsAsync(FetchLogsQuery queryParams) { - var query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, _options.Schema, _options.TableName, queryParams); - var rowNoStart = queryParams.Page * queryParams.Count; + string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); + int rowNoStart = queryParams.Page * queryParams.Count; - await using var connection = new NpgsqlConnection(_options.ConnectionString); + await using NpgsqlConnection connection = new(options.ConnectionString); - var logs = await connection.QueryAsync(query, + IEnumerable logs = await connection.QueryAsync(query, new { Offset = rowNoStart, @@ -68,9 +66,9 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam private async Task CountLogsAsync(FetchLogsQuery queryParams) { - var query = queryBuilder.BuildCountLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); + string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); - await using var connection = new NpgsqlConnection(_options.ConnectionString); + await using NpgsqlConnection connection = new(options.ConnectionString); return await connection.ExecuteScalarAsync(query, new diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs index 39f5efa3..675fe49a 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs @@ -1,76 +1,59 @@ -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.Models; using Serilog.Ui.Core.QueryBuilder.Sql; using Serilog.Ui.PostgreSqlProvider.Models; using System; -using System.Collections.Generic; -using System.Reflection; using System.Text; +using static Serilog.Ui.Core.Models.SearchOptions; namespace Serilog.Ui.PostgreSqlProvider; /// /// Provides methods to build SQL queries specifically for PostgreSQL to fetch and count logs. /// -public class PostgresQueryBuilder : SqlQueryBuilder +/// The type of the log model. +public class PostgresQueryBuilder : SqlQueryBuilder where TModel : LogModel { - /// - /// Builds a SQL query to fetch logs from the specified PostgreSQL table. - /// - /// The type of the log model. - /// The column names used in the sink for logging. - /// The schema of the table. - /// The name of the table. - /// The query parameters for fetching logs. - /// A SQL query string to fetch logs. - public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + /// + public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) { StringBuilder queryStr = new(); - GenerateSelectClause(queryStr, columns, schema, tableName); + GenerateSelectClause(queryStr, columns, schema, tableName); - GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); - string sortClause = GenerateSortClause(columns, query.SortOn, query.SortBy); - - queryStr.Append($" ORDER BY {sortClause} LIMIT @Count OFFSET @Offset"); + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Count OFFSET @Offset"); return queryStr.ToString(); } - /// - /// Builds a SQL query to count logs in the specified PostgreSQL table. - /// - /// The type of the log model. - /// The column names used in the sink for logging. - /// The schema of the table. - /// The name of the table. - /// The query parameters for counting logs. - /// A SQL query string to count logs. - public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + /// + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) { StringBuilder queryStr = new(); queryStr.Append($"SELECT COUNT(\"{columns.Message}\") ") .Append($"FROM \"{schema}\".\"{tableName}\""); - GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); return queryStr.ToString(); } + /// + protected override string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy) + => $"ORDER BY \"{GetSortColumnName(columns, sortOn)}\" {sortBy.ToString().ToUpper()}"; + /// /// Generates the SELECT clause for the SQL query. /// - /// The type of the log model. /// The StringBuilder to append the SELECT clause to. /// The column names used in the sink for logging. /// The schema of the table. /// The name of the table. - private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) - where T : LogModel + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) { - if (typeof(T) != typeof(PostgresLogModel)) + if (typeof(TModel) != typeof(PostgresLogModel)) { queryBuilder.Append("SELECT *"); } @@ -84,72 +67,56 @@ private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColu .Append($"\"{columns.LogEventSerialized}\" AS \"Properties\""); } - queryBuilder.Append($" FROM \"{schema}\".\"{tableName}\""); + queryBuilder.Append($" FROM \"{schema}\".\"{tableName}\" "); } /// /// Generates the WHERE clause for the SQL query. /// - /// The type of the log model. /// The StringBuilder to append the WHERE clause to. /// The column names used in the sink for logging. /// The log level to filter by. /// The search criteria to filter by. /// The start date to filter by. /// The end date to filter by. - private static void GenerateWhereClause( + private static void GenerateWhereClause( StringBuilder queryBuilder, SinkColumnNames columns, string? level, string? searchCriteria, DateTime? startDate, DateTime? endDate) - where T : LogModel { - List conditions = new(); + StringBuilder conditions = new(); if (!string.IsNullOrWhiteSpace(level)) { - conditions.Add($"\"{columns.Level}\" = @Level"); + conditions.Append($"AND \"{columns.Level}\" = @Level "); } if (!string.IsNullOrWhiteSpace(searchCriteria)) { - string exceptionCondition = AddExceptionToWhereClause() ? $"OR \"{columns.Exception}\" LIKE @Search" : string.Empty; - conditions.Add($"(\"{columns.Message}\" LIKE @Search {exceptionCondition})"); + conditions.Append($"AND (\"{columns.Message}\" LIKE @Search "); + conditions.Append(AddExceptionToWhereClause() ? $"OR \"{columns.Exception}\" LIKE @Search) " : ") "); } if (startDate.HasValue) { - conditions.Add($"\"{columns.Timestamp}\" >= @StartDate"); + conditions.Append($"AND \"{columns.Timestamp}\" >= @StartDate "); } if (endDate.HasValue) { - conditions.Add($"\"{columns.Timestamp}\" <= @EndDate"); + conditions.Append($"AND \"{columns.Timestamp}\" <= @EndDate "); } - if (conditions.Count <= 0) + if (conditions.Length <= 0) { return; } queryBuilder - .Append(" WHERE TRUE AND ") - .Append(string.Join(" AND ", conditions)); - } - - /// - /// Determines whether to add the exception column to the WHERE clause based on the log model type. - /// - /// The type of the log model. - /// True if the exception column should be added; otherwise, false. - private static bool AddExceptionToWhereClause() - where T : LogModel - { - PropertyInfo? exceptionProperty = typeof(T).GetProperty(nameof(PostgresLogModel.Exception)); - RemovedColumnAttribute? att = exceptionProperty?.GetCustomAttribute(); - - return att is null; + .Append("WHERE TRUE ") + .Append(conditions); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs index 34ab68d9..a340fb91 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -7,42 +7,38 @@ using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MsSqlServerProvider; +using Serilog.Ui.MsSqlServerProvider.Extensions; using Xunit; -namespace MsSql.Tests.DataProvider +namespace MsSql.Tests.DataProvider; + +[Trait("Unit-Base", "MsSql")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "MsSql")] - public class DataProviderBaseTest : IUnitBaseTests + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public async Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var suts = new List - { - () => { _ = new SqlServerDataProvider(null!); }, - () => { _ = new SqlServerDataProvider(null!); }, - }; + // Arrange + SqlServerDataProvider sut = new( + new SqlServerDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new SqlServerQueryBuilder()); - suts.ForEach(sut => sut.Should().ThrowExactly()); - } + SqlServerDataProvider sutWithCols = new( + new SqlServerDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new SqlServerQueryBuilder()); - [Fact] - public async Task It_logs_and_throws_when_db_read_breaks_down() - { - // Arrange - var sut = new SqlServerDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var sutWithAdditionalCols = - new SqlServerDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var query = new Dictionary { ["page"] = "1", ["count"] = "10", }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - // Act - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithAdditionalCols = () => sutWithAdditionalCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - await assert.Should().ThrowExactlyAsync(); - await assertWithAdditionalCols.Should().ThrowExactlyAsync(); - } + // Assert + await assert.Should().ThrowExactlyAsync(); + await assertWithCols.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs new file mode 100644 index 00000000..9ed2784e --- /dev/null +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using MsSql.Tests.Util; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MsSqlServerProvider; +using Serilog.Ui.MsSqlServerProvider.Models; +using Xunit; + +namespace MsSql.Tests.DataProvider; + +[Trait("Unit-QueryBuilder", "MsSql")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForAlternativeSink_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") + }; + + SqlServerSinkColumnNames sinkColumns = new(); + SqlServerQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + [Fact] + public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = "level", + ["search"] = "criteria" + }; + + SqlServerSinkColumnNames sinkColumns = new(); + SqlServerQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.ToLowerInvariant().Should().StartWith("select"); + query.ToLowerInvariant().Should().NotContain("exception"); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + "dbo", "logs", null!, null!, null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ], + [ + "dbo", "logs", null!, null!, null!, DateTime.Now, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ], + [ + "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ], + [ + "dbo", "logs", "Information", null!, null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [Level] = @Level ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ], + [ + "dbo", "logs", null!, "Test", null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ], + [ + "dbo", "logs", "Information", "Test", null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ], + [ + "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs index 2a5c6005..9e4397d2 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs @@ -7,8 +7,8 @@ using Serilog.Ui.Common.Tests.DataSamples; using Serilog.Ui.Common.Tests.SqlUtil; using Serilog.Ui.Core.Extensions; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MsSqlServerProvider; +using Serilog.Ui.MsSqlServerProvider.Extensions; using Testcontainers.MsSql; using Xunit; @@ -29,7 +29,7 @@ protected MsSqlServerTestProvider() Container = new MsSqlBuilder().Build(); } - private RelationalDbOptions DbOptions { get; } = new RelationalDbOptions("dbo").WithTable("Logs"); + private SqlServerDbOptions DbOptions { get; } = new SqlServerDbOptions("dbo").WithTable("Logs"); protected override sealed IContainer Container { get; set; } @@ -62,8 +62,11 @@ protected override Task InitializeAdditionalAsync() Collector = serilog.InitializeLogs(); SqlMapper.AddTypeHandler(new DapperDateTimeHandler()); + var custom = typeof(T) != typeof(SqlServerLogModel); - Provider = custom ? new SqlServerDataProvider(DbOptions) : new SqlServerDataProvider(DbOptions); + Provider = custom + ? new SqlServerDataProvider(DbOptions, new SqlServerQueryBuilder()) + : new SqlServerDataProvider(DbOptions, new SqlServerQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs index c940992a..c199f384 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -9,6 +9,7 @@ using Serilog.Ui.Core.Models; using Serilog.Ui.PostgreSqlProvider; using Serilog.Ui.PostgreSqlProvider.Extensions; +using Serilog.Ui.PostgreSqlProvider.Models; using Xunit; namespace Postgres.Tests.DataProvider; @@ -16,37 +17,21 @@ namespace Postgres.Tests.DataProvider; [Trait("Unit-Base", "Postgres")] public class DataProviderBaseTest : IUnitBaseTests { - [Fact] public void It_throws_when_any_dependency_is_null() - { - // Arrange - var sut = new List - { - () => { _ = new PostgresDataProvider(null!, null!); }, - () => { _ = new PostgresDataProvider(null!, null !); }, - }; - - // Act + => throw new NotImplementedException(); - // Assert - sut.ForEach(s => s.Should().ThrowExactly()); - } - - [Fact] public async Task It_logs_and_throws_when_db_read_breaks_down() { // Arrange - PostgresQueryBuilder queryBuilder = new(); - - var sut = new PostgresDataProvider( + PostgresDataProvider sut = new( new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), - queryBuilder); + new PostgresQueryBuilder()); - var sutWithCols = new PostgresDataProvider( + PostgresDataProvider sutWithCols = new( new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), - queryBuilder); + new PostgresQueryBuilder()); - var query = new Dictionary { ["page"] = "1", ["count"] = "10" }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; // Act var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs index 791f6dd7..7e7725e6 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -14,8 +14,6 @@ namespace Postgres.Tests.DataProvider; [Trait("Unit-QueryBuilder", "Postgres")] public class QueryBuilderTests { - private readonly PostgresQueryBuilder _sut = new(); - [Theory] [ClassData(typeof(QueryBuilderTestData))] public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( @@ -28,7 +26,7 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( string expectedQuery) { // Arrange - var queryLogs = new Dictionary + Dictionary queryLogs = new() { ["level"] = level, ["search"] = searchCriteria, @@ -37,9 +35,10 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( }; PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + PostgresQueryBuilder sut = new(); // Act - var query = _sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); // Assert query.Should().Be(expectedQuery); @@ -49,19 +48,20 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() { // Arrange - var queryLogs = new Dictionary + Dictionary queryLogs = new() { ["level"] = "level", ["search"] = "criteria" }; PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + PostgresQueryBuilder sut = new(); // Act - var query = _sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); // Assert - query.ToLowerInvariant().Should().StartWith("select *"); + query.ToLowerInvariant().Should().StartWith("select"); query.ToLowerInvariant().Should().NotContain("exception"); } diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs index 2e3ab2ff..37481eaf 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs @@ -66,8 +66,9 @@ protected override Task InitializeAdditionalAsync() Collector = serilog.InitializeLogs(); var custom = typeof(T) != typeof(PostgresLogModel); - PostgresQueryBuilder queryBuilder = new(); - Provider = custom ? new PostgresDataProvider(DbOptions, queryBuilder) : new PostgresDataProvider(DbOptions, queryBuilder); + Provider = custom + ? new PostgresDataProvider(DbOptions, new PostgresQueryBuilder()) + : new PostgresDataProvider(DbOptions, new PostgresQueryBuilder()); return Task.CompletedTask; } From f362db4d62d3d7eee7185c03f46b0757b68c679b Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 22 Sep 2024 22:19:11 +0200 Subject: [PATCH 05/13] Move the log model. --- .../{ => Models}/SqlServerLogModel.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Serilog.Ui.MsSqlServerProvider/{ => Models}/SqlServerLogModel.cs (100%) diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerLogModel.cs b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerLogModel.cs similarity index 100% rename from src/Serilog.Ui.MsSqlServerProvider/SqlServerLogModel.cs rename to src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerLogModel.cs From 97da02604f4fe049905a096eeb78c238c1f6320e Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Mon, 23 Sep 2024 23:17:43 +0200 Subject: [PATCH 06/13] Add query builder for MySql. --- .../Extensions/SqlServerDbOptions.cs | 2 +- .../Extensions/MariaDbOptions.cs | 11 ++ .../Extensions/MySqlDbOptions.cs | 10 ++ .../SerilogUiOptionBuilderExtensions.cs | 123 ++++++++--------- .../MariaDbDataProvider.cs | 17 +-- .../Models/MariaDbSinkColumnNames.cs | 16 +++ .../Models/MySqlSinkColumnNames.cs | 16 +++ .../MySqlDataProvider.cs | 13 +- src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs | 33 +++-- .../MySqlQueryBuilder.cs | 126 ++++++++++++++++++ .../Serilog.Ui.MySqlProvider.csproj | 1 + .../Shared/DataProvider.cs | 109 +++------------ .../DataProvider/DataProviderBaseTest.cs | 1 + ...erilog.Ui.MsSqlServerProvider.Tests.csproj | 2 +- .../MariaDb/DataProviderBaseTest.cs | 51 ++++--- .../DataProvider/MariaDb/QueryBuilderTests.cs | 106 +++++++++++++++ .../MySql/DataProviderBaseTest.cs | 41 +++--- .../DataProvider/MySql/QueryBuilderTests.cs | 84 ++++++++++++ .../Serilog.Ui.MySqlProvider.Tests.csproj | 2 +- .../Util/MariaDbTestProvider.cs | 12 +- .../Util/MySqlTestProvider.cs | 9 +- .../DataProvider/DataProviderBaseTest.cs | 2 + ...Serilog.Ui.PostgreSqlProvider.Tests.csproj | 7 +- .../Util/PostgresTestProvider.cs | 2 +- 24 files changed, 543 insertions(+), 253 deletions(-) create mode 100644 src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs create mode 100644 src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs create mode 100644 src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs create mode 100644 src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs create mode 100644 src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs create mode 100644 tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs create mode 100644 tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/QueryBuilderTests.cs diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs index 34ae5166..2e221ec4 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs +++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs @@ -6,5 +6,5 @@ namespace Serilog.Ui.MsSqlServerProvider.Extensions; public class SqlServerDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) { - internal SinkColumnNames ColumnNames { get; } = new SqlServerSinkColumnNames(); + public SinkColumnNames ColumnNames { get; } = new SqlServerSinkColumnNames(); } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs new file mode 100644 index 00000000..1f3be6c0 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs @@ -0,0 +1,11 @@ +using Serilog.Ui.MySqlProvider.Models; + +namespace Serilog.Ui.MySqlProvider.Extensions; + +public class MariaDbOptions : MySqlDbOptions +{ + public MariaDbOptions(string defaultSchemaName) : base(defaultSchemaName) + { + ColumnNames = new MariaDbSinkColumnNames(); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs new file mode 100644 index 00000000..f1e07950 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.MySqlProvider.Models; + +namespace Serilog.Ui.MySqlProvider.Extensions; + +public class MySqlDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) +{ + internal SinkColumnNames ColumnNames { get; set; } = new MySqlSinkColumnNames(); +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 16673cc5..1348591f 100644 --- a/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,84 +1,77 @@ -using System; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Core.Models.Options; +using System; -namespace Serilog.Ui.MySqlProvider.Extensions +namespace Serilog.Ui.MySqlProvider.Extensions; + +/// +/// MySQL data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions { /// - /// MySQL data provider specific extension methods for . + /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting + /// Serilog.Sinks.MySQL. defaults. + /// Provider expects sink to store timestamp in utc. /// - public static class SerilogUiOptionBuilderExtensions + /// The options builder. + /// The MySql options action. + public static ISerilogUiOptionsBuilder UseMySqlServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) { - /// - /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting - /// Serilog.Sinks.MySQL. defaults. - /// Provider expects sink to store timestamp in utc. - /// - /// The options builder. - /// The MySql options action. - public static ISerilogUiOptionsBuilder UseMySqlServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) - { - var dbOptions = new RelationalDbOptions("dbo"); - setupOptions(dbOptions); - dbOptions.Validate(); - - var providerName = dbOptions.GetProviderName(MySqlDataProvider.MySqlProviderName); - - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + MySqlDbOptions dbOptions = new("dbo"); + setupOptions(dbOptions); + dbOptions.Validate(); - optionsBuilder.Services.AddScoped(_ => new MySqlDataProvider(dbOptions)); + string providerName = dbOptions.GetProviderName(MySqlDataProvider.MySqlProviderName); - return optionsBuilder; - } + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + optionsBuilder.Services.AddScoped(_ => new MySqlDataProvider(dbOptions, new MySqlQueryBuilder())); - /// - /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting - /// Serilog.Sinks.MariaDB. defaults. - /// Provider expects sink to store timestamp in utc. - /// - /// The options builder. - /// The MySql options action. - public static ISerilogUiOptionsBuilder UseMariaDbServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) => optionsBuilder.UseMariaDbServer(setupOptions); + return optionsBuilder; + } - /// - /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting - /// Serilog.Sinks.MariaDB. defaults. - /// Provider expects sink to store timestamp in utc. - /// - /// The log model, containing any additional columns. It must inherit . - /// The options builder. - /// The MySql options action. - public static ISerilogUiOptionsBuilder UseMariaDbServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) where T : MySqlLogModel - { - var dbOptions = new RelationalDbOptions("dbo"); - setupOptions(dbOptions); - dbOptions.Validate(); + /// + /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting + /// Serilog.Sinks.MariaDB. defaults. + /// Provider expects sink to store timestamp in utc. + /// + /// The options builder. + /// The MySql options action. + public static ISerilogUiOptionsBuilder UseMariaDbServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + => optionsBuilder.UseMariaDbServer(setupOptions); - var providerName = dbOptions.GetProviderName(MariaDbDataProvider.ProviderName); + /// + /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting + /// Serilog.Sinks.MariaDB. defaults. + /// Provider expects sink to store timestamp in utc. + /// + /// The log model, containing any additional columns. It must inherit . + /// The options builder. + /// The MySql options action. + public static ISerilogUiOptionsBuilder UseMariaDbServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + where T : MySqlLogModel + { + MariaDbOptions dbOptions = new("dbo"); + setupOptions(dbOptions); + dbOptions.Validate(); - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + string providerName = dbOptions.GetProviderName(MariaDbDataProvider.ProviderName); - var customModel = typeof(T) != typeof(MySqlLogModel); - if (customModel) - { - optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions)); - return optionsBuilder; - } + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions)); - return optionsBuilder; + bool customModel = typeof(T) != typeof(MySqlLogModel); + if (customModel) + { + optionsBuilder.RegisterColumnsInfo(providerName); + optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions, new MySqlQueryBuilder())); + } + else + { + optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions, new MySqlQueryBuilder())); } + + return optionsBuilder; } } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs b/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs index 92bb825f..37fe6af3 100644 --- a/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs +++ b/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs @@ -1,23 +1,16 @@ -using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.MySqlProvider.Extensions; using Serilog.Ui.MySqlProvider.Shared; namespace Serilog.Ui.MySqlProvider; -public class MariaDbDataProvider(RelationalDbOptions options) : MariaDbDataProvider(options) -{ - protected override string SelectQuery - => $"SELECT Id, {ColumnMessageName}, {ColumnLevelName} AS 'Level', {ColumnTimestampName}, Exception, Properties "; - - protected override string SearchCriteriaWhereQuery() => "OR Exception LIKE @Search"; -} +public class MariaDbDataProvider(MariaDbOptions options, MySqlQueryBuilder queryBuilder) + : MariaDbDataProvider(options, queryBuilder); -public class MariaDbDataProvider(RelationalDbOptions options) : DataProvider(options) +public class MariaDbDataProvider(MariaDbOptions options, MySqlQueryBuilder queryBuilder) : DataProvider(options, queryBuilder) where T : MySqlLogModel { internal const string ProviderName = "MariaDb"; - protected override string ColumnLevelName => "LogLevel"; - - public override string Name => Options.GetProviderName(ProviderName); + public override string Name => options.GetProviderName(ProviderName); } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs b/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs new file mode 100644 index 00000000..723a6f0a --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.MySqlProvider.Models; + +internal class MariaDbSinkColumnNames : SinkColumnNames +{ + public MariaDbSinkColumnNames() + { + Exception = "Exception"; + Level = "LogLevel"; + LogEventSerialized = "Properties"; + Message = "Message"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs new file mode 100644 index 00000000..67c6888a --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.MySqlProvider.Models; + +internal class MySqlSinkColumnNames : SinkColumnNames +{ + public MySqlSinkColumnNames() + { + Exception = "Exception"; + Level = "Level"; + LogEventSerialized = "Properties"; + Message = "Message"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs b/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs index 6bdd363e..b11e8dcb 100644 --- a/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs +++ b/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs @@ -1,15 +1,14 @@ -using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.MySqlProvider.Extensions; using Serilog.Ui.MySqlProvider.Shared; namespace Serilog.Ui.MySqlProvider; -public class MySqlDataProvider(RelationalDbOptions options) : DataProvider(options) +public class MySqlDataProvider(MySqlDbOptions options, MySqlQueryBuilder queryBuilder) + : DataProvider(options, queryBuilder) { - protected override string SelectQuery - => $"SELECT Id, {ColumnMessageName}, {ColumnLevelName}, {ColumnTimestampName}, Exception, Properties "; - - protected override string SearchCriteriaWhereQuery() => "OR Exception LIKE @Search"; + private readonly MySqlDbOptions _options = options; internal const string MySqlProviderName = "MySQL"; - public override string Name => Options.GetProviderName(MySqlProviderName); + + public override string Name => _options.GetProviderName(MySqlProviderName); } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs index e1358aaa..a79802d6 100644 --- a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs +++ b/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs @@ -2,27 +2,26 @@ using Serilog.Ui.Core.Attributes; using Serilog.Ui.Core.Models; -namespace Serilog.Ui.MySqlProvider +namespace Serilog.Ui.MySqlProvider; + +/// +/// MySql/MariaDb Log Model.
+/// , , , +/// columns can't be overridden and removed from the model, due to query requirements.
+/// To remove a field, apply on it. +/// To add a field, register the property with the correct datatype on the child class and the sink. +///
+public class MySqlLogModel : LogModel { - /// - /// MySql/MariaDb Log Model.
- /// , , , - /// columns can't be overridden and removed from the model, due to query requirements.
- /// To remove a field, apply on it. - /// To add a field, register the property with the correct datatype on the child class and the sink. - ///
- public class MySqlLogModel : LogModel - { - public sealed override int RowNo => base.RowNo; + public override sealed int RowNo => base.RowNo; - public sealed override string? Level { get; set; } + public override sealed string? Level { get; set; } - public string LogLevel { get; set; } = string.Empty; + public string LogLevel { get; set; } = string.Empty; - public sealed override string? Message { get; set; } = string.Empty; + public override sealed string? Message { get; set; } = string.Empty; - public sealed override DateTime Timestamp { get; set; } + public override sealed DateTime Timestamp { get; set; } - public override string PropertyType => "json"; - } + public override string PropertyType => "json"; } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs b/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs new file mode 100644 index 00000000..1d99e7b6 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs @@ -0,0 +1,126 @@ +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using System; +using System.Text; + +namespace Serilog.Ui.MySqlProvider; + +/// +/// Provides methods to build SQL queries specifically for MySQL and MariaDB to fetch and count logs. +/// +/// The type of the log model. +public class MySqlQueryBuilder : SqlQueryBuilder where TModel : LogModel +{ + /// + 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(); + } + + /// + /// Builds a SQL query to count logs in the specified table. + /// + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append("SELECT COUNT(Id) ") + .Append($"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()}"; + + /// + /// Generates the SELECT clause for the SQL query. + /// + /// The StringBuilder to append the SELECT clause to. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + { + if (typeof(TModel) != typeof(MySqlLogModel)) + { + queryBuilder.Append("SELECT * "); + } + else + { + queryBuilder.Append("SELECT Id, ") + .Append($"{columns.Message}, ") + .Append($"{columns.Level}, ") + .Append($"{columns.Timestamp}, ") + .Append($"{columns.Exception}, ") + .Append($"{columns.LogEventSerialized} "); + } + + queryBuilder.Append($"FROM {tableName} "); + } + + /// + /// Generates the WHERE clause for the SQL query. + /// + /// The StringBuilder to append the WHERE clause to. + /// The column names used in the sink for logging. + /// The log level to filter by. + /// The search criteria to filter by. + /// The start date to filter by. + /// The end date to filter by. + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + StringBuilder conditions2 = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions2.Append($"AND {columns.Level} = @Level "); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + conditions2.Append($"AND ({columns.Message} LIKE @Search "); + conditions2.Append(AddExceptionToWhereClause() ? $"OR {columns.Exception} LIKE @Search) " : ") "); + } + + if (startDate.HasValue) + { + conditions2.Append($"AND {columns.Timestamp} >= @StartDate "); + } + + if (endDate.HasValue) + { + conditions2.Append($"AND {columns.Timestamp} <= @EndDate "); + } + + if (conditions2.Length <= 0) + { + return; + } + + queryBuilder + .Append("WHERE TRUE ") + .Append(conditions2); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj b/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj index 062fc7a1..85f3e8ac 100644 --- a/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj +++ b/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs b/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs index 5a603b03..24c8ccb8 100644 --- a/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs +++ b/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs @@ -1,29 +1,20 @@ -using System; +using Dapper; +using MySqlConnector; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MySqlProvider.Extensions; +using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Dapper; -using MySqlConnector; -using Serilog.Ui.Core; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; namespace Serilog.Ui.MySqlProvider.Shared; -public abstract class DataProvider(RelationalDbOptions options) : IDataProvider +public abstract class DataProvider(MySqlDbOptions options, MySqlQueryBuilder queryBuilder) : IDataProvider where T : MySqlLogModel { - protected virtual string ColumnTimestampName => "TimeStamp"; - - protected virtual string ColumnLevelName => "Level"; - - protected virtual string ColumnMessageName => "Message"; - - protected readonly RelationalDbOptions Options = options ?? throw new ArgumentNullException(nameof(options)); + public abstract string Name { get; } public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) { @@ -31,30 +22,19 @@ public abstract class DataProvider(RelationalDbOptions options) : IDataProvid var logsTask = GetLogsAsync(queryParams); var logCountTask = CountLogsAsync(queryParams); - - await Task.WhenAll(logsTask, logCountTask); + await Task.WhenAll(logsTask); return (await logsTask, await logCountTask); } - public abstract string Name { get; } - - protected virtual string SelectQuery => "SELECT * "; - private async Task> GetLogsAsync(FetchLogsQuery queryParams) { - var queryBuilder = new StringBuilder(); - queryBuilder.Append(SelectQuery).Append($"FROM `{Options.TableName}` "); - - GenerateWhereClause(queryBuilder, queryParams); - var sortClause = GenerateSortClause(queryParams.SortOn, queryParams.SortBy); + string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); + int rowNoStart = queryParams.Page * queryParams.Count; - queryBuilder.Append($"ORDER BY {sortClause} LIMIT @Offset, @Count"); + using MySqlConnection connection = new(options.ConnectionString); - var rowNoStart = queryParams.Page * queryParams.Count; - - using var connection = new MySqlConnection(Options.ConnectionString); - var param = new + IEnumerable logs = await connection.QueryAsync(query, new { Offset = rowNoStart, queryParams.Count, @@ -62,18 +42,15 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, queryParams.StartDate, queryParams.EndDate - }; - - var logs = await connection.QueryAsync(queryBuilder.ToString(), param); + }); return logs .Select((item, i) => { item.SetRowNo(rowNoStart, i); item.Level ??= item.LogLevel; - // 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.Utc : item.Timestamp.Kind); + // 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.Utc : item.Timestamp.Kind); item.Timestamp = ts.ToUniversalTime(); return item; }) @@ -82,13 +59,11 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam private async Task CountLogsAsync(FetchLogsQuery queryParams) { - var queryBuilder = new StringBuilder(); - queryBuilder.Append($"SELECT COUNT(Id) FROM `{Options.TableName}` "); + string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); - GenerateWhereClause(queryBuilder, queryParams); + using MySqlConnection connection = new(options.ConnectionString); - using var connection = new MySqlConnection(Options.ConnectionString); - return await connection.ExecuteScalarAsync(queryBuilder.ToString(), + return await connection.ExecuteScalarAsync(query, new { queryParams.Level, @@ -97,50 +72,4 @@ private async Task CountLogsAsync(FetchLogsQuery queryParams) queryParams.EndDate }); } - - /// - /// If Exception property is flagged with , - /// it removes the Where query part on the Exception field. - /// - /// - protected virtual string SearchCriteriaWhereQuery() - { - var exceptionProperty = typeof(T).GetProperty(nameof(MySqlLogModel.Exception)); - var att = exceptionProperty?.GetCustomAttribute(); - return att is null ? "OR Exception LIKE @Search" : string.Empty; - } - - private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams) - { - var conditionStart = "WHERE"; - - if (!string.IsNullOrWhiteSpace(queryParams.Level)) - { - queryBuilder.Append($"WHERE {ColumnLevelName} = @Level "); - conditionStart = "AND"; - } - - if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria)) - { - queryBuilder.Append($"{conditionStart} ({ColumnMessageName} LIKE @Search {SearchCriteriaWhereQuery()}) "); - conditionStart = "AND"; - } - - if (queryParams.StartDate != null) - { - queryBuilder.Append($"{conditionStart} {ColumnTimestampName} >= @StartDate "); - conditionStart = "AND"; - } - - if (queryParams.EndDate != null) - { - queryBuilder.Append($"{conditionStart} {ColumnTimestampName} <= @EndDate "); - } - } - - private string GenerateSortClause(SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) - { - var sortProperty = sortOn == SearchOptions.SortProperty.Level ? ColumnLevelName : sortOn.ToString(); - return $"{sortProperty} {sortBy.ToString().ToUpper()}"; - } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs index a340fb91..6278d88b 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -16,6 +16,7 @@ namespace MsSql.Tests.DataProvider; [Trait("Unit-Base", "MsSql")] public class DataProviderBaseTest : IUnitBaseTests { + [Fact(Skip = "Not required")] public void It_throws_when_any_dependency_is_null() => throw new NotImplementedException(); diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj index 4025efe8..854d0bb6 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj @@ -1,6 +1,6 @@  - + net8.0 MsSql.Tests MsSql.Tests diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs index 699340dd..e7de7a5a 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs @@ -7,40 +7,39 @@ using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Xunit; -namespace MySql.Tests.DataProvider.MariaDb +namespace MySql.Tests.DataProvider.MariaDb; + +[Trait("Unit-Base", "MariaDb")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "MariaDb")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact(Skip = "Not required")] + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public async Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var suts = new List - { - () => { _ = new MariaDbDataProvider(null!); }, - () => { _ = new MariaDbDataProvider(null!); }, - }; + // Arrange + MariaDbDataProvider sut = new( + new MariaDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new MySqlQueryBuilder()); - suts.ForEach(sut => sut.Should().ThrowExactly()); - } + MariaDbDataProvider sutWithCols = new( + new MariaDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new MySqlQueryBuilder()); - [Fact] - public async Task It_logs_and_throws_when_db_read_breaks_down() - { - var sut = new MariaDbDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var sutWithCols = - new MariaDbDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var query = new Dictionary { ["page"] = "1", ["count"] = "10", }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithAdditionalCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - await assert.Should().ThrowExactlyAsync(); - await assertWithAdditionalCols.Should().ThrowExactlyAsync(); - } + // Assert + await assert.Should().ThrowExactlyAsync(); + await assertWithCols.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs new file mode 100644 index 00000000..f780be59 --- /dev/null +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using MySql.Tests.Util; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Models; +using Xunit; + +namespace MySql.Tests.DataProvider.MariaDb; + +[Trait("Unit-QueryBuilder", "MariaDb")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForMariaDblSink_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") + }; + + MariaDbSinkColumnNames sinkColumns = new(); + MySqlQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + [Fact] + public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = "level", + ["search"] = "criteria" + }; + + MySqlSinkColumnNames sinkColumns = new(); + MySqlQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.ToLowerInvariant().Should().StartWith("select"); + query.ToLowerInvariant().Should().NotContain("exception"); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + "dbo", "logs", null!, null!, null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, null!, DateTime.Now, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp >= @StartDate AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", null!, null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND LogLevel = @Level ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, "Test", null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND LogLevel = @Level AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND LogLevel = @Level AND (Message 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 diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs index 595bffb9..7f4fb759 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs @@ -6,34 +6,33 @@ using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Xunit; -namespace MySql.Tests.DataProvider.MySql +namespace MySql.Tests.DataProvider.MySql; + +[Trait("Unit-Base", "MySql")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "MySql")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact(Skip = "Not required")] + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var suts = new List> - { - () => new MySqlDataProvider(null!), - }; + // Arrange + MySqlDataProvider sut = new( + new MySqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new MySqlQueryBuilder()); - suts.ForEach(sut => sut.Should().ThrowExactly()); - } + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - [Fact] - public Task It_logs_and_throws_when_db_read_breaks_down() - { - var sut = new MySqlDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var query = new Dictionary { ["page"] = "1", ["count"] = "10", }; - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - return assert.Should().ThrowExactlyAsync(); - } + // Assert + return assert.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/QueryBuilderTests.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/QueryBuilderTests.cs new file mode 100644 index 00000000..46a8d3d3 --- /dev/null +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/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.MySqlProvider; +using Serilog.Ui.MySqlProvider.Models; +using Xunit; + +namespace MySql.Tests.DataProvider.MySql; + +[Trait("Unit-QueryBuilder", "MySql")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForMySqlSink_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") + }; + + MySqlSinkColumnNames sinkColumns = new(); + MySqlQueryBuilder 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 = + [ + [ + "dbo", "logs", null!, null!, null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, null!, DateTime.Now, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp >= @StartDate AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", null!, null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND Level = @Level ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, "Test", null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND Level = @Level AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND Level = @Level AND (Message 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 diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj index 289a2174..58d56060 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj @@ -1,6 +1,6 @@  - + net8.0 MySql.Tests MySql.Tests diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs index da3f95e6..7a66fff0 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs @@ -9,8 +9,8 @@ using Serilog.Ui.Common.Tests.DataSamples; using Serilog.Ui.Common.Tests.SqlUtil; using Serilog.Ui.Core.Extensions; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Testcontainers.MariaDb; using Xunit; @@ -29,9 +29,10 @@ public class MariaDbTestProvider : DatabaseInstance protected MariaDbTestProvider() { Container = new MariaDbBuilder().Build(); + DbOptions = new MariaDbOptions("dbo").WithTable("Logs"); } - private RelationalDbOptions DbOptions { get; set; } = new RelationalDbOptions("dbo").WithTable("Logs"); + private MariaDbOptions DbOptions { get; } protected override sealed IContainer Container { get; set; } @@ -50,7 +51,7 @@ protected override async Task CheckDbReadinessAsync() protected override Task InitializeAdditionalAsync() { - var serilog = new SerilogSinkSetup(logger => + SerilogSinkSetup serilog = new(logger => { logger .WriteTo.MariaDB( @@ -62,10 +63,13 @@ protected override Task InitializeAdditionalAsync() PropertiesToColumnsMapping = PropertiesToColumnsMapping }); }); + Collector = serilog.InitializeLogs(); var custom = typeof(T) != typeof(MySqlLogModel); - Provider = custom ? new MariaDbDataProvider(DbOptions) : new MariaDbDataProvider(DbOptions); + Provider = custom + ? new MariaDbDataProvider(DbOptions, new MySqlQueryBuilder()) + : new MariaDbDataProvider(DbOptions, new MySqlQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs index 647dfad1..2cb9bfdb 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs @@ -8,6 +8,7 @@ using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Testcontainers.MySql; using Xunit; @@ -25,9 +26,10 @@ public sealed class MySqlTestProvider : DatabaseInstance public MySqlTestProvider() { Container = new MySqlBuilder().Build(); + DbOptions = new MySqlDbOptions("dbo").WithTable("Logs"); } - public RelationalDbOptions DbOptions { get; set; } = new RelationalDbOptions("dbo").WithTable("Logs"); + public MySqlDbOptions DbOptions { get; } protected override async Task CheckDbReadinessAsync() { @@ -42,13 +44,14 @@ protected override async Task CheckDbReadinessAsync() protected override Task InitializeAdditionalAsync() { - var serilog = new SerilogSinkSetup(logger => + SerilogSinkSetup serilog = new(logger => { logger.WriteTo.MySQL(DbOptions.ConnectionString, batchSize: 1, storeTimestampInUtc: true); }); + Collector = serilog.InitializeLogs(); - Provider = new MySqlDataProvider(DbOptions); + Provider = new MySqlDataProvider(DbOptions, new MySqlQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs index c199f384..aa679ead 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -17,9 +17,11 @@ namespace Postgres.Tests.DataProvider; [Trait("Unit-Base", "Postgres")] public class DataProviderBaseTest : IUnitBaseTests { + [Fact(Skip = "Not required")] public void It_throws_when_any_dependency_is_null() => throw new NotImplementedException(); + [Fact] public async Task It_logs_and_throws_when_db_read_breaks_down() { // Arrange diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj index 794b33f9..73dc28cc 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj @@ -1,8 +1,8 @@  - - Postgres.Tests - Postgres.Tests + net8.0 + Postgres.Tests + Postgres.Tests @@ -22,5 +22,4 @@ - diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs index 37481eaf..87325b8c 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs @@ -50,7 +50,7 @@ protected override async Task CheckDbReadinessAsync() protected override Task InitializeAdditionalAsync() { - var serilog = new SerilogSinkSetup(logger => + SerilogSinkSetup serilog = new(logger => { logger .WriteTo.PostgreSQL( From f0c736cb78da729901c45609f6d8f97454c808e6 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Mon, 23 Sep 2024 23:19:01 +0200 Subject: [PATCH 07/13] Move log model. --- src/Serilog.Ui.MySqlProvider/{ => Models}/MySqlLogModel.cs | 0 .../DataProvider/QueryBuilderTests.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Serilog.Ui.MySqlProvider/{ => Models}/MySqlLogModel.cs (100%) diff --git a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs similarity index 100% rename from src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs rename to src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs index 9ed2784e..d4fb3af0 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -16,7 +16,7 @@ public class QueryBuilderTests { [Theory] [ClassData(typeof(QueryBuilderTestData))] - public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( + public void BuildFetchLogsQuery_ForSink_ReturnsCorrectQuery( string schema, string tableName, string level, From b4067c97c1adedb72da01fdae7fc2df96b203b45 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Mon, 23 Sep 2024 23:19:14 +0200 Subject: [PATCH 08/13] Revert. --- .../Serilog.Ui.MsSqlServerProvider.Tests.csproj | 1 - .../Serilog.Ui.MySqlProvider.Tests.csproj | 1 - .../Serilog.Ui.PostgreSqlProvider.Tests.csproj | 1 - 3 files changed, 3 deletions(-) diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj index 854d0bb6..704289ee 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj @@ -1,6 +1,5 @@  - net8.0 MsSql.Tests MsSql.Tests diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj index 58d56060..53b3126f 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj @@ -1,6 +1,5 @@  - net8.0 MySql.Tests MySql.Tests diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj index 73dc28cc..756b74c3 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj @@ -1,6 +1,5 @@  - net8.0 Postgres.Tests Postgres.Tests From 63a140aca3476c5c8a68fac40ca0d8be27ae8f57 Mon Sep 17 00:00:00 2001 From: mo-esmp Date: Fri, 27 Sep 2024 21:25:59 +0200 Subject: [PATCH 09/13] Change SQL Server docker image to 2019. --- .../Util/MsSqlServerTestProvider.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs index 04fc5e7b..45c52a80 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Dapper; +using Dapper; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using Microsoft.Data.SqlClient; @@ -10,6 +9,7 @@ using Serilog.Ui.Core.Extensions; using Serilog.Ui.MsSqlServerProvider; using Serilog.Ui.MsSqlServerProvider.Extensions; +using System.Threading.Tasks; using Testcontainers.MsSql; using Xunit; @@ -32,6 +32,7 @@ protected MsSqlServerTestProvider() .ForUnixContainer() .UntilCommandIsCompleted("/opt/mssql-tools18/bin/sqlcmd", "-C", "-Q", "SELECT 1;"); Container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2019-lates") .WithWaitStrategy(waitStrategy) .Build(); } From fbcd5045f1ad1f69b18babbc9167acc04050e4be Mon Sep 17 00:00:00 2001 From: mo-esmp Date: Fri, 27 Sep 2024 22:24:39 +0200 Subject: [PATCH 10/13] Revert "Change SQL Server docker image to 2019." This reverts commit 63a140aca3476c5c8a68fac40ca0d8be27ae8f57. --- .../Util/MsSqlServerTestProvider.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs index 45c52a80..04fc5e7b 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs @@ -1,4 +1,5 @@ -using Dapper; +using System.Threading.Tasks; +using Dapper; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using Microsoft.Data.SqlClient; @@ -9,7 +10,6 @@ using Serilog.Ui.Core.Extensions; using Serilog.Ui.MsSqlServerProvider; using Serilog.Ui.MsSqlServerProvider.Extensions; -using System.Threading.Tasks; using Testcontainers.MsSql; using Xunit; @@ -32,7 +32,6 @@ protected MsSqlServerTestProvider() .ForUnixContainer() .UntilCommandIsCompleted("/opt/mssql-tools18/bin/sqlcmd", "-C", "-Q", "SELECT 1;"); Container = new MsSqlBuilder() - .WithImage("mcr.microsoft.com/mssql/server:2019-lates") .WithWaitStrategy(waitStrategy) .Build(); } From 573642523120f8e157ea97f1430e4e74cc388454 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sun, 29 Sep 2024 22:01:16 +0200 Subject: [PATCH 11/13] fix: sql server querying --- .../SqlServerQueryBuilder.cs | 4 ++-- .../DataProvider/QueryBuilderTests.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs index 061180b2..1bf038d5 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs +++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs @@ -20,7 +20,7 @@ public override string BuildFetchLogsQuery(SinkColumnNames columns, string schem GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); - queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Count OFFSET @Offset"); + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY"); return queryStr.ToString(); } @@ -120,7 +120,7 @@ private static void GenerateWhereClause( } queryBuilder - .Append("WHERE TRUE ") + .Append("WHERE 1 = 1 ") .Append(conditions2); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs index d4fb3af0..1c3ce788 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -71,31 +71,31 @@ public class QueryBuilderTestData : IEnumerable [ [ "dbo", "logs", null!, null!, null!, null!, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ], [ "dbo", "logs", null!, null!, null!, DateTime.Now, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ], [ "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ], [ "dbo", "logs", "Information", null!, null!, null!, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [Level] = @Level ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [Level] = @Level ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ], [ "dbo", "logs", null!, "Test", null!, null!, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ], [ "dbo", "logs", "Information", "Test", null!, null!, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ], [ "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, - "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE TRUE AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC LIMIT @Count OFFSET @Offset" + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" ] ]; From 0e91a8d56d365e168a137d90f5576a5c208e4cd0 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sun, 29 Sep 2024 22:10:48 +0200 Subject: [PATCH 12/13] force version --- .../Util/MsSqlServerTestProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs index 04fc5e7b..9ba7ec99 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs @@ -32,6 +32,7 @@ protected MsSqlServerTestProvider() .ForUnixContainer() .UntilCommandIsCompleted("/opt/mssql-tools18/bin/sqlcmd", "-C", "-Q", "SELECT 1;"); Container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") .WithWaitStrategy(waitStrategy) .Build(); } From d89bbd7cc8da1e496d6f73e0b3dd8c228916b6b5 Mon Sep 17 00:00:00 2001 From: Matteo Gregoricchio Date: Sun, 29 Sep 2024 23:05:28 +0200 Subject: [PATCH 13/13] revert back mongodb sink due to test failures on net6 --- .../Serilog.Ui.MongoDbProvider.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj b/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj index 6b181e90..0564f22b 100644 --- a/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj +++ b/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj @@ -10,7 +10,7 @@ - +