Skip to content

Commit 7f4842b

Browse files
feat(provider): add SQLite sink support (#124)
* Add Sqlite Provider Co-authored-by: followynne <mtt-safe-grgo@outlook.com>
1 parent ea83b81 commit 7f4842b

16 files changed

+636
-2
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
A simple Serilog log viewer for the following sinks:
1010

1111
- Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver))
12-
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
12+
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
1313
- 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))
1414
- Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb))
1515
- Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch))
1616
- Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb))
17+
- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/))
1718

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

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

4749
### DI registration
4850

README_Nuget.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
A simple Serilog log viewer for the following sinks:
44

55
- Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver))
6-
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
6+
- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)
77
- 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))
88
- Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb))
99
- Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch))
1010
- Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb))
11+
- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/))
1112

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

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

3941
### DI registration
4042

Serilog.Ui.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider"
6161
EndProject
6262
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}"
6363
EndProject
64+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteDataProvider", "src\Serilog.Ui.SqliteDataProvider\Serilog.Ui.SqliteDataProvider.csproj", "{A23F4275-DB47-40C9-96CE-1116E20F5EB7}"
65+
EndProject
66+
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}"
67+
EndProject
6468
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp", "samples\WebApp\WebApp.csproj", "{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}"
6569
EndProject
6670
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "samples\WebApi\WebApi.csproj", "{A2701899-102D-4926-B054-FD76F59A0791}"
@@ -137,6 +141,14 @@ Global
137141
{B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.Build.0 = Debug|Any CPU
138142
{B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.ActiveCfg = Release|Any CPU
139143
{B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.Build.0 = Release|Any CPU
144+
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
145+
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
146+
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
147+
{A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.Build.0 = Release|Any CPU
148+
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
149+
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
150+
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
151+
{C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.Build.0 = Release|Any CPU
140152
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
141153
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Debug|Any CPU.Build.0 = Debug|Any CPU
142154
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -168,6 +180,8 @@ Global
168180
{DCB452AD-2E0E-4D6A-B46D-72D0AF247381} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E}
169181
{8973E5F5-FD9B-41B1-B2D6-8B281754C443} = {ACA69857-2E3E-468C-B0B0-A86852E3492D}
170182
{B785845B-D858-4562-B224-67468B4FEE41} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
183+
{A23F4275-DB47-40C9-96CE-1116E20F5EB7} = {ACA69857-2E3E-468C-B0B0-A86852E3492D}
184+
{C9CBABEA-622C-4E11-9D68-816F685E8E0D} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
171185
{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765} = {157CA77C-513A-409F-8045-E68739AAC8C8}
172186
{A2701899-102D-4926-B054-FD76F59A0791} = {157CA77C-513A-409F-8045-E68739AAC8C8}
173187
EndGlobalSection
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Serilog.Ui.Core;
3+
using Serilog.Ui.Core.Interfaces;
4+
using Serilog.Ui.Core.Models.Options;
5+
using System;
6+
7+
namespace Serilog.Ui.SqliteDataProvider.Extensions;
8+
9+
/// <summary>
10+
/// SQLite data provider specific extension methods for <see cref="ISerilogUiOptionsBuilder"/>.
11+
/// </summary>
12+
public static class SerilogUiOptionBuilderExtensions
13+
{
14+
/// <summary> Configures the SerilogUi to connect to a SQLite database.</summary>
15+
/// <param name="optionsBuilder"> The options builder. </param>
16+
/// <param name="setupOptions">The SQLite options action.</param>
17+
public static ISerilogUiOptionsBuilder UseSqliteServer(
18+
this ISerilogUiOptionsBuilder optionsBuilder,
19+
Action<RelationalDbOptions> setupOptions)
20+
{
21+
var dbOptions = new SqliteDbOptions();
22+
setupOptions(dbOptions);
23+
dbOptions.Validate();
24+
25+
string providerName = dbOptions.GetProviderName(SqliteDataProvider.SqliteProviderName);
26+
optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
27+
optionsBuilder.Services.AddScoped<IDataProvider, SqliteDataProvider>(_ => new SqliteDataProvider(dbOptions, new SqliteQueryBuilder()));
28+
29+
return optionsBuilder;
30+
}
31+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Serilog.Ui.Core.Models.Options;
2+
using Serilog.Ui.Core.QueryBuilder.Sql;
3+
using Serilog.Ui.SqliteDataProvider.Models;
4+
5+
namespace Serilog.Ui.SqliteDataProvider.Extensions;
6+
7+
public class SqliteDbOptions() : RelationalDbOptions("ununsed")
8+
{
9+
public SinkColumnNames ColumnNames { get; } = new SqliteSinkColumnNames();
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Serilog.Ui.Core.QueryBuilder.Sql;
2+
3+
namespace Serilog.Ui.SqliteDataProvider.Models;
4+
5+
internal class SqliteSinkColumnNames : SinkColumnNames
6+
{
7+
public SqliteSinkColumnNames()
8+
{
9+
Exception = "Exception";
10+
Level = "Level";
11+
LogEventSerialized = "Properties";
12+
Message = "RenderedMessage";
13+
MessageTemplate = "";
14+
Timestamp = "Timestamp";
15+
}
16+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageId>Serilog.UI.SqliteProvider</PackageId>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
<LangVersion>latest</LangVersion>
7+
<Version>1.0.0</Version>
8+
9+
<Authors>Tech Garage (team)</Authors>
10+
<Description>SQLite data provider for Serilog UI.</Description>
11+
<PackageTags>serilog serilog-ui serilog.sinks.sqlite sqlite</PackageTags>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Dapper" Version="2.1.35" />
16+
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.*" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\Serilog.Ui.Core\Serilog.Ui.Core.csproj" />
21+
<InternalsVisibleTo Include="Sqlite.Tests" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using Ardalis.GuardClauses;
2+
using Dapper;
3+
using Microsoft.Data.Sqlite;
4+
using Serilog.Ui.Core;
5+
using Serilog.Ui.Core.Models;
6+
using Serilog.Ui.SqliteDataProvider.Extensions;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace Serilog.Ui.SqliteDataProvider;
14+
15+
public class SqliteDataProvider(SqliteDbOptions options, SqliteQueryBuilder queryBuilder) : IDataProvider
16+
{
17+
internal const string SqliteProviderName = "SQLite";
18+
private readonly SqliteDbOptions _options = Guard.Against.Null(options);
19+
20+
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
21+
{
22+
queryParams.ToUtcDates(); // assuming data is saved in UTC, due to UTC predictability
23+
24+
var logsTask = GetLogsAsync(queryParams);
25+
var logCountTask = CountLogsAsync(queryParams);
26+
27+
await Task.WhenAll(logsTask, logCountTask);
28+
29+
return (await logsTask, await logCountTask);
30+
}
31+
32+
public string Name => _options.GetProviderName(SqliteProviderName);
33+
34+
private async Task<IEnumerable<LogModel>> GetLogsAsync(FetchLogsQuery queryParams)
35+
{
36+
var query = queryBuilder.BuildFetchLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams);
37+
38+
var rowNoStart = queryParams.Page * queryParams.Count;
39+
40+
using var connection = new SqliteConnection(_options.ConnectionString);
41+
var queryParameters = new
42+
{
43+
Offset = rowNoStart,
44+
queryParams.Count,
45+
queryParams.Level,
46+
Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null,
47+
StartDate = StringifyDate(queryParams.StartDate),
48+
EndDate = StringifyDate(queryParams.EndDate)
49+
};
50+
var logs = await connection.QueryAsync<LogModel>(query.ToString(), queryParameters);
51+
52+
return logs.Select((item, i) =>
53+
{
54+
item.PropertyType = "json";
55+
56+
var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind);
57+
item.Timestamp = ts.ToUniversalTime();
58+
59+
item.SetRowNo(rowNoStart, i);
60+
return item;
61+
}).ToList();
62+
}
63+
64+
private Task<int> CountLogsAsync(FetchLogsQuery queryParams)
65+
{
66+
var query = queryBuilder.BuildCountLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams);
67+
68+
using var connection = new SqliteConnection(_options.ConnectionString);
69+
70+
return connection.QueryFirstOrDefaultAsync<int>(
71+
query.ToString(),
72+
new
73+
{
74+
queryParams.Level,
75+
Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null,
76+
StartDate = StringifyDate(queryParams.StartDate),
77+
EndDate = StringifyDate(queryParams.EndDate)
78+
});
79+
}
80+
81+
private static string StringifyDate(DateTime? date) => date.HasValue ? date.Value.ToString("s") + ".999" : "null";
82+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Text;
3+
using Serilog.Ui.Core.Models;
4+
using Serilog.Ui.Core.QueryBuilder.Sql;
5+
6+
namespace Serilog.Ui.SqliteDataProvider;
7+
8+
/// <summary>
9+
/// Provides methods to build SQL queries specifically for Sqlite to fetch and count logs.
10+
/// </summary>
11+
/// <typeparam name="TModel">The type of the log model.</typeparam>
12+
public class SqliteQueryBuilder : SqlQueryBuilder<LogModel>
13+
{
14+
///<inheritdoc />
15+
public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
16+
{
17+
StringBuilder queryStr = new();
18+
19+
GenerateSelectClause(queryStr, columns, schema, tableName);
20+
21+
GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
22+
23+
queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Offset, @Count");
24+
25+
return queryStr.ToString();
26+
}
27+
28+
/// <inheritdoc/>
29+
public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
30+
{
31+
StringBuilder queryStr = new();
32+
33+
queryStr.Append($"SELECT COUNT(Id) FROM {tableName} ");
34+
35+
GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
36+
37+
return queryStr.ToString();
38+
}
39+
40+
protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy)
41+
=> $"ORDER BY {GetSortColumnName(columns, sortOn)} {sortBy.ToString().ToUpper()}";
42+
43+
/// <inheritdoc/>
44+
private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName)
45+
{
46+
queryBuilder.Append($"SELECT Id, {columns.Message} AS Message, {columns.Level}, {columns.Timestamp}, {columns.Exception}, {columns.LogEventSerialized} ");
47+
queryBuilder.Append($"FROM {tableName} ");
48+
}
49+
50+
/// <inheritdoc/>
51+
private static void GenerateWhereClause(
52+
StringBuilder queryBuilder,
53+
SinkColumnNames columns,
54+
string? level,
55+
string? searchCriteria,
56+
DateTime? startDate,
57+
DateTime? endDate)
58+
{
59+
var conditionStart = "WHERE";
60+
61+
if (!string.IsNullOrWhiteSpace(level))
62+
{
63+
queryBuilder.Append($"{conditionStart} {columns.Level} = @Level ");
64+
conditionStart = "AND";
65+
}
66+
67+
if (!string.IsNullOrWhiteSpace(searchCriteria))
68+
{
69+
queryBuilder.Append($"{conditionStart} ({columns.Message} LIKE @Search OR {columns.Exception} LIKE @Search) ");
70+
conditionStart = "AND";
71+
}
72+
73+
if (startDate != null)
74+
{
75+
queryBuilder.Append($"{conditionStart} {columns.Timestamp} >= @StartDate ");
76+
conditionStart = "AND";
77+
}
78+
79+
if (endDate != null)
80+
{
81+
queryBuilder.Append($"{conditionStart} {columns.Timestamp} <= @EndDate ");
82+
}
83+
}
84+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using FluentAssertions;
2+
using Microsoft.Extensions.Primitives;
3+
using Serilog.Ui.Common.Tests.TestSuites;
4+
using Serilog.Ui.Core.Extensions;
5+
using Serilog.Ui.Core.Models;
6+
using Serilog.Ui.SqliteDataProvider;
7+
using Serilog.Ui.SqliteDataProvider.Extensions;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Threading.Tasks;
11+
using Xunit;
12+
13+
namespace Sqlite.Tests.DataProvider
14+
{
15+
[Trait("Unit-Base", "Sqlite")]
16+
public class DataProviderBaseTest : IUnitBaseTests
17+
{
18+
[Fact]
19+
public void It_throws_when_any_dependency_is_null()
20+
{
21+
var suts = new List<Func<SqliteDataProvider>>
22+
{
23+
() => new SqliteDataProvider(null!, new SqliteQueryBuilder()),
24+
};
25+
26+
suts.ForEach(sut => sut.Should().ThrowExactly<ArgumentNullException>());
27+
}
28+
29+
[Fact]
30+
public Task It_logs_and_throws_when_db_read_breaks_down()
31+
{
32+
var sut = new SqliteDataProvider(
33+
new SqliteDbOptions().WithConnectionString("connString").WithTable("Logs"),
34+
new SqliteQueryBuilder()
35+
);
36+
37+
Dictionary<string, StringValues> query = new() { ["page"] = "1", ["count"] = "10" };
38+
39+
var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query));
40+
return assert.Should().ThrowExactlyAsync<ArgumentException>();
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)