Skip to content

Commit

Permalink
Feature/audit sink (#1)
Browse files Browse the repository at this point in the history
* Updated reference to Serilog version 2.5.0 as suggested in serilog-mssql#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.
  • Loading branch information
alphaleonis authored and backlune committed Apr 18, 2018
1 parent a6fc78e commit 9edb6e2
Show file tree
Hide file tree
Showing 10 changed files with 797 additions and 312 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4

[*.{csproj,json,config,yml}]
indent_size = 2

[*.sh]
end_of_line = lf

[*.{cmd, bat}]
end_of_line = crlf
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,60 @@ public static LoggerConfiguration MSSqlServer(
restrictedToMinimumLevel);
}

/// <summary>
/// Adds a sink that writes log events to a table in a MSSqlServer database.
/// Create a database and execute the table creation script found here
/// https://gist.github.com/mivano/10429656
/// or use the autoCreateSqlTable option.
/// </summary>
/// <param name="loggerAuditSinkConfiguration">The logger configuration.</param>
/// <param name="connectionString">The connection string to the database where to store the events.</param>
/// <param name="tableName">Name of the table to store the events in.</param>
/// <param name="schemaName">Name of the schema for the table to store the data in. The default is 'dbo'.</param>
/// <param name="restrictedToMinimumLevel">The minimum log event level required in order to write an event to the sink.</param>
/// <param name="formatProvider">Supplies culture-specific formatting information, or null.</param>
/// <param name="autoCreateSqlTable">Create log table with the provided name on destination sql server.</param>
/// <param name="columnOptions"></param>
/// <returns>Logger configuration, allowing configuration to continue.</returns>
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
public static LoggerConfiguration MSSqlServer(this LoggerAuditSinkConfiguration loggerAuditSinkConfiguration,
string connectionString,
string tableName,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
IFormatProvider formatProvider = null,
bool autoCreateSqlTable = false,
ColumnOptions columnOptions = null,
string schemaName = "dbo")
{
if (loggerAuditSinkConfiguration == null) throw new ArgumentNullException("loggerAuditSinkConfiguration");

MSSqlServerConfigurationSection serviceConfigSection =
ConfigurationManager.GetSection("MSSqlServerSettingsSection") as MSSqlServerConfigurationSection;

// If we have additional columns from config, load them as well
if (serviceConfigSection != null && serviceConfigSection.Columns.Count > 0)
{
if (columnOptions == null)
{
columnOptions = new ColumnOptions();
}
GenerateDataColumnsFromConfig(serviceConfigSection, columnOptions);
}

connectionString = GetConnectionString(connectionString);

return loggerAuditSinkConfiguration.Sink(
new MSSqlServerAuditSink(
connectionString,
tableName,
formatProvider,
autoCreateSqlTable,
columnOptions,
schemaName
),
restrictedToMinimumLevel);
}

