Skip to content

Commit bde69d8

Browse files
authored
Fix #104 - Support PostgreSQL alternative sink (#105)
* Add query builder for PostgreSQL alternative sink. * Add unit tests for query builder. * Fix query builder. * Remove extra code. * Fix XML doc. * Move model classes to Models folder. * Fix models namespace. ---------
1 parent b28e42a commit bde69d8

16 files changed

+331
-150
lines changed

src/Serilog.Ui.Core/Serilog.Ui.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>netstandard2.0</TargetFramework>
4+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
45
<LangVersion>latest</LangVersion>
56
<Version>2.5.0</Version>
67
</PropertyGroup>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Serilog.Ui.Core;
2+
3+
namespace Serilog.Ui.PostgreSqlProvider;
4+
5+
public class PostgreSqlDbOptions : RelationalDbOptions
6+
{
7+
public PostgreSqlSinkType SinkType { get; set; }
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Serilog.Ui.PostgreSqlProvider;
2+
3+
public enum PostgreSqlSinkType
4+
{
5+
SerilogSinksPostgreSQL,
6+
7+
SerilogSinksPostgreSQLAlternative
8+
}

src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ public static class SerilogUiOptionBuilderExtensions
1212
/// <summary>
1313
/// Configures the SerilogUi to connect to a PostgreSQL database.
1414
/// </summary>
15-
/// <param name="optionsBuilder"> The options builder. </param>
15+
/// <param name="sinkType">
16+
/// The sink that used to store logs in the PostgreSQL database. This data provider supports
17+
/// <a href="https://github.com/b00ted/serilog-sinks-postgresql">Serilog.Sinks.Postgresql</a> and
18+
/// <a href="https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative">Serilog.Sinks.Postgresql.Alternative</a> sinks.
19+
/// </param>
20+
/// <param name="optionsBuilder"> The Serilog UI option builder. </param>
1621
/// <param name="connectionString"> The connection string. </param>
1722
/// <param name="tableName"> Name of the table. </param>
1823
/// <param name="schemaName">
@@ -22,6 +27,7 @@ public static class SerilogUiOptionBuilderExtensions
2227
/// <exception cref="ArgumentNullException"> throw is tableName is null </exception>
2328
public static void UseNpgSql(
2429
this SerilogUiOptionsBuilder optionsBuilder,
30+
PostgreSqlSinkType sinkType,
2531
string connectionString,
2632
string tableName,
2733
string schemaName = "public"
@@ -33,13 +39,16 @@ public static void UseNpgSql(
3339
if (string.IsNullOrWhiteSpace(tableName))
3440
throw new ArgumentNullException(nameof(tableName));
3541

36-
var relationProvider = new RelationalDbOptions
42+
var relationProvider = new PostgreSqlDbOptions
3743
{
3844
ConnectionString = connectionString,
3945
TableName = tableName,
40-
Schema = !string.IsNullOrWhiteSpace(schemaName) ? schemaName : "public"
46+
Schema = !string.IsNullOrWhiteSpace(schemaName) ? schemaName : "public",
47+
SinkType = sinkType
4148
};
4249

50+
QueryBuilder.SetSinkType(sinkType);
51+
4352
((ISerilogUiOptionsBuilder)optionsBuilder).Services
4453
.AddScoped<IDataProvider, PostgresDataProvider>(p => ActivatorUtilities.CreateInstance<PostgresDataProvider>(p, relationProvider));
4554
}

src/Serilog.Ui.PostgreSqlProvider/PostgreLogModel.cs renamed to src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Serilog.Ui.Core;
22

3-
namespace Serilog.Ui.PostgreSqlProvider
3+
namespace Serilog.Ui.PostgreSqlProvider.Models
44
{
55
internal class PostgresLogModel : LogModel
66
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Serilog.Ui.PostgreSqlProvider.Models;
2+
3+
internal class PostgreSqlAlternativeSinkColumnNames : SinkColumnNames
4+
{
5+
public PostgreSqlAlternativeSinkColumnNames()
6+
{
7+
Exception = "Exception";
8+
Level = "Level";
9+
LogEventSerialized = "LogEvent";
10+
MessageTemplate = "MessageTemplate";
11+
RenderedMessage = "Message";
12+
Timestamp = "Timestamp";
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Serilog.Ui.PostgreSqlProvider.Models;
2+
3+
internal class PostgreSqlSinkColumnNames : SinkColumnNames
4+
{
5+
public PostgreSqlSinkColumnNames()
6+
{
7+
RenderedMessage = "message";
8+
MessageTemplate = "message_template";
9+
Level = "level";
10+
Timestamp = "timestamp";
11+
Exception = "exception";
12+
LogEventSerialized = "log_event";
13+
}
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Serilog.Ui.PostgreSqlProvider.Models;
2+
3+
internal abstract class SinkColumnNames
4+
{
5+
public string RenderedMessage { get; set; }
6+
7+
public string MessageTemplate { get; set; }
8+
9+
public string Level { get; set; }
10+
11+
public string Timestamp { get; set; }
12+
13+
public string Exception { get; set; }
14+
15+
public string LogEventSerialized { get; set; }
16+
}
Lines changed: 67 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,95 @@
11
using Dapper;
22
using Npgsql;
33
using Serilog.Ui.Core;
4+
using Serilog.Ui.PostgreSqlProvider.Models;
45
using System;
56
using System.Collections.Generic;
67
using System.Data;
7-
using System.Text;
8+
using System.Linq;
89
using System.Threading.Tasks;
910

10-
namespace Serilog.Ui.PostgreSqlProvider
11+
namespace Serilog.Ui.PostgreSqlProvider;
12+
13+
public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvider
1114
{
12-
public class PostgresDataProvider : IDataProvider
15+
public string Name => options.ToDataProviderName("NPGSQL");
16+
17+
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
18+
int page,
19+
int count,
20+
string level = null,
21+
string searchCriteria = null,
22+
DateTime? startDate = null,
23+
DateTime? endDate = null
24+
)
1325
{
14-
private readonly RelationalDbOptions _options;
15-
16-
public PostgresDataProvider(RelationalDbOptions options)
26+
if (startDate != null && startDate.Value.Kind != DateTimeKind.Utc)
1727
{
18-
_options = options ?? throw new ArgumentNullException(nameof(options));
28+
startDate = DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc);
1929
}
2030

21-
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
22-
int page,
23-
int count,
24-
string level = null,
25-
string searchCriteria = null,
26-
DateTime? startDate = null,
27-
DateTime? endDate = null
28-
)
31+
if (endDate != null && endDate.Value.Kind != DateTimeKind.Utc)
2932
{
30-
if (startDate != null && startDate.Value.Kind != DateTimeKind.Utc)
31-
startDate = DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc);
32-
if (endDate != null && endDate.Value.Kind != DateTimeKind.Utc)
33-
endDate = DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc);
34-
var logsTask = GetLogsAsync(page - 1, count, level, searchCriteria, startDate, endDate);
35-
var logCountTask = CountLogsAsync(level, searchCriteria, startDate, endDate);
36-
37-
await Task.WhenAll(logsTask, logCountTask);
38-
39-
return (await logsTask, await logCountTask);
33+
endDate = DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc);
4034
}
4135

42-
public string Name => _options.ToDataProviderName("NPGSQL");
43-
44-
private async Task<IEnumerable<LogModel>> GetLogsAsync(int page,
45-
int count,
46-
string level,
47-
string searchCriteria,
48-
DateTime? startDate,
49-
DateTime? endDate)
50-
{
51-
var queryBuilder = new StringBuilder();
52-
queryBuilder.Append("SELECT message, message_template, level, timestamp, exception, log_event AS \"Properties\" FROM \"");
53-
queryBuilder.Append(_options.Schema);
54-
queryBuilder.Append("\".\"");
55-
queryBuilder.Append(_options.TableName);
56-
queryBuilder.Append("\"");
36+
var logsTask = GetLogsAsync(page - 1, count, level, searchCriteria, startDate, endDate);
37+
var logCountTask = CountLogsAsync(level, searchCriteria, startDate, endDate);
38+
await Task.WhenAll(logsTask, logCountTask);
5739

58-
GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate);
59-
60-
queryBuilder.Append(" ORDER BY timestamp DESC LIMIT @Count OFFSET @Offset ");
61-
62-
using IDbConnection connection = new NpgsqlConnection(_options.ConnectionString);
63-
var logs = await connection.QueryAsync<PostgresLogModel>(queryBuilder.ToString(),
64-
new
65-
{
66-
Offset = page * count,
67-
Count = count,
68-
// 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
69-
Level = LogLevelConverter.GetLevelValue(level),
70-
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
71-
StartDate = startDate,
72-
EndDate = endDate
73-
});
40+
return (await logsTask, await logCountTask);
41+
}
7442

75-
var index = 1;
76-
foreach (var log in logs)
77-
log.RowNo = (page * count) + index++;
43+
private async Task<IEnumerable<LogModel>> GetLogsAsync(
44+
int page,
45+
int count,
46+
string level,
47+
string searchCriteria,
48+
DateTime? startDate,
49+
DateTime? endDate)
50+
{
51+
var query = QueryBuilder.BuildFetchLogsQuery(options.Schema, options.TableName, level, searchCriteria, ref startDate, ref endDate);
7852

79-
return logs;
80-
}
53+
using IDbConnection connection = new NpgsqlConnection(options.ConnectionString);
8154

82-
private async Task<int> CountLogsAsync(
83-
string level,
84-
string searchCriteria,
85-
DateTime? startDate = null,
86-
DateTime? endDate = null)
55+
var logs = (await connection.QueryAsync<PostgresLogModel>(query,
56+
new
57+
{
58+
Offset = page * count,
59+
Count = count,
60+
// 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
61+
Level = LogLevelConverter.GetLevelValue(level),
62+
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
63+
StartDate = startDate,
64+
EndDate = endDate
65+
})).ToList();
66+
67+
var index = 1;
68+
foreach (var log in logs)
8769
{
88-
var queryBuilder = new StringBuilder();
89-
queryBuilder.Append("SELECT COUNT(message) FROM \"");
90-
queryBuilder.Append(_options.Schema);
91-
queryBuilder.Append("\".\"");
92-
queryBuilder.Append(_options.TableName);
93-
queryBuilder.Append("\"");
94-
95-
GenerateWhereClause(queryBuilder, level, searchCriteria, startDate, endDate);
96-
97-
using IDbConnection connection = new NpgsqlConnection(_options.ConnectionString);
98-
return await connection.ExecuteScalarAsync<int>(queryBuilder.ToString(),
99-
new
100-
{
101-
Level = LogLevelConverter.GetLevelValue(level),
102-
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
103-
StartDate = startDate,
104-
EndDate = endDate
105-
});
70+
log.RowNo = (page * count) + index++;
10671
}
10772

108-
private void GenerateWhereClause(
109-
StringBuilder queryBuilder,
110-
string level,
111-
string searchCriteria,
112-
DateTime? startDate = null,
113-
DateTime? endDate = null)
114-
{
115-
var whereIncluded = false;
116-
117-
if (!string.IsNullOrEmpty(level))
118-
{
119-
queryBuilder.Append(" WHERE level = @Level ");
120-
whereIncluded = true;
121-
}
73+
return logs;
74+
}
12275

123-
if (!string.IsNullOrEmpty(searchCriteria))
124-
{
125-
queryBuilder.Append(whereIncluded
126-
? " AND message LIKE @Search OR exception LIKE @Search "
127-
: " WHERE message LIKE @Search OR exception LIKE @Search ");
128-
}
76+
private async Task<int> CountLogsAsync(
77+
string level,
78+
string searchCriteria,
79+
DateTime? startDate = null,
80+
DateTime? endDate = null)
81+
{
82+
var query = QueryBuilder.BuildCountLogsQuery(options.Schema, options.TableName, level, searchCriteria, ref startDate, ref endDate);
12983

130-
if (startDate != null)
131-
{
132-
queryBuilder.Append(whereIncluded
133-
? " AND timestamp >= @StartDate "
134-
: " WHERE timestamp >= @StartDate ");
135-
whereIncluded = true;
136-
}
84+
using IDbConnection connection = new NpgsqlConnection(options.ConnectionString);
13785

138-
if (endDate != null)
86+
return await connection.ExecuteScalarAsync<int>(query,
87+
new
13988
{
140-
queryBuilder.Append(whereIncluded
141-
? " AND timestamp < @EndDate "
142-
: " WHERE timestamp < @EndDate ");
143-
}
144-
}
89+
Level = LogLevelConverter.GetLevelValue(level),
90+
Search = searchCriteria != null ? "%" + searchCriteria + "%" : null,
91+
StartDate = startDate,
92+
EndDate = endDate
93+
});
14594
}
146-
}
95+
}

0 commit comments

Comments
 (0)