Skip to content

Commit 62f502d

Browse files
mo-esmpMohsen Esmailpour
and
Mohsen Esmailpour
authored
Add RavenDB provider. #111 (#115)
* Add RavenDB provider. #111 * Fix log timestamp. * Fix tests. * Fix code review comments. --------- Co-authored-by: Mohsen Esmailpour <[email protected]>
1 parent 8eb0e8d commit 62f502d

14 files changed

+501
-5
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A simple Serilog log viewer for the following sinks:
1515
- Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql))
1616
- Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb))
1717
- Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch))
18+
- Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb))
1819

1920
<img src="https://raw.githubusercontent.com/mo-esmp/serilog-ui/master/assets/serilog-ui.jpg" width="100%" />
2021

@@ -43,7 +44,8 @@ Install one of the available providers, based upon your sink:
4344
| **Serilog.UI.MySqlProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MySqlProvider)] | `dotnet add package Serilog.UI.MySqlProvider` | `Install-Package Serilog.UI.MySqlProvider` |
4445
| **Serilog.UI.PostgreSqlProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.PostgreSqlProvider)] | `dotnet add package Serilog.UI.PostgreSqlProvider` | `Install-Package Serilog.UI.PostgreSqlProvider` |
4546
| **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` |
46-
| **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearcProvider` |
47+
| **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` |
48+
| **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` |
4749

4850
### DI registration
4951

Diff for: Serilog.Ui.sln

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests-related", "tests-rela
6060
tests\Directory.Build.Props = tests\Directory.Build.Props
6161
EndProjectSection
6262
EndProject
63+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider", "src\Serilog.Ui.RavenDbProvider\Serilog.Ui.RavenDbProvider.csproj", "{8973E5F5-FD9B-41B1-B2D6-8B281754C443}"
64+
EndProject
65+
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}"
66+
EndProject
6367
Global
6468
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6569
Debug|Any CPU = Debug|Any CPU
@@ -128,6 +132,14 @@ Global
128132
{1AB759E4-61CD-4195-9CA9-E70B63AF28B9}.Release|Any CPU.Build.0 = Release|Any CPU
129133
{5D0F29B0-11F4-4FEA-8D90-FD917F680D6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
130134
{5D0F29B0-11F4-4FEA-8D90-FD917F680D6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
135+
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
136+
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Debug|Any CPU.Build.0 = Debug|Any CPU
137+
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Release|Any CPU.ActiveCfg = Release|Any CPU
138+
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Release|Any CPU.Build.0 = Release|Any CPU
139+
{B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
140+
{B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.Build.0 = Debug|Any CPU
141+
{B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.ActiveCfg = Release|Any CPU
142+
{B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.Build.0 = Release|Any CPU
131143
EndGlobalSection
132144
GlobalSection(SolutionProperties) = preSolution
133145
HideSolutionNode = FALSE
@@ -150,6 +162,8 @@ Global
150162
{1AB759E4-61CD-4195-9CA9-E70B63AF28B9} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
151163
{04B4A8FE-0D7F-48AB-BDE5-AF934CEAD7DF} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E}
152164
{DCB452AD-2E0E-4D6A-B46D-72D0AF247381} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E}
165+
{8973E5F5-FD9B-41B1-B2D6-8B281754C443} = {ACA69857-2E3E-468C-B0B0-A86852E3492D}
166+
{B785845B-D858-4562-B224-67468B4FEE41} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
153167
EndGlobalSection
154168
GlobalSection(ExtensibilityGlobals) = postSolution
155169
SolutionGuid = {88374732-FEAD-4375-9CF1-75331A37CF07}

Diff for: src/Serilog.Ui.MongoDbProvider/Extensions/SerilogUiOptionBuilderExtensions.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ string collectionName
3737
if (string.IsNullOrWhiteSpace(databaseName))
3838
throw new ArgumentException(nameof(MongoUrl.DatabaseName));
3939

40-
var mongoProvider = new MongoDbOptions
40+
var dbOptions = new MongoDbOptions
4141
{
4242
ConnectionString = connectionString,
4343
DatabaseName = databaseName,
@@ -46,12 +46,14 @@ string collectionName
4646

4747
var builder = ((ISerilogUiOptionsBuilder)optionsBuilder);
4848

49-
// TODO Fixup MongoDb to allow multiple registrations. Think about multiple ES clients
49+
// TODO Fix up MongoDB to allow multiple registrations. Think about multiple MongoDB clients
5050
// (singletons) used in data providers (scoped)
5151
if (builder.Services.Any(c => c.ImplementationType == typeof(MongoDbDataProvider)))
52+
{
5253
throw new NotSupportedException($"Adding multiple registrations of '{typeof(MongoDbDataProvider).FullName}' is not (yet) supported.");
54+
}
5355

54-
builder.Services.AddSingleton(mongoProvider);
56+
builder.Services.AddSingleton(dbOptions);
5557
builder.Services.TryAddSingleton<IMongoClient>(o => new MongoClient(connectionString));
5658
builder.Services.AddScoped<IDataProvider, MongoDbDataProvider>();
5759
}
@@ -91,10 +93,12 @@ string collectionName
9193

9294
var builder = ((ISerilogUiOptionsBuilder)optionsBuilder);
9395

94-
// TODO Fixup MongoDb to allow multiple registrations. Think about multiple ES clients
96+
// TODO Fix up MongoDB to allow multiple registrations. Think about multiple MongoDB clients
9597
// (singletons) used in data providers (scoped)
9698
if (builder.Services.Any(c => c.ImplementationType == typeof(MongoDbDataProvider)))
99+
{
97100
throw new NotSupportedException($"Adding multiple registrations of '{typeof(MongoDbDataProvider).FullName}' is not (yet) supported.");
101+
}
98102

99103
((ISerilogUiOptionsBuilder)optionsBuilder).Services.AddSingleton(mongoProvider);
100104
((ISerilogUiOptionsBuilder)optionsBuilder).Services.TryAddSingleton<IMongoClient>(o => new MongoClient(connectionString));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Ardalis.GuardClauses;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Raven.Client.Documents;
4+
using Serilog.Ui.Core;
5+
6+
namespace Serilog.Ui.RavenDbProvider.Extensions;
7+
8+
/// <summary>
9+
/// RavenDB's data provider specific extension methods for <see cref="SerilogUiOptionsBuilder"/>.
10+
/// </summary>
11+
public static class SerilogUiOptionBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Configures the SerilogUi to connect to a RavenDB database.
15+
/// </summary>
16+
/// <param name="optionsBuilder">The Serilog UI options builder.</param>
17+
/// <param name="documentStore">A DocumentStore for a RavenDB database.</param>
18+
/// <param name="collectionName"> Name of the collection to query logs. default value is <c>LogEvents</c>.</param>
19+
/// <exception cref="ArgumentNullException">throw if documentStore is null</exception>
20+
public static void UseRavenDb(this SerilogUiOptionsBuilder optionsBuilder, IDocumentStore documentStore, string collectionName = "LogEvents")
21+
{
22+
Guard.Against.Null(documentStore, nameof(documentStore));
23+
Guard.Against.NullOrEmpty(documentStore.Urls, nameof(documentStore.Urls));
24+
Guard.Against.NullOrEmpty(documentStore.Database, nameof(documentStore.Database));
25+
Guard.Against.NullOrEmpty(collectionName, nameof(collectionName));
26+
27+
var builder = ((ISerilogUiOptionsBuilder)optionsBuilder);
28+
29+
// TODO: Fix up RavenDB to allow multiple registrations. Think about multiple RavenDB clients
30+
// (singletons) used in data providers (scoped)
31+
if (builder.Services.Any(c => c.ImplementationType == typeof(RavenDbDataProvider)))
32+
{
33+
throw new NotSupportedException($"Adding multiple registrations of '{typeof(RavenDbDataProvider).FullName}' is not (yet) supported.");
34+
}
35+
36+
builder.Services.AddSingleton(documentStore);
37+
builder.Services.AddScoped<IDataProvider>(_ => new RavenDbDataProvider(documentStore, collectionName));
38+
}
39+
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
3+
using Serilog.Ui.Core;
4+
5+
namespace Serilog.Ui.RavenDbProvider.Models;
6+
7+
internal class RavenDbLogModel
8+
{
9+
public DateTimeOffset Timestamp { get; set; }
10+
11+
public string MessageTemplate { get; set; } = null!;
12+
13+
public string Level { get; set; } = null!;
14+
15+
public JObject? Exception { get; set; }
16+
17+
public string RenderedMessage { get; set; } = null!;
18+
19+
public IDictionary<string, object>? Properties { get; set; }
20+
21+
public LogModel ToLogModel(int rowNo) => new()
22+
{
23+
RowNo = rowNo,
24+
Level = Level,
25+
Message = RenderedMessage,
26+
Timestamp = Timestamp.ToUniversalTime().DateTime,
27+
Exception = Exception?.ToString(Formatting.None),
28+
Properties = JsonConvert.SerializeObject(Properties),
29+
PropertyType = "json"
30+
};
31+
}
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using Raven.Client.Documents;
2+
using Raven.Client.Documents.Linq;
3+
using Serilog.Ui.Core;
4+
using Serilog.Ui.RavenDbProvider.Models;
5+
6+
namespace Serilog.Ui.RavenDbProvider;
7+
8+
/// <inheritdoc/>
9+
public class RavenDbDataProvider : IDataProvider
10+
{
11+
private readonly string _collectionName;
12+
private readonly IDocumentStore _documentStore;
13+
14+
public RavenDbDataProvider(IDocumentStore documentStore, string collectionName)
15+
{
16+
_documentStore = documentStore;
17+
_collectionName = collectionName;
18+
}
19+
20+
/// <inheritdoc/>
21+
public string Name => string.Join(".", "RavenDB");
22+
23+
/// <inheritdoc/>
24+
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
25+
int page,
26+
int count,
27+
string? level = null,
28+
string? searchCriteria = null,
29+
DateTime? startDate = null,
30+
DateTime? endDate = null
31+
)
32+
{
33+
if (startDate != null && startDate.Value.Kind != DateTimeKind.Utc)
34+
{
35+
startDate = DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc);
36+
}
37+
38+
if (endDate != null && endDate.Value.Kind != DateTimeKind.Utc)
39+
{
40+
endDate = DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc);
41+
}
42+
43+
var logsTask = GetLogsAsync(page - 1, count, level, searchCriteria, startDate, endDate);
44+
var logCountTask = CountLogsAsync(level, searchCriteria, startDate, endDate);
45+
await Task.WhenAll(logsTask, logCountTask);
46+
47+
return (await logsTask, await logCountTask);
48+
}
49+
50+
private async Task<IEnumerable<LogModel>> GetLogsAsync(
51+
int page,
52+
int count,
53+
string? level,
54+
string? searchCriteria,
55+
DateTime? startDate,
56+
DateTime? endDate)
57+
{
58+
using var session = _documentStore.OpenAsyncSession();
59+
var query = session.Advanced.AsyncDocumentQuery<RavenDbLogModel>(collectionName: _collectionName).ToQueryable();
60+
61+
GenerateWhereClause(ref query, level, searchCriteria, startDate, endDate);
62+
63+
var logs = await query.Skip(count * page).Take(count).ToListAsync();
64+
65+
var index = 1;
66+
67+
return logs.Select(log => log.ToLogModel((page * count) + index++)).ToList();
68+
}
69+
70+
private async Task<int> CountLogsAsync(
71+
string? level,
72+
string? searchCriteria,
73+
DateTime? startDate = null,
74+
DateTime? endDate = null)
75+
{
76+
using var session = _documentStore.OpenAsyncSession();
77+
var query = session.Advanced.AsyncDocumentQuery<RavenDbLogModel>(collectionName: _collectionName).ToQueryable();
78+
79+
GenerateWhereClause(ref query, level, searchCriteria, startDate, endDate);
80+
81+
return await query.CountAsync();
82+
}
83+
84+
private void GenerateWhereClause(
85+
ref IRavenQueryable<RavenDbLogModel> query,
86+
string? level,
87+
string? searchCriteria,
88+
DateTime? startDate,
89+
DateTime? endDate)
90+
{
91+
if (!string.IsNullOrEmpty(level))
92+
{
93+
query = query.Where(q => q.Level == level);
94+
}
95+
96+
if (!string.IsNullOrEmpty(searchCriteria))
97+
{
98+
query = query
99+
.Search(q => q.RenderedMessage, searchCriteria)
100+
.Search(q => q.Exception, searchCriteria);
101+
}
102+
103+
if (startDate != null)
104+
{
105+
query = query.Where(q => q.Timestamp >= startDate.Value);
106+
}
107+
108+
if (endDate != null)
109+
{
110+
query = query.Where(q => q.Timestamp <= endDate.Value);
111+
}
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
8+
<LangVersion>latest</LangVersion>
9+
<Version>1.0.0</Version>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="RavenDB.Client" Version="6.0.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\Serilog.Ui.Core\Serilog.Ui.Core.csproj" />
18+
<InternalsVisibleTo Include="RavenDb.Tests" />
19+
</ItemGroup>
20+
21+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
3+
<metadata>
4+
<id>Serilog.Ui.RavenDbProvider</id>
5+
<version>$version$</version>
6+
<title>Serilog.Ui.RavenDbProvider</title>
7+
<authors>Mohsen Esmailpour</authors>
8+
<owners>mo.esmp</owners>
9+
<projectUrl>https://github.com/serilog-contrib/serilog-ui</projectUrl>
10+
<license type="expression">MIT</license>
11+
<requireLicenseAcceptance>false</requireLicenseAcceptance>
12+
<description>RavenDB data provider for Serilog UI.</description>
13+
<readme>docs\README.md</readme>
14+
<releaseNotes></releaseNotes>
15+
<copyright></copyright>
16+
<tags>serilog serilog-ui serilog.sinks.ravendb</tags>
17+
<icon>assets\icon.png</icon>
18+
<dependencies>
19+
<group targetFramework=".NETStandard2.0">
20+
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="7.0.0" exclude="Build,Analyzers" />
21+
<dependency id="RavenDB.Client" version="6.0.0" exclude="Build,Analyzers" />
22+
</group>
23+
</dependencies>
24+
</metadata>
25+
<files>
26+
<file src="bin\Release\netstandard2.0\*.dll" target="lib/netstandard2.0" />
27+
<file src="..\..\assets\icon.png" target="assets\" />
28+
<file src="..\..\README.md" target="docs\" />
29+
</files>
30+
</package>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using FluentAssertions;
2+
using Raven.Client.Exceptions;
3+
using RavenDb.Tests.Util;
4+
using Serilog.Ui.Common.Tests.TestSuites.Impl;
5+
using Serilog.Ui.RavenDbProvider;
6+
7+
namespace RavenDb.Tests.DataProvider;
8+
9+
[Collection(nameof(RavenDbDataProvider))]
10+
[Trait("Integration-Pagination", "RavenDb")]
11+
public class DataProviderPaginationTest : IntegrationPaginationTests<RavenDbTestProvider>
12+
{
13+
public DataProviderPaginationTest(RavenDbTestProvider instance) : base(instance)
14+
{
15+
}
16+
17+
public override Task It_fetches_with_limit() => base.It_fetches_with_limit();
18+
19+
public override Task It_fetches_with_limit_and_skip() => base.It_fetches_with_limit_and_skip();
20+
21+
public override Task It_fetches_with_skip() => base.It_fetches_with_skip();
22+
23+
[Fact]
24+
public override Task It_throws_when_skip_is_zero()
25+
{
26+
var test = () => provider.FetchDataAsync(0, 1);
27+
return test.Should().ThrowAsync<InvalidQueryException>();
28+
}
29+
}

0 commit comments

Comments
 (0)