Skip to content

Commit f9708f3

Browse files
authored
Merge pull request #270 from ckadluba/refactor-SinkTraits
Refactor sink traits
2 parents 7bf2ebc + 9c59c51 commit f9708f3

26 files changed

+948
-606
lines changed

sample/AppConfigDemo/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Threading;
33
using Serilog;
44
using Serilog.Events;
5-
using Serilog.Sinks.MSSqlServer;
65
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Options;
76

87
namespace AppConfigDemo

src/Serilog.Sinks.MSSqlServer/GlobalSuppressions.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@
77

88
[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Supplying string literals and not using resources is accepted within this project.", Scope = "namespaceanddescendants", Target = "Serilog.Sinks.MSSqlServer")]
99
[assembly: SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Too hard to change. Accepted for now.", Scope = "namespaceanddescendants", Target = "Serilog.Sinks.MSSqlServer")]
10-
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.MSSqlServerSinkTraits.TryChangeType(System.Object,System.Type,System.Object@)~System.Boolean")]
11-
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.MSSqlServerSink.EmitBatchAsync(System.Collections.Generic.IEnumerable{Serilog.Events.LogEvent})~System.Threading.Tasks.Task")]
12-
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.MSSqlServerSinkTraits.ConvertPropertiesToXmlStructure(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,Serilog.Events.LogEventPropertyValue}})~System.String")]
13-
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.MSSqlServerSinkTraits.#ctor(System.String,System.String,Serilog.Sinks.MSSqlServer.ColumnOptions,System.IFormatProvider,System.Boolean,Serilog.Formatting.ITextFormatter,Serilog.Sinks.MSSqlServer.Platform.ISqlTableCreator)")]
14-
[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Serilog core guarantees to call Emit() with non-null logEvent parameter.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.MSSqlServerAuditSink.Emit(Serilog.Events.LogEvent)")]
15-
[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Serilog core guarantees to call EmitBatchAsync() with non-null logEvent parameter.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.MSSqlServerSink.EmitBatchAsync(System.Collections.Generic.IEnumerable{Serilog.Events.LogEvent})~System.Threading.Tasks.Task")]
10+
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.Platform.SqlTableCreator.CreateTable(System.String,System.String,System.Data.DataTable,Serilog.Sinks.MSSqlServer.ColumnOptions)")]
11+
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Platform.SqlBulkBatchWriter.WriteBatch(System.Collections.Generic.IEnumerable{Serilog.Events.LogEvent},System.Data.DataTable)~System.Threading.Tasks.Task")]
12+
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.Output.StandardColumnDataGenerator.ConvertPropertiesToXmlStructure(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,Serilog.Events.LogEventPropertyValue}})~System.String")]
13+
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Too hard to change. Accepted for now.", Scope = "member", Target = "~M:Serilog.Sinks.MSSqlServer.Output.PropertiesColumnDataGenerator.TryChangeType(System.Object,System.Type,System.Object@)~System.Boolean")]
1614
[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Cannot be changed on public classes for backward compatibility reasons.", Scope = "namespaceanddescendants", Target = "Serilog.Sinks.MSSqlServer")]
1715
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Cannot be changed on public classes for backward compatibility reasons.", Scope = "type", Target = "~T:Serilog.Sinks.MSSqlServer.ColumnOptions")]
1816
[assembly: SuppressMessage("Design", "CA1010:Collections should implement generic interface", Justification = "Cannot be changed on public classes for backward compatibility reasons.", Scope = "namespaceanddescendants", Target = "Serilog.Sinks.MSSqlServer")]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Serilog.Sinks.MSSqlServer.Platform;
2+
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Platform;
3+
4+
namespace Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Dependencies
5+
{
6+
internal class SinkDependencies
7+
{
8+
public IDataTableCreator DataTableCreator { get; set; }
9+
public ISqlTableCreator SqlTableCreator { get; set; }
10+
public ISqlBulkBatchWriter SqlBulkBatchWriter { get; set; }
11+
public ISqlLogEventWriter SqlLogEventWriter { get; set; }
12+
}
13+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using Serilog.Formatting;
3+
using Serilog.Sinks.MSSqlServer.Output;
4+
using Serilog.Sinks.MSSqlServer.Platform;
5+
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Options;
6+
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Platform;
7+
8+
namespace Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Dependencies
9+
{
10+
internal static class SinkDependenciesFactory
11+
{
12+
internal static SinkDependencies Create(
13+
string connectionString,
14+
SinkOptions sinkOptions,
15+
IFormatProvider formatProvider,
16+
ColumnOptions columnOptions,
17+
ITextFormatter logEventFormatter)
18+
{
19+
columnOptions = columnOptions ?? new ColumnOptions();
20+
columnOptions.FinalizeConfigurationForSinkConstructor();
21+
22+
var sqlConnectionFactory =
23+
new SqlConnectionFactory(connectionString,
24+
new AzureManagedServiceAuthenticator(
25+
sinkOptions?.UseAzureManagedIdentity ?? default,
26+
sinkOptions.AzureServiceTokenProviderResource));
27+
var logEventDataGenerator =
28+
new LogEventDataGenerator(columnOptions,
29+
new StandardColumnDataGenerator(columnOptions, formatProvider, logEventFormatter),
30+
new PropertiesColumnDataGenerator(columnOptions));
31+
32+
var sinkDependencies = new SinkDependencies
33+
{
34+
SqlTableCreator = new SqlTableCreator(new SqlCreateTableWriter(), sqlConnectionFactory),
35+
DataTableCreator = new DataTableCreator(),
36+
SqlBulkBatchWriter = new SqlBulkBatchWriter(
37+
sinkOptions.TableName, sinkOptions.SchemaName,
38+
columnOptions.DisableTriggers, sqlConnectionFactory, logEventDataGenerator),
39+
SqlLogEventWriter = new SqlLogEventWriter(
40+
sinkOptions.TableName, sinkOptions.SchemaName,
41+
sqlConnectionFactory, logEventDataGenerator)
42+
};
43+
44+
return sinkDependencies;
45+
}
46+
}
47+
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerAuditSink.cs

Lines changed: 33 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@
1313
// limitations under the License.
1414

1515
using System;
16-
using System.Data;
17-
using System.Data.SqlClient;
18-
using System.Text;
1916
using Serilog.Core;
20-
using Serilog.Debugging;
2117
using Serilog.Events;
2218
using Serilog.Formatting;
19+
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Dependencies;
2320
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Options;
2421
using Serilog.Sinks.MSSqlServer.Sinks.MSSqlServer.Platform;
2522

@@ -31,8 +28,7 @@ namespace Serilog.Sinks.MSSqlServer
3128
/// </summary>
3229
public class MSSqlServerAuditSink : ILogEventSink, IDisposable
3330
{
34-
private readonly ISqlConnectionFactory _sqlConnectionFactory;
35-
private readonly MSSqlServerSinkTraits _traits;
31+
private readonly ISqlLogEventWriter _sqlLogEventWriter;
3632

3733
/// <summary>
3834
/// Construct a sink posting to the specified database.
@@ -76,99 +72,60 @@ public MSSqlServerAuditSink(
7672
IFormatProvider formatProvider = null,
7773
ColumnOptions columnOptions = null,
7874
ITextFormatter logEventFormatter = null)
75+
: this(sinkOptions, columnOptions,
76+
SinkDependenciesFactory.Create(connectionString, sinkOptions, formatProvider, columnOptions, logEventFormatter))
77+
{
78+
}
79+
80+
// Internal constructor with injectable dependencies for better testability
81+
internal MSSqlServerAuditSink(
82+
SinkOptions sinkOptions,
83+
ColumnOptions columnOptions,
84+
SinkDependencies sinkDependencies)
7985
{
8086
if (sinkOptions?.TableName == null)
8187
{
8288
throw new InvalidOperationException("Table name must be specified!");
8389
}
8490

85-
columnOptions?.FinalizeConfigurationForSinkConstructor();
91+
columnOptions = columnOptions ?? new ColumnOptions();
92+
columnOptions.FinalizeConfigurationForSinkConstructor();
93+
if (columnOptions.DisableTriggers)
94+
throw new NotSupportedException($"The {nameof(ColumnOptions.DisableTriggers)} option is not supported for auditing.");
8695

87-
if (columnOptions != null)
96+
if (sinkDependencies == null)
8897
{
89-
if (columnOptions.DisableTriggers)
90-
throw new NotSupportedException($"The {nameof(ColumnOptions.DisableTriggers)} option is not supported for auditing.");
98+
throw new ArgumentNullException(nameof(sinkDependencies));
9199
}
100+
_sqlLogEventWriter = sinkDependencies?.SqlLogEventWriter ?? throw new InvalidOperationException($"SqlLogEventWriter is not initialized!");
92101

93-
var azureManagedServiceAuthenticator = new AzureManagedServiceAuthenticator(sinkOptions.UseAzureManagedIdentity,
94-
sinkOptions.AzureServiceTokenProviderResource);
95-
_sqlConnectionFactory = new SqlConnectionFactory(connectionString, azureManagedServiceAuthenticator);
96-
97-
_traits = new MSSqlServerSinkTraits(_sqlConnectionFactory, sinkOptions.TableName, sinkOptions.SchemaName,
98-
columnOptions, formatProvider, sinkOptions.AutoCreateSqlTable, logEventFormatter);
99-
}
100-
101-
/// <summary>Emit the provided log event to the sink.</summary>
102-
/// <param name="logEvent">The log event to write.</param>
103-
public void Emit(LogEvent logEvent)
104-
{
105-
try
102+
if (sinkOptions.AutoCreateSqlTable)
106103
{
107-
using (var connection = _sqlConnectionFactory.Create())
104+
if (sinkDependencies?.DataTableCreator == null)
108105
{
109-
connection.Open();
110-
using (SqlCommand command = connection.CreateCommand())
111-
{
112-
command.CommandType = CommandType.Text;
113-
114-
var fieldList = new StringBuilder($"INSERT INTO [{_traits.SchemaName}].[{_traits.TableName}] (");
115-
var parameterList = new StringBuilder(") VALUES (");
116-
117-
var index = 0;
118-
foreach (var field in _traits.GetColumnsAndValues(logEvent))
119-
{
120-
if (index != 0)
121-
{
122-
fieldList.Append(',');
123-
parameterList.Append(',');
124-
}
125-
126-
fieldList.Append(field.Key);
127-
parameterList.Append("@P");
128-
parameterList.Append(index);
129-
130-
var parameter = new SqlParameter($"@P{index}", field.Value ?? DBNull.Value);
131-
132-
// The default is SqlDbType.DateTime, which will truncate the DateTime value if the actual
133-
// type in the database table is datetime2. So we explicitly set it to DateTime2, which will
134-
// work both if the field in the table is datetime and datetime2, which is also consistent with
135-
// the behavior of the non-audit sink.
136-
if (field.Value is DateTime)
137-
parameter.SqlDbType = SqlDbType.DateTime2;
138-
139-
command.Parameters.Add(parameter);
140-
141-
index++;
142-
}
143-
144-
parameterList.Append(')');
145-
fieldList.Append(parameterList.ToString());
146-
147-
command.CommandText = fieldList.ToString();
106+
throw new InvalidOperationException($"DataTableCreator is not initialized!");
107+
}
148108

149-
command.ExecuteNonQuery();
150-
}
109+
using (var eventTable = sinkDependencies.DataTableCreator.CreateDataTable(sinkOptions.TableName, columnOptions))
110+
{
111+
sinkDependencies.SqlTableCreator.CreateTable(sinkOptions.SchemaName, sinkOptions.TableName, eventTable, columnOptions);
151112
}
152113
}
153-
catch (Exception ex)
154-
{
155-
SelfLog.WriteLine("Unable to write log event to the database due to following error: {1}", ex.Message);
156-
throw;
157-
}
158114
}
159115

116+
/// <summary>Emit the provided log event to the sink.</summary>
117+
/// <param name="logEvent">The log event to write.</param>
118+
public void Emit(LogEvent logEvent) =>
119+
_sqlLogEventWriter.WriteEvent(logEvent);
120+
160121
/// <summary>
161122
/// Releases the unmanaged resources used by the Serilog.Sinks.MSSqlServer.MSSqlServerAuditSink and optionally
162123
/// releases the managed resources.
163124
/// </summary>
164-
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged
165-
/// resources.</param>
125+
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
166126
protected virtual void Dispose(bool disposing)
167127
{
168-
if (disposing)
169-
{
170-
_traits.Dispose();
171-
}
128+
// This class needn't to dispose anything. This is just here for sink interface compatibility.
172129
}
173130

174131
/// <summary>

0 commit comments

Comments
 (0)