Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(provider): add SQLite sink support #124

Merged
merged 16 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading