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

Fix #104 - Support PostgreSQL alternative sink #105

Merged
merged 11 commits into from
Jan 26, 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
1 change: 1 addition & 0 deletions src/Serilog.Ui.Core/Serilog.Ui.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<Version>2.5.0</Version>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Serilog.Ui.Core;

namespace Serilog.Ui.PostgreSqlProvider;

public class PostgreSqlDbOptions : RelationalDbOptions
{
public PostgreSqlSinkType SinkType { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Serilog.Ui.PostgreSqlProvider;

public enum PostgreSqlSinkType
{
SerilogSinksPostgreSQL,

SerilogSinksPostgreSQLAlternative
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ public static class SerilogUiOptionBuilderExtensions
/// <summary>
/// Configures the SerilogUi to connect to a PostgreSQL database.
/// </summary>
/// <param name="optionsBuilder"> The options builder. </param>
/// <param name="sinkType">
/// The sink that used to store logs in the PostgreSQL database. This data provider supports
/// <a href="https://github.com/b00ted/serilog-sinks-postgresql">Serilog.Sinks.Postgresql</a> and
/// <a href="https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative">Serilog.Sinks.Postgresql.Alternative</a> sinks.
/// </param>
/// <param name="optionsBuilder"> The Serilog UI option builder. </param>
/// <param name="connectionString"> The connection string. </param>
/// <param name="tableName"> Name of the table. </param>
/// <param name="schemaName">
Expand All @@ -22,6 +27,7 @@ public static class SerilogUiOptionBuilderExtensions
/// <exception cref="ArgumentNullException"> throw is tableName is null </exception>
public static void UseNpgSql(
this SerilogUiOptionsBuilder optionsBuilder,
PostgreSqlSinkType sinkType,
string connectionString,
string tableName,
string schemaName = "public"
Expand All @@ -33,13 +39,16 @@ public static void UseNpgSql(
if (string.IsNullOrWhiteSpace(tableName))
throw new ArgumentNullException(nameof(tableName));

var relationProvider = new RelationalDbOptions
var relationProvider = new PostgreSqlDbOptions
{
ConnectionString = connectionString,
TableName = tableName,
Schema = !string.IsNullOrWhiteSpace(schemaName) ? schemaName : "public"
Schema = !string.IsNullOrWhiteSpace(schemaName) ? schemaName : "public",
SinkType = sinkType
};

QueryBuilder.SetSinkType(sinkType);

((ISerilogUiOptionsBuilder)optionsBuilder).Services
.AddScoped<IDataProvider, PostgresDataProvider>(p => ActivatorUtilities.CreateInstance<PostgresDataProvider>(p, relationProvider));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Serilog.Ui.Core;

namespace Serilog.Ui.PostgreSqlProvider
namespace Serilog.Ui.PostgreSqlProvider.Models
{
internal class PostgresLogModel : LogModel
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Serilog.Ui.PostgreSqlProvider.Models;

internal class PostgreSqlAlternativeSinkColumnNames : SinkColumnNames
{
public PostgreSqlAlternativeSinkColumnNames()
{
Exception = "Exception";
Level = "Level";
LogEventSerialized = "LogEvent";
MessageTemplate = "MessageTemplate";
RenderedMessage = "Message";
Timestamp = "Timestamp";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Serilog.Ui.PostgreSqlProvider.Models;

internal class PostgreSqlSinkColumnNames : SinkColumnNames
{
public PostgreSqlSinkColumnNames()
{
RenderedMessage = "message";
MessageTemplate = "message_template";
Level = "level";
Timestamp = "timestamp";
Exception = "exception";
LogEventSerialized = "log_event";
}
}
16 changes: 16 additions & 0 deletions src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Serilog.Ui.PostgreSqlProvider.Models;

internal abstract class SinkColumnNames
{
public string RenderedMessage { get; set; }

public string MessageTemplate { get; set; }

public string Level { get; set; }

public string Timestamp { get; set; }

public string Exception { get; set; }

public string LogEventSerialized { get; set; }
}
185 changes: 67 additions & 118 deletions src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,146 +1,95 @@
using Dapper;
using Npgsql;
using Serilog.Ui.Core;
using Serilog.Ui.PostgreSqlProvider.Models;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Linq;
using System.Threading.Tasks;

namespace Serilog.Ui.PostgreSqlProvider
namespace Serilog.Ui.PostgreSqlProvider;

public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvider
{
public class PostgresDataProvider : IDataProvider
public string Name => options.ToDataProviderName("NPGSQL");

public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
int page,
int count,
string level = null,
string searchCriteria = null,
DateTime? startDate = null,
DateTime? endDate = null
)
{
private readonly RelationalDbOptions _options;

public PostgresDataProvider(RelationalDbOptions options)
if (startDate != null && startDate.Value.Kind != DateTimeKind.Utc)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
startDate = DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc);
}

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

public string Name => _options.ToDataProviderName("NPGSQL");

private async Task<IEnumerable<LogModel>> GetLogsAsync(int page,
int count,
string level,
string searchCriteria,
DateTime? startDate,
DateTime? endDate)
{
var queryBuilder = new StringBuilder();
queryBuilder.Append("SELECT message, message_template, level, timestamp, exception, log_event AS \"Properties\" FROM \"");
queryBuilder.Append(_options.Schema);
queryBuilder.Append("\".\"");
queryBuilder.Append(_options.TableName);
queryBuilder.Append("\"");
var logsTask = GetLogsAsync(page - 1, count, level, searchCriteria, startDate, endDate);
var logCountTask = CountLogsAsync(level, searchCriteria, startDate, endDate);
await Task.WhenAll(logsTask, logCountTask);

GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate);

queryBuilder.Append(" ORDER BY timestamp DESC LIMIT @Count OFFSET @Offset ");

using IDbConnection connection = new NpgsqlConnection(_options.ConnectionString);
var logs = await connection.QueryAsync<PostgresLogModel>(queryBuilder.ToString(),
new
{
Offset = page * count,
Count = count,
// TODO: this level could be a text column, to be passed as parameter: https://github.com/b00ted/serilog-sinks-postgresql/blob/ce73c7423383d91ddc3823fe350c1c71fc23bab9/Serilog.Sinks.PostgreSQL/Sinks/PostgreSQL/ColumnWriters.cs#L97
Level = LogLevelConverter.GetLevelValue(level),
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
StartDate = startDate,
EndDate = endDate
});
return (await logsTask, await logCountTask);
}

var index = 1;
foreach (var log in logs)
log.RowNo = (page * count) + index++;
private async Task<IEnumerable<LogModel>> GetLogsAsync(
int page,
int count,
string level,
string searchCriteria,
DateTime? startDate,
DateTime? endDate)
{
var query = QueryBuilder.BuildFetchLogsQuery(options.Schema, options.TableName, level, searchCriteria, ref startDate, ref endDate);

return logs;
}
using IDbConnection connection = new NpgsqlConnection(options.ConnectionString);

private async Task<int> CountLogsAsync(
string level,
string searchCriteria,
DateTime? startDate = null,
DateTime? endDate = null)
var logs = (await connection.QueryAsync<PostgresLogModel>(query,
new
{
Offset = page * count,
Count = count,
// TODO: this level could be a text column, to be passed as parameter: https://github.com/b00ted/serilog-sinks-postgresql/blob/ce73c7423383d91ddc3823fe350c1c71fc23bab9/Serilog.Sinks.PostgreSQL/Sinks/PostgreSQL/ColumnWriters.cs#L97
Level = LogLevelConverter.GetLevelValue(level),
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
StartDate = startDate,
EndDate = endDate
})).ToList();

var index = 1;
foreach (var log in logs)
{
var queryBuilder = new StringBuilder();
queryBuilder.Append("SELECT COUNT(message) FROM \"");
queryBuilder.Append(_options.Schema);
queryBuilder.Append("\".\"");
queryBuilder.Append(_options.TableName);
queryBuilder.Append("\"");

GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate);

using IDbConnection connection = new NpgsqlConnection(_options.ConnectionString);
return await connection.ExecuteScalarAsync<int>(queryBuilder.ToString(),
new
{
Level = LogLevelConverter.GetLevelValue(level),
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
StartDate = startDate,
EndDate = endDate
});
log.RowNo = (page * count) + index++;
}

private void GenerateWhereClause(
StringBuilder queryBuilder,
string level,
string searchCriteria,
DateTime? startDate = null,
DateTime? endDate = null)
{
var whereIncluded = false;

if (!string.IsNullOrEmpty(level))
{
queryBuilder.Append(" WHERE level = @Level ");
whereIncluded = true;
}
return logs;
}

if (!string.IsNullOrEmpty(searchCriteria))
{
queryBuilder.Append(whereIncluded
? " AND message LIKE @Search OR exception LIKE @Search "
: " WHERE message LIKE @Search OR exception LIKE @Search ");
}
private async Task<int> CountLogsAsync(
string level,
string searchCriteria,
DateTime? startDate = null,
DateTime? endDate = null)
{
var query = QueryBuilder.BuildCountLogsQuery(options.Schema, options.TableName, level, searchCriteria, ref startDate, ref endDate);

if (startDate != null)
{
queryBuilder.Append(whereIncluded
? " AND timestamp >= @StartDate "
: " WHERE timestamp >= @StartDate ");
whereIncluded = true;
}
using IDbConnection connection = new NpgsqlConnection(options.ConnectionString);

if (endDate != null)
return await connection.ExecuteScalarAsync<int>(query,
new
{
queryBuilder.Append(whereIncluded
? " AND timestamp < @EndDate "
: " WHERE timestamp < @EndDate ");
}
}
Level = LogLevelConverter.GetLevelValue(level),
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
StartDate = startDate,
EndDate = endDate
});
}
}
}
Loading
Loading