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.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..b9b112b3 --- /dev/null +++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs @@ -0,0 +1,67 @@ +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; + +/// +/// Abstract class that provides methods to build SQL queries for fetching and counting logs. +/// +public abstract class SqlQueryBuilder where TModel : LogModel +{ + /// + /// Builds a SQL query to fetch logs from 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 fetching logs. + /// A SQL query string to fetch logs. + public abstract string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query); + + /// + /// 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 abstract string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query); + + /// + /// 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 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() + { + PropertyInfo? exceptionProperty = typeof(TModel).GetProperty("Exception"); + RemovedColumnAttribute? att = exceptionProperty?.GetCustomAttribute(); + + return att is null; + } +} \ 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 cf49d99e..dbba811c 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 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..2e221ec4 --- /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) +{ + public SinkColumnNames ColumnNames { get; } = new SqlServerSinkColumnNames(); +} \ No newline at end of file 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 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 ff8363bd..c01d0388 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj +++ b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj @@ -5,7 +5,7 @@ netstandard2.0 latest - 3.0.0 + 3.1.0 Microsoft SQL Server data provider for Serilog UI. serilog serilog-ui serilog.sinks.mssqlserver mssqlserver @@ -18,5 +18,6 @@ + \ 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..1bf038d5 --- /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)} OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY"); + + 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 1 = 1 ") + .Append(conditions2); + } +} \ 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/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs new file mode 100644 index 00000000..a79802d6 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs @@ -0,0 +1,27 @@ +using System; +using Serilog.Ui.Core.Attributes; +using Serilog.Ui.Core.Models; + +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 +{ + public override sealed int RowNo => base.RowNo; + + public override sealed string? Level { get; set; } + + public string LogLevel { get; set; } = string.Empty; + + public override sealed string? Message { get; set; } = string.Empty; + + public override sealed DateTime Timestamp { get; set; } + + public override string PropertyType => "json"; +} \ 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 deleted file mode 100644 index e1358aaa..00000000 --- a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; - -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 - { - public sealed override int RowNo => base.RowNo; - - public sealed override string? Level { get; set; } - - public string LogLevel { get; set; } = string.Empty; - - public sealed override string? Message { get; set; } = string.Empty; - - public sealed override DateTime Timestamp { get; set; } - - 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 209595e7..05fc1b6e 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/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs index 06cea7c4..0ff48ee9 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 @@ -31,6 +31,9 @@ public PostgreSqlDbOptions WithSinkType(PostgreSqlSinkType sinkType) ColumnNames = sinkType == PostgreSqlSinkType.SerilogSinksPostgreSQLAlternative ? new PostgreSqlAlternativeSinkColumnNames() : 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..8ba23502 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,56 +1,51 @@ -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 +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)); + /// + /// 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)); - 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/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/PostgresDataProvider.cs similarity index 67% rename from src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs rename to src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs index fc76c115..3a2d44be 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs @@ -1,38 +1,36 @@ -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.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)); - /// - 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) : IDataProvide private async Task> GetLogsAsync(FetchLogsQuery queryParams) { - var query = options.ColumnNames.BuildFetchLogsQuery(_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 = options.ColumnNames.BuildCountLogsQuery(_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 new file mode 100644 index 00000000..675fe49a --- /dev/null +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs @@ -0,0 +1,122 @@ +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.PostgreSqlProvider.Models; +using System; +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. +/// +/// The type of the log model. +public class PostgresQueryBuilder : 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(); + } + + /// + 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(); + } + + /// + 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 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(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 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 conditions = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions.Append($"AND \"{columns.Level}\" = @Level "); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + conditions.Append($"AND (\"{columns.Message}\" LIKE @Search "); + conditions.Append(AddExceptionToWhereClause() ? $"OR \"{columns.Exception}\" LIKE @Search) " : ") "); + } + + if (startDate.HasValue) + { + conditions.Append($"AND \"{columns.Timestamp}\" >= @StartDate "); + } + + if (endDate.HasValue) + { + conditions.Append($"AND \"{columns.Timestamp}\" <= @EndDate "); + } + + if (conditions.Length <= 0) + { + return; + } + + queryBuilder + .Append("WHERE TRUE ") + .Append(conditions); + } +} \ 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 9aaffd0f..47a97a7a 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj +++ b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj @@ -4,10 +4,8 @@ Serilog.UI.PostgreSqlProvider netstandard2.0 latest - 3.0.0 - + 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 a8905db6..ce1ab57f 100644 --- a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj +++ b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj @@ -7,12 +7,12 @@ 3.0.2 - - - + + + - + 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 @@ - + diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs index 34ab68d9..6278d88b 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -7,42 +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.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 + [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 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..1c3ce788 --- /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_ForSink_ReturnsCorrectQuery( + string schema, + string tableName, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate, + string expectedQuery) + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = level, + ["search"] = searchCriteria, + ["startDate"] = startDate?.ToString("O"), + ["endDate"] = endDate?.ToString("O") + }; + + 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 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 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 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 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 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 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 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" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file 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 18a51528..ac334f2b 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj @@ -1,5 +1,4 @@  - MsSql.Tests MsSql.Tests diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs index 80c1c6d1..9ba7ec99 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs @@ -8,8 +8,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; @@ -32,11 +32,12 @@ 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(); } - 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; } @@ -69,8 +70,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.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 8f0197aa..ee69e498 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj @@ -1,5 +1,4 @@  - 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 cd2b0bce..aa679ead 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -9,41 +9,38 @@ 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 +namespace Postgres.Tests.DataProvider; + +[Trait("Unit-Base", "Postgres")] +public class DataProviderBaseTest : IUnitBaseTests { - [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() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var sut = new List - { - () => { _ = new PostgresDataProvider(null!); }, - () => { _ = new PostgresDataProvider(null!); }, - }; + // Arrange + PostgresDataProvider sut = new( + new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new PostgresQueryBuilder()); - sut.ForEach(s => s.Should().ThrowExactly()); - } + PostgresDataProvider sutWithCols = new( + new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new PostgresQueryBuilder()); - [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" }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithCols = () => 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 assertWithCols.Should().ThrowExactlyAsync(); - } + // 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..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 PostgreSqlAlternativeSinkColumnNames _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, @@ -36,8 +34,11 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( ["endDate"] = endDate?.ToString("O") }; + PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + PostgresQueryBuilder sut = new(); + // Act - var query = _sut.BuildFetchLogsQuery(schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); // Assert query.Should().Be(expectedQuery); @@ -47,17 +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("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/Serilog.Ui.PostgreSqlProvider.Tests.csproj b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj index 9f1db41b..c5e708c9 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,7 @@  - - Postgres.Tests - Postgres.Tests + Postgres.Tests + Postgres.Tests @@ -22,5 +21,4 @@ - diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs index 6bd8c3da..87325b8c 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); @@ -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( @@ -66,7 +66,9 @@ protected override Task InitializeAdditionalAsync() Collector = serilog.InitializeLogs(); var custom = typeof(T) != typeof(PostgresLogModel); - Provider = custom ? new PostgresDataProvider(DbOptions) : new PostgresDataProvider(DbOptions); + Provider = custom + ? new PostgresDataProvider(DbOptions, new PostgresQueryBuilder()) + : new PostgresDataProvider(DbOptions, new PostgresQueryBuilder()); return Task.CompletedTask; }