Skip to content
Open
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
2 changes: 1 addition & 1 deletion build/Build.Backend.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ partial class Build

// due to [ref](https://github.com/Mongo2Go/Mongo2Go/issues/144)
ProcessTasks
.StartProcess("sudo", $"chown -R {user}:{user} /home/runneradmin")
.StartProcess("sudo", $"chown -R {user}:{user} /home/runner")
.AssertZeroExitCode();
// encoded spaces [ref](https://github.com/microsoft/azure-pipelines-tasks/issues/18731#issuecomment-1689118779)
DotnetCoverage?.Invoke(
Expand Down
116 changes: 58 additions & 58 deletions src/Serilog.Ui.Core/AggregateDataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,79 +1,79 @@
using System;
using Ardalis.GuardClauses;
using Serilog.Ui.Core.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Serilog.Ui.Core.Models;

namespace Serilog.Ui.Core
namespace Serilog.Ui.Core;

/// <summary>
/// Aggregates multiple <see cref="IDataProvider" /> into one instance.
/// </summary>
public class AggregateDataProvider : IDataProvider
{
private readonly Dictionary<string, IDataProvider> _dataProviders = new();

/// <summary>
/// Aggregates multiple <see cref="IDataProvider"/> into one instance.
/// It creates an instance of <see cref="AggregateDataProvider" />.
/// </summary>
public class AggregateDataProvider : IDataProvider
/// <param name="dataProviders">IEnumerable of providers.</param>
/// <exception cref="ArgumentNullException">when <paramref name="dataProviders" /> is null</exception>
/// <exception cref="ArgumentException">when <paramref name="dataProviders" /> is empty</exception>
public AggregateDataProvider(IEnumerable<IDataProvider> dataProviders)
{
private readonly Dictionary<string, IDataProvider> _dataProviders = new();
List<IDataProvider> providers = Guard.Against.NullOrEmpty(dataProviders).ToList();

/// <summary>
/// It creates an instance of <see cref="AggregateDataProvider"/>.
/// </summary>
/// <param name="dataProviders">IEnumerable of providers.</param>
/// <exception cref="ArgumentNullException">when <paramref name="dataProviders"/> is null</exception>
/// <exception cref="ArgumentException">when <paramref name="dataProviders"/> is empty</exception>
public AggregateDataProvider(IEnumerable<IDataProvider> dataProviders)
foreach (List<IDataProvider>? grouped in providers.GroupBy(dp => dp.Name, p => p, (_, e) => e.ToList()))
{
var providers = Guard.Against.NullOrEmpty(dataProviders).ToList();

foreach (var grouped in providers.GroupBy(dp => dp.Name, p => p, (_, e) => e.ToList()))
{
var name = grouped[0].Name;
string name = grouped[0].Name;

if (grouped.Count == 1)
if (grouped.Count == 1)
_dataProviders.Add(name, grouped[0]);
else
// When providers with the same name are registered, we ensure uniqueness by
// generating a key I.e. ["MSSQL.dbo.logs", "MSSQL.dbo.logs"] =>
// ["MSSQL.dbo.logs[0]", "MSSQL.dbo.logs[1]"]
for (int i = 0; i < grouped.Count; i++)
{
_dataProviders.Add(name, grouped[0]);
IDataProvider? dataProvider = grouped[i];
_dataProviders.Add($"{name}[{i}]", dataProvider);
}
else
{
// When providers with the same name are registered, we ensure uniqueness by
// generating a key I.e. ["MSSQL.dbo.logs", "MSSQL.dbo.logs"] =>
// ["MSSQL.dbo.logs[0]", "MSSQL.dbo.logs[1]"]
for (var i = 0; i < grouped.Count; i++)
{
var dataProvider = grouped[i];
_dataProviders.Add($"{name}[{i}]", dataProvider);
}
}
}

SelectedDataProvider = _dataProviders.First().Value;
}

/// <summary>
/// <inheritdoc cref="IDataProvider.Name"/>
/// NOTE: We assume only one Aggregate provider, so the name is static.
/// </summary>
public string Name => nameof(AggregateDataProvider);
SelectedDataProvider = _dataProviders.First().Value;
}

/// <summary>
/// If there is only one data provider, this is it.
/// If there are multiple, this is the current data provider.
/// </summary>
private IDataProvider SelectedDataProvider { get; set; }

/// <summary>
/// Existing data providers keys.
/// </summary>
public IEnumerable<string> Keys => _dataProviders.Keys;

/// <summary>
/// If there is only one data provider, this is it.
/// If there are multiple, this is the current data provider.
/// </summary>
private IDataProvider SelectedDataProvider { get; set; }
/// <summary>
/// <inheritdoc cref="IDataProvider.Name" />
/// NOTE: We assume only one Aggregate provider, so the name is static.
/// </summary>
public string Name => nameof(AggregateDataProvider);

/// <summary>
/// Switch active data provider by key.
/// </summary>
/// <param name="key">Data provider key</param>
public void SwitchToProvider(string key) => SelectedDataProvider = _dataProviders[key];
/// <inheritdoc />
public Task<(IEnumerable<LogModel>, int)> FetchDataAsync(FetchLogsQuery queryParams,
CancellationToken cancellationToken = default)
=> SelectedDataProvider.FetchDataAsync(queryParams, cancellationToken);

/// <summary>
/// Existing data providers keys.
/// </summary>
public IEnumerable<string> Keys => _dataProviders.Keys;
/// <inheritdoc />
public Task<LogStatisticModel> FetchDashboardAsync(CancellationToken cancellationToken = default)
=> SelectedDataProvider.FetchDashboardAsync(cancellationToken);

/// <inheritdoc/>
public Task<(IEnumerable<LogModel>, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
=> SelectedDataProvider.FetchDataAsync(queryParams, cancellationToken);
}
/// <summary>
/// Switch active data provider by key.
/// </summary>
/// <param name="key">Data provider key</param>
public void SwitchToProvider(string key) => SelectedDataProvider = _dataProviders[key];
}
31 changes: 18 additions & 13 deletions src/Serilog.Ui.Core/IDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@
using System.Threading.Tasks;
using Serilog.Ui.Core.Models;

namespace Serilog.Ui.Core
namespace Serilog.Ui.Core;

/// <summary>
/// Data provider interface
/// </summary>
public interface IDataProvider
{
/// <summary>
/// Data provider interface
/// Name of the provider, used to identify this provider when using multiple.
/// </summary>
string Name { get; }

/// <summary>
/// Fetches the log data asynchronous.
/// </summary>
public interface IDataProvider
{
/// <summary>
/// Fetches the log data asynchronous.
/// </summary>
Task<(IEnumerable<LogModel> results, int total)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default);
Task<(IEnumerable<LogModel> results, int total)> FetchDataAsync(FetchLogsQuery queryParams,
CancellationToken cancellationToken = default);

/// <summary>
/// Name of the provider, used to identify this provider when using multiple.
/// </summary>
string Name { get; }
}
/// <summary>
/// Fetches dashboard statistics asynchronous.
/// </summary>
Task<LogStatisticModel> FetchDashboardAsync(CancellationToken cancellationToken = default);
}
29 changes: 29 additions & 0 deletions src/Serilog.Ui.Core/Models/LogStatisticModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;

