Skip to content

Add RavenDB provider. #111 #115

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

Merged
merged 4 commits into from
Mar 19, 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 @@ -15,6 +15,7 @@ A simple Serilog log viewer for the following sinks:
- Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql))
- 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))

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

Expand Down Expand Up @@ -43,7 +44,8 @@ Install one of the available providers, based upon your sink:
| **Serilog.UI.MySqlProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MySqlProvider)] | `dotnet add package Serilog.UI.MySqlProvider` | `Install-Package Serilog.UI.MySqlProvider` |
| **Serilog.UI.PostgreSqlProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.PostgreSqlProvider)] | `dotnet add package Serilog.UI.PostgreSqlProvider` | `Install-Package Serilog.UI.PostgreSqlProvider` |
| **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.ElasticSearcProvider` |
| **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` |

### 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 @@ -60,6 +60,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests-related", "tests-rela
tests\Directory.Build.Props = tests\Directory.Build.Props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider", "src\Serilog.Ui.RavenDbProvider\Serilog.Ui.RavenDbProvider.csproj", "{8973E5F5-FD9B-41B1-B2D6-8B281754C443}"
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -128,6 +132,14 @@ Global
{1AB759E4-61CD-4195-9CA9-E70B63AF28B9}.Release|Any CPU.Build.0 = Release|Any CPU
{5D0F29B0-11F4-4FEA-8D90-FD917F680D6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D0F29B0-11F4-4FEA-8D90-FD917F680D6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8973E5F5-FD9B-41B1-B2D6-8B281754C443}.Release|Any CPU.Build.0 = Release|Any CPU
{B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -150,6 +162,8 @@ Global
{1AB759E4-61CD-4195-9CA9-E70B63AF28B9} = {75F9223B-15F2-4465-B01D-2A5A49FCD000}
{04B4A8FE-0D7F-48AB-BDE5-AF934CEAD7DF} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {88374732-FEAD-4375-9CF1-75331A37CF07}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ string collectionName
if (string.IsNullOrWhiteSpace(databaseName))
throw new ArgumentException(nameof(MongoUrl.DatabaseName));

var mongoProvider = new MongoDbOptions
var dbOptions = new MongoDbOptions
{
ConnectionString = connectionString,
DatabaseName = databaseName,
Expand All @@ -46,12 +46,14 @@ string collectionName

var builder = ((ISerilogUiOptionsBuilder)optionsBuilder);

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

builder.Services.AddSingleton(mongoProvider);
builder.Services.AddSingleton(dbOptions);
builder.Services.TryAddSingleton<IMongoClient>(o => new MongoClient(connectionString));
builder.Services.AddScoped<IDataProvider, MongoDbDataProvider>();
}
Expand Down Expand Up @@ -91,10 +93,12 @@ string collectionName

var builder = ((ISerilogUiOptionsBuilder)optionsBuilder);

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

((ISerilogUiOptionsBuilder)optionsBuilder).Services.AddSingleton(mongoProvider);
((ISerilogUiOptionsBuilder)optionsBuilder).Services.TryAddSingleton<IMongoClient>(o => new MongoClient(connectionString));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Ardalis.GuardClauses;
using Microsoft.Extensions.DependencyInjection;
using Raven.Client.Documents;
using Serilog.Ui.Core;

namespace Serilog.Ui.RavenDbProvider.Extensions;

/// <summary>
/// RavenDB's data provider specific extension methods for <see cref="SerilogUiOptionsBuilder"/>.
/// </summary>
public static class SerilogUiOptionBuilderExtensions
{
/// <summary>
/// Configures the SerilogUi to connect to a RavenDB database.
/// </summary>
/// <param name="optionsBuilder">The Serilog UI options builder.</param>
/// <param name="documentStore">A DocumentStore for a RavenDB database.</param>
/// <param name="collectionName"> Name of the collection to query logs. default value is <c>LogEvents</c>.</param>
/// <exception cref="ArgumentNullException">throw if documentStore is null</exception>
public static void UseRavenDb(this SerilogUiOptionsBuilder optionsBuilder, IDocumentStore documentStore, string collectionName = "LogEvents")
{
Guard.Against.Null(documentStore, nameof(documentStore));
Guard.Against.NullOrEmpty(documentStore.Urls, nameof(documentStore.Urls));
Guard.Against.NullOrEmpty(documentStore.Database, nameof(documentStore.Database));
Guard.Against.NullOrEmpty(collectionName, nameof(collectionName));

var builder = ((ISerilogUiOptionsBuilder)optionsBuilder);

// TODO: Fix up RavenDB to allow multiple registrations. Think about multiple RavenDB clients
// (singletons) used in data providers (scoped)
if (builder.Services.Any(c => c.ImplementationType == typeof(RavenDbDataProvider)))
{
throw new NotSupportedException($"Adding multiple registrations of '{typeof(RavenDbDataProvider).FullName}' is not (yet) supported.");
}

builder.Services.AddSingleton(documentStore);
builder.Services.AddScoped<IDataProvider>(_ => new RavenDbDataProvider(documentStore, collectionName));
}
}
31 changes: 31 additions & 0 deletions src/Serilog.Ui.RavenDbProvider/Models/RavenDbLogModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog.Ui.Core;

namespace Serilog.Ui.RavenDbProvider.Models;

internal class RavenDbLogModel
{
public DateTimeOffset Timestamp { get; set; }

public string MessageTemplate { get; set; } = null!;

public string Level { get; set; } = null!;

public JObject? Exception { get; set; }

public string RenderedMessage { get; set; } = null!;

public IDictionary<string, object>? Properties { get; set; }

public LogModel ToLogModel(int rowNo) => new()
{
RowNo = rowNo,
Level = Level,
Message = RenderedMessage,
Timestamp = Timestamp.ToUniversalTime().DateTime,
Exception = Exception?.ToString(Formatting.None),
Properties = JsonConvert.SerializeObject(Properties),
PropertyType = "json"
};
}
113 changes: 113 additions & 0 deletions src/Serilog.Ui.RavenDbProvider/RavenDbDataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Raven.Client.Documents;
using Raven.Client.Documents.Linq;
using Serilog.Ui.Core;
using Serilog.Ui.RavenDbProvider.Models;

namespace Serilog.Ui.RavenDbProvider;

/// <inheritdoc/>
public class RavenDbDataProvider : IDataProvider
{
private readonly string _collectionName;
private readonly IDocumentStore _documentStore;

public RavenDbDataProvider(IDocumentStore documentStore, string collectionName)
{
_documentStore = documentStore;
_collectionName = collectionName;
}

/// <inheritdoc/>
public string Name => string.Join(".", "RavenDB");

/// <inheritdoc/>
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
int page,
int count,
string? level = null,
string? searchCriteria = null,
DateTime? startDate = null,
DateTime? endDate = null
)
{
if (startDate != null && startDate.Value.Kind != DateTimeKind.Utc)
{
startDate = DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc);
}

if (endDate != null && endDate.Value.Kind != DateTimeKind.Utc)
{
endDate = DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc);
}

