diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Hybrid/LoggerConfigurationMSSqlServerExtensions.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Hybrid/LoggerConfigurationMSSqlServerExtensions.cs index eefb88af..8df8ce9c 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Hybrid/LoggerConfigurationMSSqlServerExtensions.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Hybrid/LoggerConfigurationMSSqlServerExtensions.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Serilog.Configuration; using Serilog.Debugging; @@ -167,6 +168,85 @@ internal static LoggerConfiguration MSSqlServerInternal( return loggerConfiguration.Sink(periodicBatchingSink, restrictedToMinimumLevel, sinkOptions?.LevelSwitch); } + /// + /// 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. + /// + /// The logger configuration. + /// A function to initialize a connection to the database where to store the events. + /// The initial catalog within the database (used if AutoCreateSqlDatabase is enabled). + /// Supplies additional settings for the sink + /// A config section defining additional settings for the sink + /// Additional application-level configuration. Required if connectionString is a name. + /// The minimum level for events passed through the sink. Ignored when LevelSwitch in is specified. + /// Supplies culture-specific formatting information, or null. + /// An externally-modified group of column settings + /// A config section defining various column settings + /// Supplies custom formatter for the LogEvent column, or null + /// Logger configuration, allowing configuration to continue. + /// A required parameter is null. + public static LoggerConfiguration MSSqlServer( + this LoggerSinkConfiguration loggerConfiguration, + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions = null, + IConfigurationSection sinkOptionsSection = null, + IConfiguration appConfiguration = null, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + IFormatProvider formatProvider = null, + ColumnOptions columnOptions = null, + IConfigurationSection columnOptionsSection = null, + ITextFormatter logEventFormatter = null) => + loggerConfiguration.MSSqlServerInternal( + sqlConnectionFactory: sqlConnectionFactory, + initialCatalog: initialCatalog, + sinkOptions: sinkOptions, + sinkOptionsSection: sinkOptionsSection, + appConfiguration: appConfiguration, + restrictedToMinimumLevel: restrictedToMinimumLevel, + formatProvider: formatProvider, + columnOptions: columnOptions, + columnOptionsSection: columnOptionsSection, + logEventFormatter: logEventFormatter, + applySystemConfiguration: new ApplySystemConfiguration(), + applyMicrosoftExtensionsConfiguration: new ApplyMicrosoftExtensionsConfiguration(), + sinkFactory: new MSSqlServerSinkFactory(), + batchingSinkFactory: new PeriodicBatchingSinkFactory()); + + // Internal overload with parameters used by tests to override the config section and inject mocks + internal static LoggerConfiguration MSSqlServerInternal( + this LoggerSinkConfiguration loggerConfiguration, + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions, + IConfigurationSection sinkOptionsSection, + IConfiguration appConfiguration, + LogEventLevel restrictedToMinimumLevel, + IFormatProvider formatProvider, + ColumnOptions columnOptions, + IConfigurationSection columnOptionsSection, + ITextFormatter logEventFormatter, + IApplySystemConfiguration applySystemConfiguration, + IApplyMicrosoftExtensionsConfiguration applyMicrosoftExtensionsConfiguration, + IMSSqlServerSinkFactory sinkFactory, + IPeriodicBatchingSinkFactory batchingSinkFactory) + { + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + + ReadConfiguration(ref sinkOptions, sinkOptionsSection, appConfiguration, + ref columnOptions, columnOptionsSection, applySystemConfiguration, applyMicrosoftExtensionsConfiguration); + + var sink = sinkFactory.Create(sqlConnectionFactory, initialCatalog, sinkOptions, formatProvider, columnOptions, logEventFormatter); + + var periodicBatchingSink = batchingSinkFactory.Create(sink, sinkOptions); + + return loggerConfiguration.Sink(periodicBatchingSink, restrictedToMinimumLevel, sinkOptions?.LevelSwitch); + } + + /// /// Adds a sink that writes log events to a table in a MSSqlServer database. /// @@ -313,6 +393,40 @@ private static void ReadConfiguration( connectionString = applyMicrosoftExtensionsConfiguration.GetConnectionString(connectionString, appConfiguration); } + if (columnOptionsSection != null) + { + columnOptions = applyMicrosoftExtensionsConfiguration.ConfigureColumnOptions(columnOptions, columnOptionsSection); + } + + if (sinkOptionsSection != null) + { + sinkOptions = applyMicrosoftExtensionsConfiguration.ConfigureSinkOptions(sinkOptions, sinkOptionsSection); + } + } + + private static void ReadConfiguration( + ref MSSqlServerSinkOptions sinkOptions, + IConfigurationSection sinkOptionsSection, + IConfiguration appConfiguration, + ref ColumnOptions columnOptions, + IConfigurationSection columnOptionsSection, + IApplySystemConfiguration applySystemConfiguration, + IApplyMicrosoftExtensionsConfiguration applyMicrosoftExtensionsConfiguration) + { + sinkOptions = sinkOptions ?? new MSSqlServerSinkOptions(); + columnOptions = columnOptions ?? new ColumnOptions(); + + var serviceConfigSection = applySystemConfiguration.GetSinkConfigurationSection(AppConfigSectionName); + if (serviceConfigSection != null) + { + columnOptions = applySystemConfiguration.ConfigureColumnOptions(serviceConfigSection, columnOptions); + sinkOptions = applySystemConfiguration.ConfigureSinkOptions(serviceConfigSection, sinkOptions); + + if (appConfiguration != null || columnOptionsSection != null || sinkOptionsSection != null) + SelfLog.WriteLine("Warning: Both System.Configuration (app.config or web.config) and Microsoft.Extensions.Configuration are being applied to the MSSQLServer sink."); + } + + if (columnOptionsSection != null) { columnOptions = applyMicrosoftExtensionsConfiguration.ConfigureColumnOptions(columnOptions, columnOptionsSection); diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Microsoft.Extensions.Configuration/LoggerConfigurationMSSqlServerExtensions.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Microsoft.Extensions.Configuration/LoggerConfigurationMSSqlServerExtensions.cs index 0084b132..85b3b836 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Microsoft.Extensions.Configuration/LoggerConfigurationMSSqlServerExtensions.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Extensions/Microsoft.Extensions.Configuration/LoggerConfigurationMSSqlServerExtensions.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Serilog.Configuration; using Serilog.Events; @@ -131,6 +132,54 @@ public static LoggerConfiguration MSSqlServer( return loggerConfiguration.Sink(periodicBatchingSink, restrictedToMinimumLevel, sinkOptions?.LevelSwitch); } + + /// + /// 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. + /// + /// The logger configuration. + /// A function to initialize a connection to the database where to store the events. + /// The initial catalog within the database (used if AutoCreateSqlDatabase is enabled). + /// Supplies additional settings for the sink + /// A config section defining additional settings for the sink + /// Additional application-level configuration. Required if connectionString is a name. + /// The minimum level for events passed through the sink. Ignored when LevelSwitch in is specified. + /// Supplies culture-specific formatting information, or null. + /// An externally-modified group of column settings + /// A config section defining various column settings + /// Supplies custom formatter for the LogEvent column, or null + /// Logger configuration, allowing configuration to continue. + /// A required parameter is null. + public static LoggerConfiguration MSSqlServer( + this LoggerSinkConfiguration loggerConfiguration, + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions = null, + IConfigurationSection sinkOptionsSection = null, + IConfiguration appConfiguration = null, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + IFormatProvider formatProvider = null, + ColumnOptions columnOptions = null, + IConfigurationSection columnOptionsSection = null, + ITextFormatter logEventFormatter = null) + { + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + + ReadConfiguration(ref sinkOptions, appConfiguration, ref columnOptions, + columnOptionsSection, sinkOptionsSection); + + IMSSqlServerSinkFactory sinkFactory = new MSSqlServerSinkFactory(); + var sink = sinkFactory.Create(sqlConnectionFactory, initialCatalog, sinkOptions, formatProvider, columnOptions, logEventFormatter); + + IPeriodicBatchingSinkFactory periodicBatchingSinkFactory = new PeriodicBatchingSinkFactory(); + var periodicBatchingSink = periodicBatchingSinkFactory.Create(sink, sinkOptions); + + return loggerConfiguration.Sink(periodicBatchingSink, restrictedToMinimumLevel, sinkOptions?.LevelSwitch); + } + /// /// Adds a sink that writes log events to a table in a MSSqlServer database. /// @@ -236,5 +285,20 @@ private static void ReadConfiguration( columnOptions = microsoftExtensionsConfiguration.ConfigureColumnOptions(columnOptions, columnOptionsSection); sinkOptions = microsoftExtensionsConfiguration.ConfigureSinkOptions(sinkOptions, sinkOptionsSection); } + + private static void ReadConfiguration( + ref MSSqlServerSinkOptions sinkOptions, + IConfiguration appConfiguration, + ref ColumnOptions columnOptions, + IConfigurationSection columnOptionsSection, + IConfigurationSection sinkOptionsSection) + { + sinkOptions = sinkOptions ?? new MSSqlServerSinkOptions(); + columnOptions = columnOptions ?? new ColumnOptions(); + + IApplyMicrosoftExtensionsConfiguration microsoftExtensionsConfiguration = new ApplyMicrosoftExtensionsConfiguration(); + columnOptions = microsoftExtensionsConfiguration.ConfigureColumnOptions(columnOptions, columnOptionsSection); + sinkOptions = microsoftExtensionsConfiguration.ConfigureSinkOptions(sinkOptions, sinkOptionsSection); + } } } diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/IMSSqlServerSinkFactory.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/IMSSqlServerSinkFactory.cs index 4ff2b293..0babb81a 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/IMSSqlServerSinkFactory.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/IMSSqlServerSinkFactory.cs @@ -1,6 +1,7 @@ using System; using Serilog.Formatting; using Serilog.Core; +using Microsoft.Data.SqlClient; namespace Serilog.Sinks.MSSqlServer.Configuration.Factories { @@ -12,5 +13,13 @@ IBatchedLogEventSink Create( IFormatProvider formatProvider, ColumnOptions columnOptions, ITextFormatter logEventFormatter); + + IBatchedLogEventSink Create( + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions, + IFormatProvider formatProvider, + ColumnOptions columnOptions, + ITextFormatter logEventFormatter); } } diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/MSSqlServerSinkFactory.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/MSSqlServerSinkFactory.cs index 4ba6d714..a78e5ebc 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/MSSqlServerSinkFactory.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Factories/MSSqlServerSinkFactory.cs @@ -1,6 +1,7 @@ using System; using Serilog.Formatting; using Serilog.Core; +using Microsoft.Data.SqlClient; namespace Serilog.Sinks.MSSqlServer.Configuration.Factories { @@ -18,5 +19,20 @@ public IBatchedLogEventSink Create( formatProvider, columnOptions, logEventFormatter); + + public IBatchedLogEventSink Create( + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions, + IFormatProvider formatProvider, + ColumnOptions columnOptions, + ITextFormatter logEventFormatter) => + new MSSqlServerSink( + sqlConnectionFactory, + initialCatalog, + sinkOptions, + formatProvider, + columnOptions, + logEventFormatter); } } diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependenciesFactory.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependenciesFactory.cs index f9a5cee7..878da6c4 100644 --- a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependenciesFactory.cs +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependenciesFactory.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Data.SqlClient; using Serilog.Formatting; using Serilog.Sinks.MSSqlServer.Output; using Serilog.Sinks.MSSqlServer.Platform; @@ -60,6 +61,49 @@ internal static SinkDependencies Create( sqlConnectionFactory, sqlCommandFactory, logEventDataGenerator) }; + return sinkDependencies; + } + + internal static SinkDependencies Create( + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions, + IFormatProvider formatProvider, + ColumnOptions columnOptions, + ITextFormatter logEventFormatter) + { + columnOptions = columnOptions ?? new ColumnOptions(); + columnOptions.FinalizeConfigurationForSinkConstructor(); + + var connectionFactory = new SqlConnectionFactory(sqlConnectionFactory); + var sqlCommandFactory = new SqlCommandFactory(); + var dataTableCreator = new DataTableCreator(sinkOptions.TableName, columnOptions); + var sqlCreateTableWriter = new SqlCreateTableWriter(sinkOptions.SchemaName, + sinkOptions.TableName, columnOptions, dataTableCreator); + + var logEventDataGenerator = + new LogEventDataGenerator(columnOptions, + new StandardColumnDataGenerator(columnOptions, formatProvider, + new XmlPropertyFormatter(), + logEventFormatter), + new AdditionalColumnDataGenerator( + new ColumnSimplePropertyValueResolver(), + new ColumnHierarchicalPropertyValueResolver())); + var sqlCreateDatabaseWriter = new SqlCreateDatabaseWriter(initialCatalog); + var sinkDependencies = new SinkDependencies + { + SqlDatabaseCreator = new SqlDatabaseCreator( + sqlCreateDatabaseWriter, connectionFactory, sqlCommandFactory), + SqlTableCreator = new SqlTableCreator( + sqlCreateTableWriter, connectionFactory, sqlCommandFactory), + SqlBulkBatchWriter = new SqlBulkBatchWriter( + sinkOptions.TableName, sinkOptions.SchemaName, columnOptions.DisableTriggers, + dataTableCreator, connectionFactory, logEventDataGenerator), + SqlLogEventWriter = new SqlInsertStatementWriter( + sinkOptions.TableName, sinkOptions.SchemaName, + connectionFactory, sqlCommandFactory, logEventDataGenerator) + }; + return sinkDependencies; } } diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSink.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSink.cs index d81f674a..f1378a82 100644 --- a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSink.cs +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSink.cs @@ -20,6 +20,7 @@ using Serilog.Sinks.MSSqlServer.Dependencies; using Serilog.Sinks.MSSqlServer.Platform; using Serilog.Core; +using Microsoft.Data.SqlClient; namespace Serilog.Sinks.MSSqlServer { @@ -100,6 +101,26 @@ public MSSqlServerSink( { } + /// + /// Construct a sink posting to the specified database. + /// + /// Factory to initialize connection to the database. + /// The initial catalog within the database (used if AutoCreateSqlDatabase is enabled). + /// Supplies additional options for the sink + /// Supplies culture-specific formatting information, or null. + /// Options that pertain to columns + /// Supplies custom formatter for the LogEvent column, or null + public MSSqlServerSink( + Func sqlConnectionFactory, + string initialCatalog, + MSSqlServerSinkOptions sinkOptions, + IFormatProvider formatProvider = null, + ColumnOptions columnOptions = null, + ITextFormatter logEventFormatter = null) + : this(sinkOptions, SinkDependenciesFactory.Create(sqlConnectionFactory, initialCatalog, sinkOptions, formatProvider, columnOptions, logEventFormatter)) + { + } + // Internal constructor with injectable dependencies for better testability internal MSSqlServerSink( MSSqlServerSinkOptions sinkOptions, diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/SqlConnectionWrapper.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/SqlConnectionWrapper.cs index 638ae47d..4a6ed618 100644 --- a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/SqlConnectionWrapper.cs +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/SqlConnectionWrapper.cs @@ -14,6 +14,11 @@ public SqlConnectionWrapper(string connectionString) _sqlConnection = new SqlConnection(connectionString); } + public SqlConnectionWrapper(Func connectionFactory) + { + _sqlConnection = connectionFactory(); + } + public SqlConnection SqlConnection => _sqlConnection; public void Open() diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlConnectionFactory.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlConnectionFactory.cs index b18f7878..5c7f6e0a 100644 --- a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlConnectionFactory.cs +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlConnectionFactory.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Data.SqlClient; using Serilog.Sinks.MSSqlServer.Platform.SqlClient; namespace Serilog.Sinks.MSSqlServer.Platform @@ -7,6 +8,7 @@ internal class SqlConnectionFactory : ISqlConnectionFactory { private readonly string _connectionString; private readonly ISqlConnectionStringBuilderWrapper _sqlConnectionStringBuilderWrapper; + private readonly Func _sqlConnectionFactory; public SqlConnectionFactory(ISqlConnectionStringBuilderWrapper sqlConnectionStringBuilderWrapper) { @@ -16,8 +18,17 @@ public SqlConnectionFactory(ISqlConnectionStringBuilderWrapper sqlConnectionStri _connectionString = _sqlConnectionStringBuilderWrapper.ConnectionString; } + public SqlConnectionFactory(Func connectionFactory) + { + _sqlConnectionFactory = connectionFactory; + } + public ISqlConnectionWrapper Create() { + if(_sqlConnectionFactory != null) + { + return new SqlConnectionWrapper(_sqlConnectionFactory); + } return new SqlConnectionWrapper(_connectionString); } } diff --git a/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlConnectionFactoryTests.cs b/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlConnectionFactoryTests.cs index f14674f1..cb59ec9c 100644 --- a/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlConnectionFactoryTests.cs +++ b/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlConnectionFactoryTests.cs @@ -21,7 +21,7 @@ public SqlConnectionFactoryTests() [Fact] public void IntializeThrowsIfSqlConnectionStringBuilderWrapperIsNull() { - Assert.Throws(() => new SqlConnectionFactory(null)); + Assert.Throws(() => new SqlConnectionFactory((ISqlConnectionStringBuilderWrapper)null)); } [Fact]