/// <summary>
/// Examine if supplied connection string is a reference to an item in the "ConnectionStrings" section of web.config
/// If it is, return the ConnectionStrings item, if not, return string as supplied.
Expand All @@ -101,7 +155,7 @@ public static LoggerConfiguration MSSqlServer(
/// <remarks>Pulled from review of Entity Framework 6 methodology for doing the same</remarks>
private static string GetConnectionString(string nameOrConnectionString)
{

// If there is an `=`, we assume this is a raw connection string not a named value
// If there are no `=`, attempt to pull the named value from config
if (nameOrConnectionString.IndexOf('=') < 0)
Expand Down Expand Up @@ -140,48 +194,48 @@ private static void GenerateDataColumnsFromConfig(MSSqlServerConfigurationSectio
switch (c.DataType)
{
case "bigint":
dataType = Type.GetType("System.Int64");
dataType = typeof(long);
break;
case "bit":
dataType = Type.GetType("System.Boolean");
dataType = typeof(bool);
break;
case "char":
case "nchar":
case "ntext":
case "nvarchar":
case "text":
case "varchar":
dataType = Type.GetType("System.String");
dataType = typeof(string);
break;
case "date":
case "datetime":
case "datetime2":
case "smalldatetime":
dataType = Type.GetType("System.DateTime");
dataType = typeof(DateTime);
break;
case "decimal":
case "money":
case "numeric":
case "smallmoney":
dataType = Type.GetType("System.Decimal");
dataType = typeof(Decimal);
break;
case "float":
dataType = Type.GetType("System.Double");
dataType = typeof(double);
break;
case "int":
dataType = Type.GetType("System.Int32");
dataType = typeof(int);
break;
case "real":
dataType = Type.GetType("System.Single");
dataType = typeof(float);
break;
case "smallint":
dataType = Type.GetType("System.Int16");
dataType = typeof(short);
break;
case "time":
dataType = Type.GetType("System.TimeSpan");
dataType = typeof(TimeSpan);
break;
case "uniqueidentifier":
dataType = Type.GetType("System.Guid");
dataType = typeof(Guid);
break;
}
column.DataType = dataType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2018 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Serilog.Core;
using Serilog.Debugging;
using Serilog.Events;
using System;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;

namespace Serilog.Sinks.MSSqlServer
{
/// <summary>
/// Writes log events as rows in a table of MSSqlServer database using Audit logic, meaning that each row is synchronously committed
/// and any errors that occur are propagated to the caller.
/// </summary>
public class MSSqlServerAuditSink : ILogEventSink, IDisposable
{
private readonly MSSqlServerSinkTraits _traits;

/// <summary>
/// Construct a sink posting to the specified database.
/// </summary>
/// <param name="connectionString">Connection string to access the database.</param>
/// <param name="tableName">Name of the table to store the data in.</param>
/// <param name="schemaName">Name of the schema for the table to store the data in. The default is 'dbo'.</param>
/// <param name="formatProvider">Supplies culture-specific formatting information, or null.</param>
/// <param name="autoCreateSqlTable">Create log table with the provided name on destination sql server.</param>
/// <param name="columnOptions">Options that pertain to columns</param>
public MSSqlServerAuditSink(
string connectionString,
string tableName,
IFormatProvider formatProvider,
bool autoCreateSqlTable = false,
ColumnOptions columnOptions = null,
string schemaName = "dbo"
)
{
if (columnOptions != null)
{
if (columnOptions.DisableTriggers)
throw new NotSupportedException($"The {nameof(ColumnOptions.DisableTriggers)} option is not supported for auditing.");
}
_traits = new MSSqlServerSinkTraits(connectionString, tableName, schemaName, columnOptions, formatProvider, autoCreateSqlTable);


}

/// <summary>Emit the provided log event to the sink.</summary>
/// <param name="logEvent">The log event to write.</param>
public void Emit(LogEvent logEvent)
{
try
{
using (SqlConnection connection = new SqlConnection(_traits.ConnectionString))
{
connection.Open();
using (SqlCommand command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;

StringBuilder fieldList = new StringBuilder($"INSERT INTO [{_traits.SchemaName}].[{_traits.TableName}] (");
StringBuilder parameterList = new StringBuilder(") VALUES (");

int index = 0;
foreach (var field in _traits.GetColumnsAndValues(logEvent))
{
if (index != 0)
{
fieldList.Append(',');
parameterList.Append(',');
}

fieldList.Append(field.Key);
parameterList.Append("@P");
parameterList.Append(index);

SqlParameter parameter = new SqlParameter($"@P{index}", field.Value ?? DBNull.Value);

// The default is SqlDbType.DateTime, which will truncate the DateTime value if the actual
// type in the database table is datetime2. So we explicitly set it to DateTime2, which will
// work both if the field in the table is datetime and datetime2, which is also consistent with
// the behavior of the non-audit sink.
if (field.Value is DateTime)
parameter.SqlDbType = SqlDbType.DateTime2;

command.Parameters.Add(parameter);

index++;
}

parameterList.Append(')');
fieldList.Append(parameterList.ToString());

command.CommandText = fieldList.ToString();

command.ExecuteNonQuery();
}
}
}
catch (Exception ex)
{
SelfLog.WriteLine("Unable to write log event to the database due to following error: {1}", ex.Message);
throw;
}
}

/// <summary>
/// Releases the unmanaged resources used by the Serilog.Sinks.MSSqlServer.MSSqlServerAuditSink and optionally
/// releases the managed resources.
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged
/// resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_traits.Dispose();
}
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Loading

0 comments on commit 9edb6e2

Please sign in to comment.