-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(provider): add SQLite sink support (#124)
* Add Sqlite Provider Co-authored-by: followynne <[email protected]>
- Loading branch information
1 parent
ea83b81
commit 7f4842b
Showing
16 changed files
with
636 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Serilog.Ui.Core; | ||
using Serilog.Ui.Core.Interfaces; | ||
using Serilog.Ui.Core.Models.Options; | ||
using System; | ||
|
||
namespace Serilog.Ui.SqliteDataProvider.Extensions; | ||
|
||
/// <summary> | ||
/// SQLite data provider specific extension methods for <see cref="ISerilogUiOptionsBuilder"/>. | ||
/// </summary> | ||
public static class SerilogUiOptionBuilderExtensions | ||
{ | ||
/// <summary> Configures the SerilogUi to connect to a SQLite database.</summary> | ||
/// <param name="optionsBuilder"> The options builder. </param> | ||
/// <param name="setupOptions">The SQLite options action.</param> | ||
public static ISerilogUiOptionsBuilder UseSqliteServer( | ||
this ISerilogUiOptionsBuilder optionsBuilder, | ||
Action<RelationalDbOptions> setupOptions) | ||
{ | ||
var dbOptions = new SqliteDbOptions(); | ||
setupOptions(dbOptions); | ||
dbOptions.Validate(); | ||
|
||
string providerName = dbOptions.GetProviderName(SqliteDataProvider.SqliteProviderName); | ||
optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); | ||
optionsBuilder.Services.AddScoped<IDataProvider, SqliteDataProvider>(_ => new SqliteDataProvider(dbOptions, new SqliteQueryBuilder())); | ||
|
||
return optionsBuilder; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using Serilog.Ui.Core.Models.Options; | ||
using Serilog.Ui.Core.QueryBuilder.Sql; | ||
using Serilog.Ui.SqliteDataProvider.Models; | ||
|
||
namespace Serilog.Ui.SqliteDataProvider.Extensions; | ||
|
||
public class SqliteDbOptions() : RelationalDbOptions("ununsed") | ||
{ | ||
public SinkColumnNames ColumnNames { get; } = new SqliteSinkColumnNames(); | ||
} |
16 changes: 16 additions & 0 deletions
16
src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using Serilog.Ui.Core.QueryBuilder.Sql; | ||
|
||
namespace Serilog.Ui.SqliteDataProvider.Models; | ||
|
||
internal class SqliteSinkColumnNames : SinkColumnNames | ||
{ | ||
public SqliteSinkColumnNames() | ||
{ | ||
Exception = "Exception"; | ||
Level = "Level"; | ||
LogEventSerialized = "Properties"; | ||
Message = "RenderedMessage"; | ||
MessageTemplate = ""; | ||
Timestamp = "Timestamp"; | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<PackageId>Serilog.UI.SqliteProvider</PackageId> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<LangVersion>latest</LangVersion> | ||
<Version>1.0.0</Version> | ||
|
||
<Authors>Tech Garage (team)</Authors> | ||
<Description>SQLite data provider for Serilog UI.</Description> | ||
<PackageTags>serilog serilog-ui serilog.sinks.sqlite sqlite</PackageTags> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Dapper" Version="2.1.35" /> | ||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.*" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Serilog.Ui.Core\Serilog.Ui.Core.csproj" /> | ||
<InternalsVisibleTo Include="Sqlite.Tests" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using Ardalis.GuardClauses; | ||
using Dapper; | ||
using Microsoft.Data.Sqlite; | ||
using Serilog.Ui.Core; | ||
using Serilog.Ui.Core.Models; | ||
using Serilog.Ui.SqliteDataProvider.Extensions; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Serilog.Ui.SqliteDataProvider; | ||
|
||
public class SqliteDataProvider(SqliteDbOptions options, SqliteQueryBuilder queryBuilder) : IDataProvider | ||
{ | ||
internal const string SqliteProviderName = "SQLite"; | ||
private readonly SqliteDbOptions _options = Guard.Against.Null(options); | ||
|
||
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) | ||
{ | ||
queryParams.ToUtcDates(); // assuming data is saved in UTC, due to UTC predictability | ||
|
||
var logsTask = GetLogsAsync(queryParams); | ||
var logCountTask = CountLogsAsync(queryParams); | ||
|
||
await Task.WhenAll(logsTask, logCountTask); | ||
|
||
return (await logsTask, await logCountTask); | ||
} | ||
|
||
public string Name => _options.GetProviderName(SqliteProviderName); | ||
|
||
private async Task<IEnumerable<LogModel>> GetLogsAsync(FetchLogsQuery queryParams) | ||
{ | ||
var query = queryBuilder.BuildFetchLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); | ||
|
||
var rowNoStart = queryParams.Page * queryParams.Count; | ||
|
||
using var connection = new SqliteConnection(_options.ConnectionString); | ||
var queryParameters = new | ||
{ | ||
Offset = rowNoStart, | ||
queryParams.Count, | ||
queryParams.Level, | ||
Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, | ||
StartDate = StringifyDate(queryParams.StartDate), | ||
EndDate = StringifyDate(queryParams.EndDate) | ||
}; | ||
var logs = await connection.QueryAsync<LogModel>(query.ToString(), queryParameters); | ||
|
||
return logs.Select((item, i) => | ||
{ | ||
item.PropertyType = "json"; | ||
var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind); | ||
item.Timestamp = ts.ToUniversalTime(); | ||
item.SetRowNo(rowNoStart, i); | ||
return item; | ||
}).ToList(); | ||
} | ||
|
||
private Task<int> CountLogsAsync(FetchLogsQuery queryParams) | ||
{ | ||
var query = queryBuilder.BuildCountLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); | ||
|
||
using var connection = new SqliteConnection(_options.ConnectionString); | ||
|
||
return connection.QueryFirstOrDefaultAsync<int>( | ||
query.ToString(), | ||
new | ||
{ | ||
queryParams.Level, | ||
Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, | ||
StartDate = StringifyDate(queryParams.StartDate), | ||
EndDate = StringifyDate(queryParams.EndDate) | ||
}); | ||
} | ||
|
||
private static string StringifyDate(DateTime? date) => date.HasValue ? date.Value.ToString("s") + ".999" : "null"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using System; | ||
using System.Text; | ||
using Serilog.Ui.Core.Models; | ||
using Serilog.Ui.Core.QueryBuilder.Sql; | ||
|
||
namespace Serilog.Ui.SqliteDataProvider; | ||
|
||
/// <summary> | ||
/// Provides methods to build SQL queries specifically for Sqlite to fetch and count logs. | ||
/// </summary> | ||
/// <typeparam name="TModel">The type of the log model.</typeparam> | ||
public class SqliteQueryBuilder : SqlQueryBuilder<LogModel> | ||
{ | ||
///<inheritdoc /> | ||
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(); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) | ||
{ | ||
StringBuilder queryStr = new(); | ||
|
||
queryStr.Append($"SELECT COUNT(Id) FROM {tableName} "); | ||
|
||
GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); | ||
|
||
return queryStr.ToString(); | ||
} | ||
|
||
protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) | ||
=> $"ORDER BY {GetSortColumnName(columns, sortOn)} {sortBy.ToString().ToUpper()}"; | ||
|
||
/// <inheritdoc/> | ||
private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) | ||
{ | ||
queryBuilder.Append($"SELECT Id, {columns.Message} AS Message, {columns.Level}, {columns.Timestamp}, {columns.Exception}, {columns.LogEventSerialized} "); | ||
queryBuilder.Append($"FROM {tableName} "); | ||
} | ||
|
||
/// <inheritdoc/> | ||
private static void GenerateWhereClause( | ||
StringBuilder queryBuilder, | ||
SinkColumnNames columns, | ||
string? level, | ||
string? searchCriteria, | ||
DateTime? startDate, | ||
DateTime? endDate) | ||
{ | ||
var conditionStart = "WHERE"; | ||
|
||
if (!string.IsNullOrWhiteSpace(level)) | ||
{ | ||
queryBuilder.Append($"{conditionStart} {columns.Level} = @Level "); | ||
conditionStart = "AND"; | ||
} | ||
|
||
if (!string.IsNullOrWhiteSpace(searchCriteria)) | ||
{ | ||
queryBuilder.Append($"{conditionStart} ({columns.Message} LIKE @Search OR {columns.Exception} LIKE @Search) "); | ||
conditionStart = "AND"; | ||
} | ||
|
||
if (startDate != null) | ||
{ | ||
queryBuilder.Append($"{conditionStart} {columns.Timestamp} >= @StartDate "); | ||
conditionStart = "AND"; | ||
} | ||
|
||
if (endDate != null) | ||
{ | ||
queryBuilder.Append($"{conditionStart} {columns.Timestamp} <= @EndDate "); | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using FluentAssertions; | ||
using Microsoft.Extensions.Primitives; | ||
using Serilog.Ui.Common.Tests.TestSuites; | ||
using Serilog.Ui.Core.Extensions; | ||
using Serilog.Ui.Core.Models; | ||
using Serilog.Ui.SqliteDataProvider; | ||
using Serilog.Ui.SqliteDataProvider.Extensions; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
namespace Sqlite.Tests.DataProvider | ||
{ | ||
[Trait("Unit-Base", "Sqlite")] | ||
public class DataProviderBaseTest : IUnitBaseTests | ||
{ | ||
[Fact] | ||
public void It_throws_when_any_dependency_is_null() | ||
{ | ||
var suts = new List<Func<SqliteDataProvider>> | ||
{ | ||
() => new SqliteDataProvider(null!, new SqliteQueryBuilder()), | ||
}; | ||
|
||
suts.ForEach(sut => sut.Should().ThrowExactly<ArgumentNullException>()); | ||
} | ||
|
||
[Fact] | ||
public Task It_logs_and_throws_when_db_read_breaks_down() | ||
{ | ||
var sut = new SqliteDataProvider( | ||
new SqliteDbOptions().WithConnectionString("connString").WithTable("Logs"), | ||
new SqliteQueryBuilder() | ||
); | ||
|
||
Dictionary<string, StringValues> query = new() { ["page"] = "1", ["count"] = "10" }; | ||
|
||
var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); | ||
return assert.Should().ThrowExactlyAsync<ArgumentException>(); | ||
} | ||
} | ||
} |
Oops, something went wrong.