namespace Serilog.Ui.Core.Models;

/// <summary>
/// Represents dashboard statistics for log data visualization.
/// </summary>
public class LogStatisticModel
{
/// <summary>
/// Gets or sets the total count of logs.
/// </summary>
public int TotalLogs { get; set; }

/// <summary>
/// Gets or sets the count of logs by level.
/// </summary>
public Dictionary<string, int> LogsByLevel { get; set; } = new();

/// <summary>
/// Gets or sets the count of logs for today.
/// </summary>
public int TodayLogs { get; set; }

/// <summary>
/// Gets or sets the count of error logs for today.
/// </summary>
public int TodayErrorLogs { get; set; }
}
39 changes: 22 additions & 17 deletions src/Serilog.Ui.ElasticSearchProvider/ElasticSearchDbDataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using System;
using Nest;
using Newtonsoft.Json;
using Serilog.Ui.Core;
using Serilog.Ui.Core.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Nest;
using Newtonsoft.Json;
using Serilog.Ui.Core;
using Serilog.Ui.Core.Models;
using static Serilog.Ui.Core.Models.SearchOptions;

namespace Serilog.Ui.ElasticSearchProvider;
Expand All @@ -24,35 +24,40 @@ public class ElasticSearchDbDataProvider(IElasticClient client, ElasticSearchDbO

private readonly ElasticSearchDbOptions _options = options ?? throw new ArgumentNullException(nameof(options));

public Task<(IEnumerable<LogModel>, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
{
return GetLogsAsync(queryParams, cancellationToken);
}
public Task<(IEnumerable<LogModel>, int)> FetchDataAsync(FetchLogsQuery queryParams,
CancellationToken cancellationToken = default) => GetLogsAsync(queryParams, cancellationToken);

public Task<LogStatisticModel> FetchDashboardAsync(CancellationToken cancellationToken = default) =>
throw new NotImplementedException();

public string Name => _options.ProviderName;

private async Task<(IEnumerable<LogModel>, int)> GetLogsAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
private async Task<(IEnumerable<LogModel>, int)> GetLogsAsync(FetchLogsQuery queryParams,
CancellationToken cancellationToken = default)
{
// since serilog-sink does not have keyword indexes on level and message, we can only sort on @timestamp
Func<SortDescriptor<ElasticSearchDbLogModel>, SortDescriptor<ElasticSearchDbLogModel>> sortDescriptor =
queryParams.SortBy == SortDirection.Desc
? descriptor => descriptor.Descending(TimeStampPropertyName)
: descriptor => descriptor.Ascending(TimeStampPropertyName);
var rowNoStart = queryParams.Page * queryParams.Count;
var descriptor = new SearchDescriptor<ElasticSearchDbLogModel>()
int rowNoStart = queryParams.Page * queryParams.Count;
SearchDescriptor<ElasticSearchDbLogModel>? descriptor = new SearchDescriptor<ElasticSearchDbLogModel>()
.Index(_options.IndexName)
.Query(q =>
+q.Match(m => m.Field(f => f.Level).Query(queryParams.Level)) &&
+q.DateRange(dr => dr.Field(f => f.Timestamp).GreaterThanOrEquals(queryParams.StartDate).LessThanOrEquals(queryParams.EndDate)) &&
+q.Match(m => m.Field(f => f.Message).Query(queryParams.SearchCriteria)) ||
(+q.Match(m => m.Field(f => f.Level).Query(queryParams.Level)) &&
+q.DateRange(dr =>
dr.Field(f => f.Timestamp).GreaterThanOrEquals(queryParams.StartDate)
.LessThanOrEquals(queryParams.EndDate)) &&
+q.Match(m => m.Field(f => f.Message).Query(queryParams.SearchCriteria))) ||
+q.Match(m => m.Field(f => f.Exceptions).Query(queryParams.SearchCriteria)))
.Sort(sortDescriptor)
.Skip(rowNoStart)
.Size(queryParams.Count);

var result = await _client.SearchAsync<ElasticSearchDbLogModel>(descriptor, cancellationToken);
ISearchResponse<ElasticSearchDbLogModel>? result =
await _client.SearchAsync<ElasticSearchDbLogModel>(descriptor, cancellationToken);

int.TryParse(result?.Total.ToString(), out var total);
int.TryParse(result?.Total.ToString(), out int total);

return (result?.Documents.Select((x, index) => x.ToLogModel(rowNoStart, index)).ToList() ?? [], total);
}
Expand Down
Loading
Loading