Skip to content

Commit 9edb6e2

Browse files
alphaleonisbacklune
authored andcommitted
Feature/audit sink (#1)
* Updated reference to Serilog version 2.5.0 as suggested in #110 * Added a new Sink `MSSqlServerAuditSink` that persists LogEvents to the database immediately and propagates any errors that occur. * Moved, and refactored common functionality from `MSSqlServerSink` into `MSSqlServerSinkTraits` which is now used by both sinks.
1 parent a6fc78e commit 9edb6e2

File tree

10 files changed

+797
-312
lines changed

10 files changed

+797
-312
lines changed

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
root = true
2+
3+
[*]
4+
trim_trailing_whitespace = true
5+
insert_final_newline = true
6+
indent_style = space
7+
indent_size = 4
8+
9+
[*.{csproj,json,config,yml}]
10+
indent_size = 2
11+
12+
[*.sh]
13+
end_of_line = lf
14+
15+
[*.{cmd, bat}]
16+
end_of_line = crlf

src/Serilog.Sinks.MSSqlServer/LoggerConfigurationMSSqlServerExtensions.cs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,60 @@ public static LoggerConfiguration MSSqlServer(
9393
restrictedToMinimumLevel);
9494
}
9595

96+
/// <summary>
97+
/// Adds a sink that writes log events to a table in a MSSqlServer database.
98+
/// Create a database and execute the table creation script found here
99+
/// https://gist.github.com/mivano/10429656
100+
/// or use the autoCreateSqlTable option.
101+
/// </summary>
102+
/// <param name="loggerAuditSinkConfiguration">The logger configuration.</param>
103+
/// <param name="connectionString">The connection string to the database where to store the events.</param>
104+
/// <param name="tableName">Name of the table to store the events in.</param>
105+
/// <param name="schemaName">Name of the schema for the table to store the data in. The default is 'dbo'.</param>
106+
/// <param name="restrictedToMinimumLevel">The minimum log event level required in order to write an event to the sink.</param>
107+
/// <param name="formatProvider">Supplies culture-specific formatting information, or null.</param>
108+
/// <param name="autoCreateSqlTable">Create log table with the provided name on destination sql server.</param>
109+
/// <param name="columnOptions"></param>
110+
/// <returns>Logger configuration, allowing configuration to continue.</returns>
111+
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
112+
public static LoggerConfiguration MSSqlServer(this LoggerAuditSinkConfiguration loggerAuditSinkConfiguration,
113+
string connectionString,
114+
string tableName,
115+
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
116+
IFormatProvider formatProvider = null,
117+
bool autoCreateSqlTable = false,
118+
ColumnOptions columnOptions = null,
119+
string schemaName = "dbo")
120+
{
121+
if (loggerAuditSinkConfiguration == null) throw new ArgumentNullException("loggerAuditSinkConfiguration");
122+
123+
MSSqlServerConfigurationSection serviceConfigSection =
124+
ConfigurationManager.GetSection("MSSqlServerSettingsSection") as MSSqlServerConfigurationSection;
125+
126+
// If we have additional columns from config, load them as well
127+
if (serviceConfigSection != null && serviceConfigSection.Columns.Count > 0)
128+
{
129+
if (columnOptions == null)
130+
{
131+
columnOptions = new ColumnOptions();
132+
}
133+
GenerateDataColumnsFromConfig(serviceConfigSection, columnOptions);
134+
}
135+
136+
connectionString = GetConnectionString(connectionString);
137+
138+
return loggerAuditSinkConfiguration.Sink(
139+
new MSSqlServerAuditSink(
140+
connectionString,
141+
tableName,
142+
formatProvider,
143+
autoCreateSqlTable,
144+
columnOptions,
145+
schemaName
146+
),
147+
restrictedToMinimumLevel);
148+
}
149+
96150
/// <summary>
97151
/// Examine if supplied connection string is a reference to an item in the "ConnectionStrings" section of web.config
98152
/// If it is, return the ConnectionStrings item, if not, return string as supplied.
@@ -101,7 +155,7 @@ public static LoggerConfiguration MSSqlServer(
101155
/// <remarks>Pulled from review of Entity Framework 6 methodology for doing the same</remarks>
102156
private static string GetConnectionString(string nameOrConnectionString)
103157
{
104-
158+
105159
// If there is an `=`, we assume this is a raw connection string not a named value
106160
// If there are no `=`, attempt to pull the named value from config
107161
if (nameOrConnectionString.IndexOf('=') < 0)
@@ -140,48 +194,48 @@ private static void GenerateDataColumnsFromConfig(MSSqlServerConfigurationSectio
140194
switch (c.DataType)
141195
{
142196
case "bigint":
143-
dataType = Type.GetType("System.Int64");
197+
dataType = typeof(long);
144198
break;
145199
case "bit":
146-
dataType = Type.GetType("System.Boolean");
200+
dataType = typeof(bool);
147201
break;
148202
case "char":
149203
case "nchar":
150204
case "ntext":
151205
case "nvarchar":
152206
case "text":
153207
case "varchar":
154-
dataType = Type.GetType("System.String");
208+
dataType = typeof(string);
155209
break;
156210
case "date":
157211
case "datetime":
158212
case "datetime2":
159213
case "smalldatetime":
160-
dataType = Type.GetType("System.DateTime");
214+
dataType = typeof(DateTime);
161215
break;
162216
case "decimal":
163217
case "money":
164218
case "numeric":
165219
case "smallmoney":
166-
dataType = Type.GetType("System.Decimal");
220+
dataType = typeof(Decimal);
167221
break;
168222
case "float":
169-
dataType = Type.GetType("System.Double");
223+
dataType = typeof(double);
170224
break;
171225
case "int":
172-
dataType = Type.GetType("System.Int32");
226+
dataType = typeof(int);
173227
break;
174228
case "real":
175-
dataType = Type.GetType("System.Single");
229+
dataType = typeof(float);
176230
break;
177231
case "smallint":
178-
dataType = Type.GetType("System.Int16");
232+
dataType = typeof(short);
179233
break;
180234
case "time":
181-
dataType = Type.GetType("System.TimeSpan");
235+
dataType = typeof(TimeSpan);
182236
break;
183237
case "uniqueidentifier":
184-
dataType = Type.GetType("System.Guid");
238+
dataType = typeof(Guid);
185239
break;
186240
}
187241
column.DataType = dataType;
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2018 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Serilog.Core;
16+
using Serilog.Debugging;
17+
using Serilog.Events;
18+
using System;
19+
using System.Data;
20+
using System.Data.SqlClient;
21+
using System.Linq;
22+
using System.Text;
23+
24+
namespace Serilog.Sinks.MSSqlServer
25+
{
26+
/// <summary>
27+
/// Writes log events as rows in a table of MSSqlServer database using Audit logic, meaning that each row is synchronously committed
28+
/// and any errors that occur are propagated to the caller.
29+
/// </summary>
30+
public class MSSqlServerAuditSink : ILogEventSink, IDisposable
31+
{
32+
private readonly MSSqlServerSinkTraits _traits;
33+
34+
/// <summary>
35+
/// Construct a sink posting to the specified database.
36+
/// </summary>
37+
/// <param name="connectionString">Connection string to access the database.</param>
38+
/// <param name="tableName">Name of the table to store the data in.</param>
39+
/// <param name="schemaName">Name of the schema for the table to store the data in. The default is 'dbo'.</param>
40+
/// <param name="formatProvider">Supplies culture-specific formatting information, or null.</param>
41+
/// <param name="autoCreateSqlTable">Create log table with the provided name on destination sql server.</param>
42+
/// <param name="columnOptions">Options that pertain to columns</param>
43+
public MSSqlServerAuditSink(
44+
string connectionString,
45+
string tableName,
46+
IFormatProvider formatProvider,
47+
bool autoCreateSqlTable = false,
48+
ColumnOptions columnOptions = null,
49+
string schemaName = "dbo"
50+
)
51+
{
52+
if (columnOptions != null)
53+
{
54+
if (columnOptions.DisableTriggers)
55+
throw new NotSupportedException($"The {nameof(ColumnOptions.DisableTriggers)} option is not supported for auditing.");
56+
}
57+
_traits = new MSSqlServerSinkTraits(connectionString, tableName, schemaName, columnOptions, formatProvider, autoCreateSqlTable);
58+
59+
60+
}
61+
62+
/// <summary>Emit the provided log event to the sink.</summary>
63+
/// <param name="logEvent">The log event to write.</param>
64+
public void Emit(LogEvent logEvent)
65+
{
66+
try
67+
{
68+
using (SqlConnection connection = new SqlConnection(_traits.ConnectionString))
69+
{
70+
connection.Open();
71+
using (SqlCommand command = connection.CreateCommand())
72+
{
73+
command.CommandType = CommandType.Text;
74+
75+
StringBuilder fieldList = new StringBuilder($"INSERT INTO [{_traits.SchemaName}].[{_traits.TableName}] (");
76+
StringBuilder parameterList = new StringBuilder(") VALUES (");
77+
78+
int index = 0;
79+
foreach (var field in _traits.GetColumnsAndValues(logEvent))
80+
{
81+
if (index != 0)
82+
{
83+
fieldList.Append(',');
84+
parameterList.Append(',');
85+
}
86+
87+
fieldList.Append(field.Key);
88+
parameterList.Append("@P");
89+
parameterList.Append(index);
90+
91+
SqlParameter parameter = new SqlParameter($"@P{index}", field.Value ?? DBNull.Value);
92+
93+
// The default is SqlDbType.DateTime, which will truncate the DateTime value if the actual
94+
// type in the database table is datetime2. So we explicitly set it to DateTime2, which will
95+
// work both if the field in the table is datetime and datetime2, which is also consistent with
96+
// the behavior of the non-audit sink.
97+
if (field.Value is DateTime)
98+
parameter.SqlDbType = SqlDbType.DateTime2;
99+
100+
command.Parameters.Add(parameter);
101+
102+
index++;
103+
}
104+
105+
parameterList.Append(')');
106+
fieldList.Append(parameterList.ToString());
107+
108+
command.CommandText = fieldList.ToString();
109+
110+
command.ExecuteNonQuery();
111+
}
112+
}
113+
}
114+
catch (Exception ex)
115+
{
116+
SelfLog.WriteLine("Unable to write log event to the database due to following error: {1}", ex.Message);
117+
throw;
118+
}
119+
}
120+
121+
/// <summary>
122+
/// Releases the unmanaged resources used by the Serilog.Sinks.MSSqlServer.MSSqlServerAuditSink and optionally
123+
/// releases the managed resources.
124+
/// </summary>
125+
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged
126+
/// resources.</param>
127+
protected virtual void Dispose(bool disposing)
128+
{
129+
if (disposing)
130+
{
131+
_traits.Dispose();
132+
}
133+
}
134+
135+
/// <summary>
136+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
137+
/// </summary>
138+
public void Dispose()
139+
{
140+
Dispose(true);
141+
GC.SuppressFinalize(this);
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)