var logsTask = GetLogsAsync(page - 1, count, level, searchCriteria, startDate, endDate);
var logCountTask = CountLogsAsync(level, searchCriteria, startDate, endDate);
await Task.WhenAll(logsTask, logCountTask);

return (await logsTask, await logCountTask);
}

private async Task<IEnumerable<LogModel>> GetLogsAsync(
int page,
int count,
string? level,
string? searchCriteria,
DateTime? startDate,
DateTime? endDate)
{
using var session = _documentStore.OpenAsyncSession();
var query = session.Advanced.AsyncDocumentQuery<RavenDbLogModel>(collectionName: _collectionName).ToQueryable();

GenerateWhereClause(ref query, level, searchCriteria, startDate, endDate);

var logs = await query.Skip(count * page).Take(count).ToListAsync();

var index = 1;

return logs.Select(log => log.ToLogModel((page * count) + index++)).ToList();
}

private async Task<int> CountLogsAsync(
string? level,
string? searchCriteria,
DateTime? startDate = null,
DateTime? endDate = null)
{
using var session = _documentStore.OpenAsyncSession();
var query = session.Advanced.AsyncDocumentQuery<RavenDbLogModel>(collectionName: _collectionName).ToQueryable();

GenerateWhereClause(ref query, level, searchCriteria, startDate, endDate);

return await query.CountAsync();
}

