Skip to content

Commit 1d8d2dd

Browse files
committed
Extracted LogEventDataGenerator, StandardColumnDataGenerator and PropertiesColumnDataGenerator from MSSqlServerSinkTraits.
1 parent 7bf2ebc commit 1d8d2dd

File tree

11 files changed

+311
-243
lines changed

11 files changed

+311
-243
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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +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")]
1310
[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)")]
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.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")]
1414
[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)")]
1515
[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")]
1616
[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")]

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSinkTraits.cs

Lines changed: 6 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
using System;
1616
using System.Collections.Generic;
1717
using System.Data;
18-
using System.Globalization;
19-
using System.Linq;
20-
using System.Text;
2118
using Serilog.Debugging;
2219
using Serilog.Events;
2320
using Serilog.Formatting;
@@ -30,16 +27,13 @@ namespace Serilog.Sinks.MSSqlServer
3027
/// <summary>Contains common functionality and properties used by both MSSqlServerSinks.</summary>
3128
internal class MSSqlServerSinkTraits : IDisposable
3229
{
30+
private readonly ILogEventDataGenerator _logEventDataGenerator;
3331
private bool _disposedValue;
3432

3533
public string TableName { get; }
3634
public string SchemaName { get; }
3735
public ColumnOptions ColumnOptions { get; }
38-
public IFormatProvider FormatProvider { get; }
39-
public ITextFormatter LogEventFormatter { get; }
40-
public ISet<string> AdditionalColumnPropertyNames { get; }
4136
public DataTable EventTable { get; }
42-
public ISet<string> StandardColumnNames { get; }
4337

4438
public MSSqlServerSinkTraits(
4539
ISqlConnectionFactory sqlConnectionFactory,
@@ -73,22 +67,11 @@ internal MSSqlServerSinkTraits(
7367
TableName = tableName;
7468
SchemaName = schemaName;
7569
ColumnOptions = columnOptions ?? new ColumnOptions();
76-
FormatProvider = formatProvider;
7770

78-
StandardColumnNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
79-
foreach (var stdCol in ColumnOptions.Store)
80-
{
81-
var col = ColumnOptions.GetStandardColumnOptions(stdCol);
82-
StandardColumnNames.Add(col.ColumnName);
83-
}
84-
85-
AdditionalColumnPropertyNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
86-
if (ColumnOptions.AdditionalColumns != null)
87-
foreach (var col in ColumnOptions.AdditionalColumns)
88-
AdditionalColumnPropertyNames.Add(col.PropertyName);
89-
90-
if (ColumnOptions.Store.Contains(StandardColumn.LogEvent))
91-
LogEventFormatter = logEventFormatter ?? new JsonLogEventFormatter(this);
71+
// TODO initialize this outside of this class
72+
var standardColumnDataGenerator = new StandardColumnDataGenerator(columnOptions, formatProvider, logEventFormatter);
73+
var propertiesColumnDataGenerator = new PropertiesColumnDataGenerator(columnOptions);
74+
_logEventDataGenerator = new LogEventDataGenerator(columnOptions, standardColumnDataGenerator, propertiesColumnDataGenerator);
9275

9376
EventTable = CreateDataTable();
9477

@@ -105,181 +88,9 @@ internal MSSqlServerSinkTraits(
10588
}
10689
}
10790

108-
/// <summary>Gets a list of the column names paired with their values to emit for the specified <paramref name="logEvent"/>.</summary>
109-
/// <param name="logEvent">The log event to emit.</param>
110-
/// <returns>
111-
/// A list of mappings between column names and values to emit to the database for the specified <paramref name="logEvent"/>.
112-
/// </returns>
11391
public IEnumerable<KeyValuePair<string, object>> GetColumnsAndValues(LogEvent logEvent)
11492
{
115-
foreach (var column in ColumnOptions.Store)
116-
{
117-
// skip Id (auto-incrementing identity)
118-
if (column != StandardColumn.Id)
119-
yield return GetStandardColumnNameAndValue(column, logEvent);
120-
}
121-
122-
if (ColumnOptions.AdditionalColumns != null)
123-
{
124-
foreach (var columnValuePair in ConvertPropertiesToColumn(logEvent.Properties))
125-
yield return columnValuePair;
126-
}
127-
}
128-
129-
internal KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColumn column, LogEvent logEvent)
130-
{
131-
switch (column)
132-
{
133-
case StandardColumn.Message:
134-
return new KeyValuePair<string, object>(ColumnOptions.Message.ColumnName, logEvent.RenderMessage(FormatProvider));
135-
case StandardColumn.MessageTemplate:
136-
return new KeyValuePair<string, object>(ColumnOptions.MessageTemplate.ColumnName, logEvent.MessageTemplate.Text);
137-
case StandardColumn.Level:
138-
return new KeyValuePair<string, object>(ColumnOptions.Level.ColumnName, ColumnOptions.Level.StoreAsEnum ? (object)logEvent.Level : logEvent.Level.ToString());
139-
case StandardColumn.TimeStamp:
140-
return GetTimeStampStandardColumnNameAndValue(logEvent);
141-
case StandardColumn.Exception:
142-
return new KeyValuePair<string, object>(ColumnOptions.Exception.ColumnName, logEvent.Exception?.ToString());
143-
case StandardColumn.Properties:
144-
return new KeyValuePair<string, object>(ColumnOptions.Properties.ColumnName, ConvertPropertiesToXmlStructure(logEvent.Properties));
145-
case StandardColumn.LogEvent:
146-
return new KeyValuePair<string, object>(ColumnOptions.LogEvent.ColumnName, RenderLogEventColumn(logEvent));
147-
default:
148-
throw new ArgumentOutOfRangeException(nameof(column));
149-
}
150-
}
151-
152-
private KeyValuePair<string, object> GetTimeStampStandardColumnNameAndValue(LogEvent logEvent)
153-
{
154-
var dateTimeOffset = ColumnOptions.TimeStamp.ConvertToUtc ? logEvent.Timestamp.ToUniversalTime() : logEvent.Timestamp;
155-
156-
if (ColumnOptions.TimeStamp.DataType == SqlDbType.DateTimeOffset)
157-
return new KeyValuePair<string, object>(ColumnOptions.TimeStamp.ColumnName, dateTimeOffset);
158-
159-
return new KeyValuePair<string, object>(ColumnOptions.TimeStamp.ColumnName, dateTimeOffset.DateTime);
160-
}
161-
162-
private string RenderLogEventColumn(LogEvent logEvent)
163-
{
164-
if (ColumnOptions.LogEvent.ExcludeAdditionalProperties)
165-
{
166-
var filteredProperties = logEvent.Properties.Where(p => !AdditionalColumnPropertyNames.Contains(p.Key));
167-
logEvent = new LogEvent(logEvent.Timestamp, logEvent.Level, logEvent.Exception, logEvent.MessageTemplate, filteredProperties.Select(x => new LogEventProperty(x.Key, x.Value)));
168-
}
169-
170-
var sb = new StringBuilder();
171-
using (var writer = new System.IO.StringWriter(sb))
172-
LogEventFormatter.Format(logEvent, writer);
173-
return sb.ToString();
174-
}
175-
176-
private string ConvertPropertiesToXmlStructure(IEnumerable<KeyValuePair<string, LogEventPropertyValue>> properties)
177-
{
178-
var options = ColumnOptions.Properties;
179-
180-
if (options.ExcludeAdditionalProperties)
181-
properties = properties.Where(p => !AdditionalColumnPropertyNames.Contains(p.Key));
182-
183-
if (options.PropertiesFilter != null)
184-
{
185-
try
186-
{
187-
properties = properties.Where(p => options.PropertiesFilter(p.Key));
188-
}
189-
catch (Exception ex)
190-
{
191-
SelfLog.WriteLine("Unable to filter properties to store in {0} due to following error: {1}", this, ex);
192-
}
193-
}
194-
195-
var sb = new StringBuilder();
196-
197-
sb.AppendFormat(CultureInfo.InvariantCulture, "<{0}>", options.RootElementName);
198-
199-
foreach (var property in properties)
200-
{
201-
var value = XmlPropertyFormatter.Simplify(property.Value, options);
202-
if (options.OmitElementIfEmpty && string.IsNullOrEmpty(value))
203-
{
204-
continue;
205-
}
206-
207-
if (options.UsePropertyKeyAsElementName)
208-
{
209-
sb.AppendFormat(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", XmlPropertyFormatter.GetValidElementName(property.Key), value);
210-
}
211-
else
212-
{
213-
sb.AppendFormat(CultureInfo.InvariantCulture, "<{0} key='{1}'>{2}</{0}>", options.PropertyElementName, property.Key, value);
214-
}
215-
}
216-
217-
sb.AppendFormat(CultureInfo.InvariantCulture, "</{0}>", options.RootElementName);
218-
219-
return sb.ToString();
220-
}
221-
222-
/// <summary>
223-
/// Mapping values from properties which have a corresponding data row.
224-
/// Matching is done based on Column name and property key
225-
/// Standard columns are not mapped
226-
/// </summary>
227-
/// <param name="properties"></param>
228-
private IEnumerable<KeyValuePair<string, object>> ConvertPropertiesToColumn(IReadOnlyDictionary<string, LogEventPropertyValue> properties)
229-
{
230-
foreach (var property in properties)
231-
{
232-
var additionalColumn = ColumnOptions
233-
.AdditionalColumns
234-
.FirstOrDefault(ac => ac.PropertyName == property.Key);
235-
236-
if (additionalColumn == null || StandardColumnNames.Contains(property.Key))
237-
continue;
238-
239-
var columnName = additionalColumn.ColumnName;
240-
var columnType = EventTable.Columns[columnName].DataType;
241-
242-
if (!(property.Value is ScalarValue scalarValue))
243-
{
244-
yield return new KeyValuePair<string, object>(columnName, property.Value.ToString());
245-
continue;
246-
}
247-
248-
if (scalarValue.Value == null && EventTable.Columns[columnName].AllowDBNull)
249-
{
250-
yield return new KeyValuePair<string, object>(columnName, DBNull.Value);
251-
continue;
252-
}
253-
254-
if (TryChangeType(scalarValue.Value, columnType, out var conversion))
255-
{
256-
yield return new KeyValuePair<string, object>(columnName, conversion);
257-
}
258-
else
259-
{
260-
yield return new KeyValuePair<string, object>(columnName, property.Value.ToString());
261-
}
262-
}
263-
}
264-
265-
/// <summary>
266-
/// Try to convert the object to the given type
267-
/// </summary>
268-
/// <param name="obj">object</param>
269-
/// <param name="type">type to convert to</param>
270-
/// <param name="conversion">result of the converted value</param>
271-
private static bool TryChangeType(object obj, Type type, out object conversion)
272-
{
273-
conversion = null;
274-
try
275-
{
276-
conversion = Convert.ChangeType(obj, type, CultureInfo.InvariantCulture);
277-
return true;
278-
}
279-
catch
280-
{
281-
return false;
282-
}
93+
return _logEventDataGenerator.GetColumnsAndValues(logEvent);
28394
}
28495

28596
private DataTable CreateDataTable()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Collections.Generic;
2+
using Serilog.Events;
3+
4+
namespace Serilog.Sinks.MSSqlServer.Output
5+
{
6+
internal interface ILogEventDataGenerator
7+
{
8+
IEnumerable<KeyValuePair<string, object>> GetColumnsAndValues(LogEvent logEvent);
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Collections.Generic;
2+
using Serilog.Events;
3+
4+
namespace Serilog.Sinks.MSSqlServer.Output
5+
{
6+
internal interface IPropertiesColumnDataGenerator
7+
{
8+
IEnumerable<KeyValuePair<string, object>> ConvertPropertiesToColumn(IReadOnlyDictionary<string, LogEventPropertyValue> properties);
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Collections.Generic;
2+
using Serilog.Events;
3+
4+
namespace Serilog.Sinks.MSSqlServer.Output
5+
{
6+
internal interface IStandardColumnDataGenerator
7+
{
8+
KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColumn column, LogEvent logEvent);
9+
}
10+
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Output/JsonLogEventFormatter.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ namespace Serilog.Sinks.MSSqlServer.Output
3131
internal class JsonLogEventFormatter : ITextFormatter
3232
{
3333
private const string _commaDelimiter = ",";
34+
private readonly ColumnOptions _columnOptions;
35+
private readonly IStandardColumnDataGenerator _standardColumnsDataGenerator;
3436
private readonly JsonValueFormatter _valueFormatter;
35-
private readonly MSSqlServerSinkTraits _traits;
3637

3738
/// <summary>
38-
/// Constructor. A reference to the parent Traits object is used so that JSON
39+
/// Constructor. A reference to the parent IStandardColumnsDataGenerator object is used so that JSON
3940
/// can serialize Standard Column values exactly the way they would be written
4041
/// to discrete SQL columns.
4142
/// </summary>
42-
public JsonLogEventFormatter(MSSqlServerSinkTraits parent)
43+
public JsonLogEventFormatter(ColumnOptions columnOptions, IStandardColumnDataGenerator standardColumnsDataGenerator)
4344
{
44-
_traits = parent;
45+
_columnOptions = columnOptions ?? throw new ArgumentNullException(nameof(columnOptions));
46+
_standardColumnsDataGenerator = standardColumnsDataGenerator ?? throw new ArgumentNullException(nameof(standardColumnsDataGenerator));
4547
_valueFormatter = new JsonValueFormatter(typeTagName: null);
4648
}
4749

@@ -57,7 +59,7 @@ public void Format(LogEvent logEvent, TextWriter output)
5759

5860
var precedingDelimiter = "";
5961

60-
if (_traits.ColumnOptions.LogEvent.ExcludeStandardColumns == false)
62+
if (_columnOptions.LogEvent.ExcludeStandardColumns == false)
6163
{
6264
// The XML Properties column has never included the Standard Columns, but prior
6365
// to adding this sink-specific JSON formatter, the LogEvent JSON column relied
@@ -104,7 +106,7 @@ private void WriteStandardColumns(LogEvent logEvent, TextWriter output, ref stri
104106

105107
private void WriteIfPresent(StandardColumn col, LogEvent logEvent, TextWriter output, ref string precedingDelimiter)
106108
{
107-
if (!_traits.ColumnOptions.Store.Contains(col))
109+
if (!_columnOptions.Store.Contains(col))
108110
return;
109111

110112
output.Write(precedingDelimiter);
@@ -116,21 +118,21 @@ private void WriteIfPresent(StandardColumn col, LogEvent logEvent, TextWriter ou
116118

117119
private void WriteTimeStampIfPresent(LogEvent logEvent, TextWriter output, ref string precedingDelimiter)
118120
{
119-
if (!_traits.ColumnOptions.Store.Contains(StandardColumn.TimeStamp))
121+
if (!_columnOptions.Store.Contains(StandardColumn.TimeStamp))
120122
return;
121123

122124
output.Write(precedingDelimiter);
123125
precedingDelimiter = _commaDelimiter;
124126
var colData = WritePropertyName(logEvent, output, StandardColumn.TimeStamp);
125-
var value = _traits.ColumnOptions.TimeStamp.DataType == SqlDbType.DateTime
127+
var value = _columnOptions.TimeStamp.DataType == SqlDbType.DateTime
126128
? ((DateTime)colData.Value).ToString("o", CultureInfo.InvariantCulture)
127129
: ((DateTimeOffset)colData.Value).ToString("o", CultureInfo.InvariantCulture);
128130
JsonValueFormatter.WriteQuotedJsonString(value, output);
129131
}
130132

131133
private KeyValuePair<string, object> WritePropertyName(LogEvent le, TextWriter writer, StandardColumn col)
132134
{
133-
var colData = _traits.GetStandardColumnNameAndValue(col, le);
135+
var colData = _standardColumnsDataGenerator.GetStandardColumnNameAndValue(col, le);
134136
JsonValueFormatter.WriteQuotedJsonString(colData.Key, writer);
135137
writer.Write(":");
136138

0 commit comments

Comments
 (0)