Skip to content

Commit

Permalink
feat(provider): add SQLite sink support (#124)
Browse files Browse the repository at this point in the history
* Add Sqlite Provider

Co-authored-by: followynne <[email protected]>
  • Loading branch information
techgarage-ir and followynne authored Oct 6, 2024
1 parent ea83b81 commit 7f4842b
Show file tree
Hide file tree
Showing 16 changed files with 636 additions and 2 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
A simple Serilog log viewer for the following sinks:

- Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver))
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
- Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql)) and Serilog.Sinks.**Postgresql.Alternative** ([Nuget](https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative))
- Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb))
- Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch))
- Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb))
- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/))

<img src="https://raw.githubusercontent.com/serilog-contrib/serilog-ui/master/assets/serilog-ui-v3.jpg" width="100%" />

Expand Down Expand Up @@ -43,6 +44,7 @@ Install one or more of the available providers, based upon your sink(s):
| **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` |
| **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` |
| **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` |
| **Serilog.UI.SQLiteProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.SQLiteProvider)] | `dotnet add package Serilog.UI.SQLiteProvider` | `Install-Package Serilog.UI.SQLiteProvider` |

### DI registration

Expand Down
4 changes: 3 additions & 1 deletion README_Nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
A simple Serilog log viewer for the following sinks:

- Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver))
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
- Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql)) and Serilog.Sinks.**Postgresql.Alternative** ([Nuget](https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative))
- Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb))
- Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch))
- Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb))
- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/))

# Read the [Wiki](https://github.com/serilog-contrib/serilog-ui/wiki)

Expand Down Expand Up @@ -35,6 +36,7 @@ Install one or more of the available providers, based upon your sink(s):
| **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` |
| **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` |
| **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` |
| **Serilog.UI.SQLiteProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.SQLiteProvider)] | `dotnet add package Serilog.UI.SQLiteProvider` | `Install-Package Serilog.UI.SQLiteProvider` |

### DI registration

Expand Down
14 changes: 14 additions & 0 deletions Serilog.Ui.sln
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider.Tests", "tests\Serilog.Ui.RavenDbProvider.Tests\Serilog.Ui.RavenDbProvider.Tests.csproj", "{B785845B-D858-4562-B224-67468B4FEE41}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteDataProvider", "src\Serilog.Ui.SqliteDataProvider\Serilog.Ui.SqliteDataProvider.csproj", "{A23F4275-DB47-40C9-96CE-1116E20F5EB7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteProvider.Tests", "tests\Serilog.Ui.SqliteProvider.Tests\Serilog.Ui.SqliteProvider.Tests.csproj", "{C9CBABEA-622C-4E11-9D68-816F685E8E0D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp", "samples\WebApp\WebApp.csproj", "{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "samples\WebApi\WebApi.csproj", "{A2701899-102D-4926-B054-FD76F59A0791}"
Expand Down Expand Up @@ -137,6 +141,14 @@ Global
{B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.Build.0 = Release|Any CPU
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.Build.0 = Release|Any CPU
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.Build.0 = Release|Any CPU
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -168,6 +180,8 @@ Global
{DCB452AD-2E0E-4D6A-B46D-72D0AF247381} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E}
{8973E5F5-FD9B-41B1-B2D6-8B281754C443} = {ACA69857-2E3E-468C-B0B0-A86852E3492D}
{B785845B-D858-4562-B224-67468B4FEE41} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
{A23F4275-DB47-40C9-96CE-1116E20F5EB7} = {ACA69857-2E3E-468C-B0B0-A86852E3492D}
{C9CBABEA-622C-4E11-9D68-816F685E8E0D} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765} = {157CA77C-513A-409F-8045-E68739AAC8C8}
{A2701899-102D-4926-B054-FD76F59A0791} = {157CA77C-513A-409F-8045-E68739AAC8C8}
EndGlobalSection
Expand Down
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 src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs
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 src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs
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";
}
}
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>
82 changes: 82 additions & 0 deletions src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs
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";
}
84 changes: 84 additions & 0 deletions src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs
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 ");
}
}
}
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>();
}
}
}
Loading

0 comments on commit 7f4842b

Please sign in to comment.