private void GenerateWhereClause(
ref IRavenQueryable<RavenDbLogModel> query,
string? level,
string? searchCriteria,
DateTime? startDate,
DateTime? endDate)
{
if (!string.IsNullOrEmpty(level))
{
query = query.Where(q => q.Level == level);
}

if (!string.IsNullOrEmpty(searchCriteria))
{
query = query
.Search(q => q.RenderedMessage, searchCriteria)
.Search(q => q.Exception, searchCriteria);
}

if (startDate != null)
{
query = query.Where(q => q.Timestamp >= startDate.Value);
}

if (endDate != null)
{
query = query.Where(q => q.Timestamp <= endDate.Value);
}
}
}
21 changes: 21 additions & 0 deletions src/Serilog.Ui.RavenDbProvider/Serilog.Ui.RavenDbProvider.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<Version>1.0.0</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RavenDB.Client" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Serilog.Ui.Core\Serilog.Ui.Core.csproj" />
<InternalsVisibleTo Include="RavenDb.Tests" />
</ItemGroup>

</Project>
30 changes: 30 additions & 0 deletions src/Serilog.Ui.RavenDbProvider/Serilog.Ui.RavenDbProvider.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Serilog.Ui.RavenDbProvider</id>
<version>$version$</version>
<title>Serilog.Ui.RavenDbProvider</title>
<authors>Mohsen Esmailpour</authors>
<owners>mo.esmp</owners>
<projectUrl>https://github.com/serilog-contrib/serilog-ui</projectUrl>
<license type="expression">MIT</license>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>RavenDB data provider for Serilog UI.</description>
<readme>docs\README.md</readme>
<releaseNotes></releaseNotes>
<copyright></copyright>
<tags>serilog serilog-ui serilog.sinks.ravendb</tags>
<icon>assets\icon.png</icon>
<dependencies>
<group targetFramework=".NETStandard2.0">
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="7.0.0" exclude="Build,Analyzers" />
<dependency id="RavenDB.Client" version="6.0.0" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
<files>
<file src="bin\Release\netstandard2.0\*.dll" target="lib/netstandard2.0" />
<file src="..\..\assets\icon.png" target="assets\" />
<file src="..\..\README.md" target="docs\" />
</files>
</package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using FluentAssertions;
using Raven.Client.Exceptions;
using RavenDb.Tests.Util;
using Serilog.Ui.Common.Tests.TestSuites.Impl;
using Serilog.Ui.RavenDbProvider;

namespace RavenDb.Tests.DataProvider;

[Collection(nameof(RavenDbDataProvider))]
[Trait("Integration-Pagination", "RavenDb")]
public class DataProviderPaginationTest : IntegrationPaginationTests<RavenDbTestProvider>
{
public DataProviderPaginationTest(RavenDbTestProvider instance) : base(instance)
{
}

public override Task It_fetches_with_limit() => base.It_fetches_with_limit();

public override Task It_fetches_with_limit_and_skip() => base.It_fetches_with_limit_and_skip();

public override Task It_fetches_with_skip() => base.It_fetches_with_skip();

[Fact]
public override Task It_throws_when_skip_is_zero()
{
var test = () => provider.FetchDataAsync(0, 1);
return test.Should().ThrowAsync<InvalidQueryException>();
}
}
Loading