Skip to content

Commit 5786367

Browse files
authored
Merge pull request #510 from BrettJaner/feat/insert-statement-batch-writer
Add SqlInsertStatementWriter which uses INSERT statements instead of SqlBulkCopy
2 parents 246a463 + 4db4513 commit 5786367

File tree

16 files changed

+293
-138
lines changed

16 files changed

+293
-138
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ Basic settings of the sink are configured using the properties in a `MSSqlServer
246246
* `BatchPeriod`
247247
* `EagerlyEmitFirstEvent`
248248
* `LevelSwitch`
249+
* `UseSqlBulkCopy`
249250

250251
### TableName
251252

@@ -291,6 +292,11 @@ This setting is not used by the audit sink as it writes each event immediately a
291292

292293
A switch allowing the pass-through minimum level to be changed at runtime. If this is set, the parameter `restrictedToMinimumLevel` in the [sink configuration method](#sink-configuration) is ignored.
293294

295+
### UseSqlBulkCopy
296+
297+
A flag to use `SqlBulkCopy` instead of individual INSERT statements when writing log events. The default is `true`.
298+
This setting is not used by the audit sink as it always uses INSERT statements to write events.
299+
294300
## ColumnOptions Object
295301

296302
Features of the log table are defined by changing properties on a `ColumnOptions` object:
@@ -319,7 +325,7 @@ Setting this to `true` changes the table to the clustered columnstore index (CCI
319325

320326
### DisableTriggers
321327

322-
Disabling triggers can significantly improve batch-write performance.
328+
Disabling triggers can significantly improve batch-write performance. Only applies when `SqlBulkCopy` is used.
323329

324330
### AdditionalColumns
325331

src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsSinkOptionsProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private static void ReadBatchSettings(IConfigurationSection config, MSSqlServerS
3333
SetProperty.IfNotNull<int>(config["batchPostingLimit"], val => sinkOptions.BatchPostingLimit = val);
3434
SetProperty.IfNotNull<string>(config["batchPeriod"], val => sinkOptions.BatchPeriod = TimeSpan.Parse(val, CultureInfo.InvariantCulture));
3535
SetProperty.IfNotNull<bool>(config["eagerlyEmitFirstEvent"], val => sinkOptions.EagerlyEmitFirstEvent = val);
36+
SetProperty.IfNotNull<bool>(config["useSqlBulkCopy"], val => sinkOptions.UseSqlBulkCopy = val);
3637
}
3738
}
3839
}

src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/MSSqlServerConfigurationSection.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ public ValueConfigElement EagerlyEmitFirstEvent
198198
{
199199
get => (ValueConfigElement)base[nameof(EagerlyEmitFirstEvent)];
200200
}
201+
202+
[ConfigurationProperty(nameof(UseSqlBulkCopy))]
203+
public ValueConfigElement UseSqlBulkCopy
204+
{
205+
get => (ValueConfigElement)base[nameof(UseSqlBulkCopy)];
206+
}
201207
}
202208
}
203209

src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationSinkOptionsProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ private static void ReadBatchSettings(MSSqlServerConfigurationSection config, MS
3232
SetProperty.IfProvided<string>(config.BatchPeriod, nameof(config.BatchPeriod.Value), value => sinkOptions.BatchPeriod = TimeSpan.Parse(value, CultureInfo.InvariantCulture));
3333
SetProperty.IfProvided<bool>(config.EagerlyEmitFirstEvent, nameof(config.EagerlyEmitFirstEvent.Value),
3434
value => sinkOptions.EagerlyEmitFirstEvent = value);
35+
SetProperty.IfProvided<bool>(config.UseSqlBulkCopy, nameof(config.UseSqlBulkCopy.Value),
36+
value => sinkOptions.UseSqlBulkCopy = value);
3537
}
3638
}
3739
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/ColumnOptions/ColumnOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public ICollection<StandardColumn> Store
9090

9191
/// <summary>
9292
/// Indicates if triggers should be disabled when inserting log entries.
93+
/// Only applies when SqlBulkCopy is used.
9394
/// </summary>
9495
public bool DisableTriggers { get; set; }
9596

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependenciesFactory.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ internal static SinkDependencies Create(
5252
sqlCreateDatabaseWriter, sqlConnectionFactoryNoDb),
5353
SqlTableCreator = new SqlTableCreator(
5454
sqlCreateTableWriter, sqlConnectionFactory),
55-
SqlBulkBatchWriter = new SqlBulkBatchWriter(
56-
sinkOptions.TableName, sinkOptions.SchemaName, columnOptions.DisableTriggers,
57-
sqlConnectionFactory, logEventDataGenerator),
58-
SqlLogEventWriter = new SqlLogEventWriter(
55+
SqlBulkBatchWriter = sinkOptions.UseSqlBulkCopy
56+
? (ISqlBulkBatchWriter)new SqlBulkBatchWriter(
57+
sinkOptions.TableName, sinkOptions.SchemaName, columnOptions.DisableTriggers,
58+
sqlConnectionFactory, logEventDataGenerator)
59+
: (ISqlBulkBatchWriter)new SqlInsertStatementWriter(
60+
sinkOptions.TableName, sinkOptions.SchemaName,
61+
sqlConnectionFactory, logEventDataGenerator),
62+
SqlLogEventWriter = new SqlInsertStatementWriter(
5963
sinkOptions.TableName, sinkOptions.SchemaName,
6064
sqlConnectionFactory, logEventDataGenerator)
6165
};

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSinkOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public MSSqlServerSinkOptions()
1717
BatchPostingLimit = MSSqlServerSink.DefaultBatchPostingLimit;
1818
BatchPeriod = MSSqlServerSink.DefaultPeriod;
1919
EagerlyEmitFirstEvent = true;
20+
UseSqlBulkCopy = true;
2021
}
2122

2223
internal MSSqlServerSinkOptions(
@@ -77,5 +78,10 @@ internal MSSqlServerSinkOptions(
7778
/// A switch allowing the pass-through minimum level to be changed at runtime
7879
/// </summary>
7980
public LoggingLevelSwitch LevelSwitch { get; set; }
81+
82+
/// <summary>
83+
/// Flag to use <see cref="Microsoft.Data.SqlClient.SqlBulkCopy"/> instead of individual INSERT statements (default: true)
84+
/// </summary>
85+
public bool UseSqlBulkCopy { get; set; }
8086
}
8187
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/ISqlCommandWrapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Data;
3+
using System.Threading.Tasks;
34

45
namespace Serilog.Sinks.MSSqlServer.Platform.SqlClient
56
{
@@ -10,5 +11,6 @@ internal interface ISqlCommandWrapper : IDisposable
1011

1112
void AddParameter(string parameterName, object value);
1213
int ExecuteNonQuery();
14+
Task<int> ExecuteNonQueryAsync();
1315
}
1416
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/SqlCommandWrapper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Data;
3+
using System.Threading.Tasks;
34
using Microsoft.Data.SqlClient;
45

56
namespace Serilog.Sinks.MSSqlServer.Platform.SqlClient
@@ -43,6 +44,9 @@ public void AddParameter(string parameterName, object value)
4344
public int ExecuteNonQuery() =>
4445
_sqlCommand.ExecuteNonQuery();
4546

47+
public Task<int> ExecuteNonQueryAsync() =>
48+
_sqlCommand.ExecuteNonQueryAsync();
49+
4650
protected virtual void Dispose(bool disposing)
4751
{
4852
if (!_disposedValue)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Serilog.Debugging;
7+
using Serilog.Events;
8+
using Serilog.Sinks.MSSqlServer.Output;
9+
using static System.FormattableString;
10+
11+
namespace Serilog.Sinks.MSSqlServer.Platform
12+
{
13+
internal class SqlInsertStatementWriter : ISqlBulkBatchWriter, ISqlLogEventWriter
14+
{
15+
private readonly string _tableName;
16+
private readonly string _schemaName;
17+
private readonly ISqlConnectionFactory _sqlConnectionFactory;
18+
private readonly ILogEventDataGenerator _logEventDataGenerator;
19+
20+
public SqlInsertStatementWriter(
21+
string tableName,
22+
string schemaName,
23+
ISqlConnectionFactory sqlConnectionFactory,
24+
ILogEventDataGenerator logEventDataGenerator)
25+
{
26+
_tableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
27+
_schemaName = schemaName ?? throw new ArgumentNullException(nameof(schemaName));
28+
_sqlConnectionFactory = sqlConnectionFactory ?? throw new ArgumentNullException(nameof(sqlConnectionFactory));
29+
_logEventDataGenerator = logEventDataGenerator ?? throw new ArgumentNullException(nameof(logEventDataGenerator));
30+
}
31+
32+
public Task WriteBatch(IEnumerable<LogEvent> events, DataTable dataTable) => WriteBatch(events);
33+
34+
public void WriteEvent(LogEvent logEvent) => WriteBatch(new[] { logEvent }).GetAwaiter().GetResult();
35+
36+
public async Task WriteBatch(IEnumerable<LogEvent> events)
37+
{
38+
try
39+
{
40+
using (var cn = _sqlConnectionFactory.Create())
41+
{
42+
await cn.OpenAsync().ConfigureAwait(false);
43+
44+
foreach (var logEvent in events)
45+
{
46+
using (var command = cn.CreateCommand())
47+
{
48+
command.CommandType = CommandType.Text;
49+
50+
var fieldList = new StringBuilder(Invariant($"INSERT INTO [{_schemaName}].[{_tableName}] ("));
51+
var parameterList = new StringBuilder(") VALUES (");
52+
53+
var index = 0;
54+
foreach (var field in _logEventDataGenerator.GetColumnsAndValues(logEvent))
55+
{
56+
if (index != 0)
57+
{
58+
fieldList.Append(',');
59+
parameterList.Append(',');
60+
}
61+
62+
fieldList.Append(Invariant($"[{field.Key}]"));
63+
parameterList.Append("@P");
64+
parameterList.Append(index);
65+
66+
command.AddParameter(Invariant($"@P{index}"), field.Value);
67+
68+
index++;
69+
}
70+
71+
parameterList.Append(')');
72+
fieldList.Append(parameterList);
73+
74+
command.CommandText = fieldList.ToString();
75+
76+
await command.ExecuteNonQueryAsync().ConfigureAwait(false);
77+
}
78+
}
79+
}
80+
}
81+
catch (Exception ex)
82+
{
83+
SelfLog.WriteLine("Unable to write log event to the database due to following error: {0}", ex);
84+
throw;
85+
}
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)