diff --git a/.gitignore b/.gitignore index 0ced0ba..ff4324d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ bld/ # Visual Studo 2015 cache/options directory .vs/ +# JetBrains project files +.idea/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -196,3 +199,6 @@ FakesAssemblies/ *.opt project.lock.json + +#Test files +*.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d70d01d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: csharp + +matrix: + include: + - os: linux + dist: trusty + sudo: required + dotnet: 2.1.4 + group: edge +script: + - ./build.sh \ No newline at end of file diff --git a/README.md b/README.md index de71252..9a737a0 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,19 @@ Configuration is read from the `Serilog` section. ```json { "Serilog": { - "Using": ["Serilog.Sinks.Literate"], + "Using": ["Serilog.Sinks.Console"], "MinimumLevel": "Debug", "WriteTo": [ - { "Name": "LiterateConsole" }, + { "Name": "Console" }, { "Name": "File", "Args": { "path": "%TEMP%\\Logs\\serilog-configuration-sample.txt" } } ], "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"], + "Destructure": [ + { "Name": "With", "Args": { "policy": "Sample.CustomPolicy, Sample" } }, + { "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } }, + { "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 100 } }, + { "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } } + ], "Properties": { "Application": "Sample" } @@ -21,7 +27,7 @@ Configuration is read from the `Serilog` section. } ``` -This example relies on the _Serilog.Sinks.Literate_, _Serilog.Sinks.File_, _Serilog.Enrichers.Environment_, _Serilog.Settings.Configuration_ and _Serilog.Enrichers.Thread_ packages also being installed. +This example relies on the _Serilog.Sinks.Console_, _Serilog.Sinks.File_, _Serilog.Enrichers.Environment_, _Serilog.Settings.Configuration_ and _Serilog.Enrichers.Thread_ packages also being installed. After installing this package, use `ReadFrom.Configuration()` and pass an `IConfiguration` object. @@ -46,7 +52,7 @@ public class Program The `WriteTo` and `Enrich` sections support the same syntax, for example the following is valid if no arguments are needed by the sinks: ```json -"WriteTo": ["LiterateConsole", "DiagnosticTrace"] +"WriteTo": ["Console", "DiagnosticTrace"] ``` Or alternatively, the long-form (`"Name":` ...) syntax from the first example can be used when arguments need to be supplied. @@ -91,3 +97,39 @@ For example, to set the minimum log level using the _Windows_ command prompt: set Serilog:MinimumLevel=Debug dotnet run ``` + +### Nested configuration sections + +Some Serilog packages require a reference to a logger configuration object. The sample program in this project illustrates this with the following entry configuring the _Serilog.Sinks.Async_ package to wrap the _Serilog.Sinks.File_ package. The `configure` parameter references the File sink configuration: + +```json +"WriteTo:Async": { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "File", + "Args": { + "path": "%TEMP%\\Logs\\serilog-configuration-sample.txt", + "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}" + } + } + ] + } +}, +``` + +### IConfiguration parameter + +If a Serilog package requires additional external configuration information (for example, access to a `ConnectionStrings` section, which would be outside of the `Serilog` section), the sink should include an `IConfiguration` parameter in the configuration extension method. This package will automatically populate that parameter. It should not be declared in the argument list in the configuration source. + +### Complex parameter value binding + +When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri` and `TimeSpan` objects and `enum` elements. + +If the parameter value is not a discrete value, the package will use the configuration binding system provided by _Microsoft.Extensions.Options.ConfigurationExtensions_ to attempt to populate the parameter. Almost anything that can be bound by `IConfiguration.Get` should work with this package. An example of this is the optional `List` parameter used to configure the .NET Standard version of the _Serilog.Sinks.MSSqlServer_ package. + +### IConfigurationSection parameters + +Certain Serilog packages may require configuration information that can't be easily represented by discrete values or direct binding-friendly representations. An example might be lists of values to remove from a collection of default values. In this case the method can accept an entire `IConfigurationSection` as a call parameter and this package will recognize that and populate the parameter. In this way, Serilog packages can support arbitrarily complex configuration scenarios. + diff --git a/appveyor.yml b/appveyor.yml index f17ab19..0e93876 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,12 +2,6 @@ version: '{build}' skip_tags: true image: Visual Studio 2017 configuration: Release -install: - - ps: mkdir -Force ".\build\" | Out-Null - - ps: Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.ps1" -OutFile ".\build\installcli.ps1" - - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli" - - ps: '& .\build\installcli.ps1 -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath -Version 1.0.1' - - ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" build_script: - ps: ./Build.ps1 test: off @@ -16,7 +10,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: nvZ/z+pMS91b3kG4DgfES5AcmwwGoBYQxr9kp4XiJHj25SAlgdIxFx++1N0lFH2x + secure: bd9z4P73oltOXudAjPehwp9iDKsPtC+HbgshOrSgoyQKr5xVK+bxJQngrDJkHdY8 skip_symbols: true on: branch: /^(master|dev)$/ @@ -26,4 +20,4 @@ deploy: artifact: /Serilog.*\.nupkg/ tag: v$(appveyor_build_version) on: - branch: master \ No newline at end of file + branch: master diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..19ab21c --- /dev/null +++ b/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e +dotnet --info +dotnet restore + +for path in src/**/*.csproj; do + dotnet build -f netstandard2.0 -c Release ${path} +done + +for path in test/*.Tests/*.csproj; do + dotnet test -f netcoreapp2.0 -c Release ${path} +done + +cd sample/Sample/ +dotnet build -f netcoreapp2.0 -c Release +dotnet bin/Release/netcoreapp2.0/Sample.dll --run-once diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index 95e6989..562e427 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -3,9 +3,10 @@ using Microsoft.Extensions.Configuration; using Serilog; using System.IO; - +using System.Linq; using Serilog.Core; using Serilog.Events; +using System.Collections.Generic; namespace Sample { @@ -22,6 +23,8 @@ public static void Main(string[] args) .ReadFrom.Configuration(configuration) .CreateLogger(); + logger.Information("Args: {a}", args); + do { logger.ForContext().Information("Hello, world!"); @@ -30,12 +33,26 @@ public static void Main(string[] args) logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Error("Hello, world!"); logger.ForContext(Constants.SourceContextPropertyName, "MyApp.Something.Tricky").Verbose("Hello, world!"); - Console.WriteLine(); + logger.Information("Destructure with max object nesting depth:\n{@NestedObject}", + new { FiveDeep = new { Two = new { Three = new { Four = new { Five = "the end" } } } } }); + + logger.Information("Destructure with max string length:\n{@LongString}", + new { TwentyChars = "0123456789abcdefghij" }); + + logger.Information("Destructure with max collection count:\n{@BigData}", + new { TenItems = new[] { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" } }); + + logger.Information("Destructure with policy to strip password:\n{@LoginData}", + new LoginData { Username = "BGates", Password = "isityearoflinuxyet" }); + + Console.WriteLine("\nPress \"q\" to quit, or any other key to run again.\n"); } - while (Console.ReadKey().KeyChar != 'q'); + while(!args.Contains("--run-once") && (Console.ReadKey().KeyChar != 'q')); } } + // The filter syntax in the sample configuration file is + // processed by the Serilog.Filters.Expressions package. public class CustomFilter : ILogEventFilter { public bool IsEnabled(LogEvent logEvent) @@ -43,4 +60,29 @@ public bool IsEnabled(LogEvent logEvent) return true; } } + + public class LoginData + { + public string Username; + public string Password; + } + + public class CustomPolicy : IDestructuringPolicy + { + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) + { + result = null; + + if(value is LoginData) + { + result = new StructureValue( + new List + { + new LogEventProperty("Username", new ScalarValue(((LoginData)value).Username)) + }); + } + + return (result != null); + } + } } diff --git a/sample/Sample/Sample.csproj b/sample/Sample/Sample.csproj index cf21fb1..1b69583 100644 --- a/sample/Sample/Sample.csproj +++ b/sample/Sample/Sample.csproj @@ -1,7 +1,7 @@  - net46;netcoreapp1.0 + net46;netcoreapp2.0 Exe @@ -13,8 +13,15 @@ - + + + + + + + + diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json index db91572..2cbcccc 100644 --- a/sample/Sample/appsettings.json +++ b/sample/Sample/appsettings.json @@ -43,6 +43,24 @@ "Properties": { "Application": "Sample" }, + "Destructure": [ + { + "Name": "With", + "Args": { "policy": "Sample.CustomPolicy, Sample" } + }, + { + "Name": "ToMaximumDepth", + "Args": { "maximumDestructuringDepth": 3 } + }, + { + "Name": "ToMaximumStringLength", + "Args": { "maximumStringLength": 10 } + }, + { + "Name": "ToMaximumCollectionCount", + "Args": { "maximumCollectionCount": 5 } + } + ], "Filter": [ { "Name": "ByIncludingOnly", diff --git a/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs b/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs index b4e9d34..2de4db4 100644 --- a/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs +++ b/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs @@ -26,13 +26,18 @@ namespace Serilog /// public static class ConfigurationLoggerConfigurationExtensions { - const string DefaultSectionName = "Serilog"; + /// + /// Configuration section name required by this package. + /// + public const string DefaultSectionName = "Serilog"; /// - /// Reads logger settings from the provided configuration object using the default section name. + /// Reads logger settings from the provided configuration object using the default section name. Generally this + /// is preferable over the other method that takes a configuration section. Only this version will populate + /// IConfiguration parameters on target methods. /// /// Logger setting configuration. - /// A configuration object with a Serilog section. + /// A configuration object which contains a Serilog section. /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform /// default will be used. /// An object allowing configuration to continue. @@ -42,29 +47,86 @@ public static LoggerConfiguration Configuration( DependencyContext dependencyContext = null) { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - return settingConfiguration.ConfigurationSection(configuration.GetSection(DefaultSectionName), dependencyContext); + + return settingConfiguration.Settings( + new ConfigurationReader( + configuration, + dependencyContext ?? (Assembly.GetEntryAssembly() != null ? DependencyContext.Default : null))); } /// - /// Reads logger settings from the provided configuration section. + /// Reads logger settings from the provided configuration section. Generally it is preferable to use the other + /// extension method that takes the full configuration object. /// /// Logger setting configuration. - /// The Serilog configuration section + /// The Serilog configuration section /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform /// default will be used. /// An object allowing configuration to continue. public static LoggerConfiguration ConfigurationSection( this LoggerSettingsConfiguration settingConfiguration, - IConfigurationSection configuration, + IConfigurationSection configSection, DependencyContext dependencyContext = null) { if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - + if (configSection == null) throw new ArgumentNullException(nameof(configSection)); + return settingConfiguration.Settings( new ConfigurationReader( - configuration, + configSection, dependencyContext ?? (Assembly.GetEntryAssembly() != null ? DependencyContext.Default : null))); } + + /// + /// Reads logger settings from the provided configuration object using the default section name. Generally this + /// is preferable over the other method that takes a configuration section. Only this version will populate + /// IConfiguration parameters on target methods. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// Defines how the package identifies assemblies to scan for sinks and other Types. + /// An object allowing configuration to continue. + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + ConfigurationAssemblySource configurationAssemblySource) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + if(configurationAssemblySource == ConfigurationAssemblySource.UseLoadedAssemblies) + { + return Configuration(settingConfiguration, configuration); + } + else + { + return settingConfiguration.Settings(new ConfigurationReader(configuration, null)); + } + } + + /// + /// Reads logger settings from the provided configuration section. Generally it is preferable to use the other + /// extension method that takes the full configuration object. + /// + /// Logger setting configuration. + /// The Serilog configuration section + /// Defines how the package identifies assemblies to scan for sinks and other Types. + /// An object allowing configuration to continue. + public static LoggerConfiguration ConfigurationSection( + this LoggerSettingsConfiguration settingConfiguration, + IConfigurationSection configSection, + ConfigurationAssemblySource configurationAssemblySource) + { + if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); + if (configSection == null) throw new ArgumentNullException(nameof(configSection)); + + if (configurationAssemblySource == ConfigurationAssemblySource.UseLoadedAssemblies) + { + return Configuration(settingConfiguration, configSection); + } + else + { + return settingConfiguration.Settings(new ConfigurationReader(configSection, null)); + } + } } } diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index d07e621..b3bb545 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -2,9 +2,9 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 2.6.1 + 3.0.0 Serilog Contributors - net451;netstandard1.6 + netstandard2.0;net451;net461 true true Serilog.Settings.Configuration @@ -16,20 +16,21 @@ https://serilog.net/images/serilog-configuration-nuget.png https://github.com/serilog/serilog-settings-configuration https://www.apache.org/licenses/LICENSE-2.0 - - - true + https://github.com/serilog/serilog-settings-configuration + git Serilog - - + - - $(DefineConstants);APPDOMAIN - + + + + + + diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationAssemblySource.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationAssemblySource.cs new file mode 100644 index 0000000..de5800c --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationAssemblySource.cs @@ -0,0 +1,32 @@ +// Copyright 2013-2016 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. + +namespace Serilog.Settings.Configuration +{ + /// + /// Defines how the package will identify the assemblies which are scanned for sinks and other Type information. + /// + public enum ConfigurationAssemblySource + { + /// + /// Try to scan the assemblies already in memory. This is the default. If GetEntryAssembly is null, fallback to DLL scanning. + /// + UseLoadedAssemblies, + + /// + /// Scan for assemblies in DLLs from the working directory. This is the fallback when GetEntryAssembly is null. + /// + AlwaysScanDllFiles + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index 8b50e08..87359bb 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -11,7 +11,6 @@ using Serilog.Core; using Serilog.Debugging; using Serilog.Events; -using System.Linq.Expressions; using System.Text.RegularExpressions; namespace Serilog.Settings.Configuration @@ -20,20 +19,33 @@ class ConfigurationReader : IConfigurationReader { const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$"; - readonly IConfigurationSection _configuration; + static IConfiguration _configuration; + + readonly IConfigurationSection _section; readonly DependencyContext _dependencyContext; readonly IReadOnlyCollection _configurationAssemblies; - public ConfigurationReader(IConfigurationSection configuration, DependencyContext dependencyContext) + public ConfigurationReader(IConfiguration configuration, DependencyContext dependencyContext) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _section = configuration.GetSection(ConfigurationLoggerConfigurationExtensions.DefaultSectionName); _dependencyContext = dependencyContext; _configurationAssemblies = LoadConfigurationAssemblies(); } - ConfigurationReader(IConfigurationSection configuration, IReadOnlyCollection configurationAssemblies, DependencyContext dependencyContext) + // Generally the initial call should use IConfiguration rather than IConfigurationSection, otherwise + // IConfiguration parameters in the target methods will not be populated. + ConfigurationReader(IConfigurationSection configSection, DependencyContext dependencyContext) { - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); + _dependencyContext = dependencyContext; + _configurationAssemblies = LoadConfigurationAssemblies(); + } + + // Used internally for processing nested configuration sections -- see GetMethodCalls below. + internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyCollection configurationAssemblies, DependencyContext dependencyContext) + { + _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); _dependencyContext = dependencyContext; _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); } @@ -44,46 +56,42 @@ public void Configure(LoggerConfiguration loggerConfiguration) ApplyMinimumLevel(loggerConfiguration, declaredLevelSwitches); ApplyEnrichment(loggerConfiguration, declaredLevelSwitches); ApplyFilters(loggerConfiguration, declaredLevelSwitches); + ApplyDestructuring(loggerConfiguration, declaredLevelSwitches); ApplySinks(loggerConfiguration, declaredLevelSwitches); ApplyAuditSinks(loggerConfiguration, declaredLevelSwitches); } IReadOnlyDictionary ProcessLevelSwitchDeclarations() { - var levelSwitchesDirective = _configuration.GetSection("LevelSwitches"); + var levelSwitchesDirective = _section.GetSection("LevelSwitches"); var namedSwitches = new Dictionary(); - if (levelSwitchesDirective != null) + foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren()) { - foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren()) + var switchName = levelSwitchDeclaration.Key; + var switchInitialLevel = levelSwitchDeclaration.Value; + // switchName must be something like $switch to avoid ambiguities + if (!IsValidSwitchName(switchName)) { - var switchName = levelSwitchDeclaration.Key; - var switchInitialLevel = levelSwitchDeclaration.Value; - // switchName must be something like $switch to avoid ambiguities - if (!IsValidSwitchName(switchName)) - { - throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. Level switch must be declared with a '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); - } - LoggingLevelSwitch newSwitch; - if (string.IsNullOrEmpty(switchInitialLevel)) - { - newSwitch = new LoggingLevelSwitch(); - } - else - { - var initialLevel = ParseLogEventLevel(switchInitialLevel); - newSwitch = new LoggingLevelSwitch(initialLevel); - } - namedSwitches.Add(switchName, newSwitch); + throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. Level switch must be declared with a '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); } + LoggingLevelSwitch newSwitch; + if (string.IsNullOrEmpty(switchInitialLevel)) + { + newSwitch = new LoggingLevelSwitch(); + } + else + { + var initialLevel = ParseLogEventLevel(switchInitialLevel); + newSwitch = new LoggingLevelSwitch(initialLevel); + } + namedSwitches.Add(switchName, newSwitch); } - return namedSwitches; } - void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, - IReadOnlyDictionary declaredLevelSwitches) + void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var minimumLevelDirective = _configuration.GetSection("MinimumLevel"); + var minimumLevelDirective = _section.GetSection("MinimumLevel"); var defaultMinLevelDirective = minimumLevelDirective.Value != null ? minimumLevelDirective : minimumLevelDirective.GetSection("Default"); if (defaultMinLevelDirective.Value != null) @@ -92,7 +100,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, } var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy"); - if (minLevelControlledByDirective?.Value != null) + if (minLevelControlledByDirective.Value != null) { var globalMinimumLevelSwitch = declaredLevelSwitches.LookUpSwitchByName(minLevelControlledByDirective.Value); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already @@ -134,60 +142,63 @@ void ApplyMinimumLevel(IConfigurationSection directive, Action declaredLevelSwitches) + void ApplyFilters(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var filterDirective = _configuration.GetSection("Filter"); - if (filterDirective != null) + var filterDirective = _section.GetSection("Filter"); + if (filterDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(filterDirective); CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter, declaredLevelSwitches); } } - void ApplySinks(LoggerConfiguration loggerConfiguration, - IReadOnlyDictionary declaredLevelSwitches) + void ApplyDestructuring(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + { + var destructureDirective = _section.GetSection("Destructure"); + if (destructureDirective.GetChildren().Any()) + { + var methodCalls = GetMethodCalls(destructureDirective); + CallConfigurationMethods(methodCalls, FindDestructureConfigurationMethods(_configurationAssemblies), loggerConfiguration.Destructure, declaredLevelSwitches); + } + } + + void ApplySinks(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var writeToDirective = _configuration.GetSection("WriteTo"); - if (writeToDirective != null) + var writeToDirective = _section.GetSection("WriteTo"); + if (writeToDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(writeToDirective); CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo, declaredLevelSwitches); } } - void ApplyAuditSinks(LoggerConfiguration loggerConfiguration, - IReadOnlyDictionary declaredLevelSwitches) + void ApplyAuditSinks(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var auditToDirective = _configuration.GetSection("AuditTo"); - if (auditToDirective != null) + var auditToDirective = _section.GetSection("AuditTo"); + if (auditToDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(auditToDirective); CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo, declaredLevelSwitches); } } - void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration, - IReadOnlyDictionary declaredLevelSwitches) + void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var methodCalls = GetMethodCalls(_configuration); + var methodCalls = GetMethodCalls(_section); CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration, declaredLevelSwitches); } - void ApplyEnrichment(LoggerConfiguration loggerConfiguration, - IReadOnlyDictionary declaredLevelSwitches) + void ApplyEnrichment(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var enrichDirective = _configuration.GetSection("Enrich"); - if (enrichDirective != null) + var enrichDirective = _section.GetSection("Enrich"); + if (enrichDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(enrichDirective); CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich, declaredLevelSwitches); } - var propertiesDirective = _configuration.GetSection("Properties"); - if (propertiesDirective != null) + var propertiesDirective = _section.GetSection("Properties"); + if (propertiesDirective.GetChildren().Any()) { foreach (var enrichProperyDirective in propertiesDirective.GetChildren()) { @@ -209,9 +220,11 @@ internal ILookup> GetMet where child.Value == null let name = GetSectionName(child) let callArgs = (from argument in child.GetSection("Args").GetChildren() - select new { + select new + { Name = argument.Key, - Value = GetArgumentValue(argument) }).ToDictionary(p => p.Name, p => p.Value) + Value = GetArgumentValue(argument) + }).ToDictionary(p => p.Name, p => p.Value) select new { Name = name, Args = callArgs })) .ToLookup(p => p.Name, p => p.Args); @@ -220,13 +233,23 @@ internal ILookup> GetMet IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection) { IConfigurationArgumentValue argumentValue; + + // Reject configurations where an element has both scalar and complex + // values as a result of reading multiple configuration sources. + if (argumentSection.Value != null && argumentSection.GetChildren().Any()) + throw new InvalidOperationException( + $"The value for the argument '{argumentSection.Path}' is assigned different value " + + "types in more than one configuration source. Ensure all configurations consistently " + + "use either a scalar (int, string, boolean) or a complex (array, section, list, " + + "POCO, etc.) type for this argument value."); + if (argumentSection.Value != null) { argumentValue = new StringArgumentValue(() => argumentSection.Value, argumentSection.GetReloadToken); } else { - argumentValue = new ConfigurationSectionArgumentValue(new ConfigurationReader(argumentSection, _configurationAssemblies, _dependencyContext)); + argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies, _dependencyContext); } return argumentValue; @@ -246,8 +269,8 @@ IReadOnlyCollection LoadConfigurationAssemblies() { var assemblies = new Dictionary(); - var usingSection = _configuration.GetSection("Using"); - if (usingSection != null) + var usingSection = _section.GetSection("Using"); + if (usingSection.GetChildren().Any()) { foreach (var simpleName in usingSection.GetChildren().Select(c => c.Value)) { @@ -286,12 +309,10 @@ where filter(assemblyName.Name) } else { -#if APPDOMAIN query = from outputAssemblyPath in System.IO.Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") let assemblyFileName = System.IO.Path.GetFileNameWithoutExtension(outputAssemblyPath) where filter(assemblyFileName) select AssemblyName.GetAssemblyName(outputAssemblyPath); -#endif } return query.ToArray(); @@ -306,9 +327,12 @@ static void CallConfigurationMethods(ILookup s.Key == p.Name) + let directive = method.Value.FirstOrDefault(s => s.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase)) select directive.Key == null ? p.DefaultValue : directive.Value.ConvertTo(p.ParameterType, declaredLevelSwitches)).ToList(); + var parm = methodInfo.GetParameters().FirstOrDefault(i => i.ParameterType == typeof(IConfiguration)); + if (parm != null) call[parm.Position - 1] = _configuration; + call.Insert(0, receiver); methodInfo.Invoke(null, call.ToArray()); @@ -318,48 +342,71 @@ static void CallConfigurationMethods(ILookup candidateMethods, string name, Dictionary suppliedArgumentValues) { + // Per issue #111, it is safe to use case-insensitive matching on argument names. The CLR doesn't permit this type + // of overloading, and the Microsoft.Extensions.Configuration keys are case-insensitive (case is preserved with some + // config sources, but key-matching is case-insensitive and case-preservation does not appear to be guaranteed). return candidateMethods .Where(m => m.Name == name && - m.GetParameters().Skip(1).All(p => p.HasDefaultValue || suppliedArgumentValues.Any(s => s.Key == p.Name))) - .OrderByDescending(m => m.GetParameters().Count(p => suppliedArgumentValues.Any(s => s.Key == p.Name))) + m.GetParameters().Skip(1) + .All(p => p.HasDefaultValue || suppliedArgumentValues.Any(s => s.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase)))) + .OrderByDescending(m => + { + var matchingArgs = m.GetParameters().Where(p => suppliedArgumentValues.Any(s => s.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase))).ToList(); + + // Prefer the configuration method with most number of matching arguments and of those the ones with + // the most string type parameters to predict best match with least type casting + return new Tuple( + matchingArgs.Count, + matchingArgs.Count(p => p.ParameterType == typeof(string))); + }) .FirstOrDefault(); } - internal static IList FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) { - var found = FindConfigurationMethods(configurationAssemblies, typeof(LoggerSinkConfiguration)); + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerSinkConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerSinkConfiguration).GetTypeInfo().Assembly)) - found.Add(GetSurrogateConfigurationMethod, LoggingLevelSwitch>((c, a, s) => Logger(c, a, LevelAlias.Minimum, s))); + found.AddRange(SurrogateConfigurationMethods.WriteTo); return found; } - internal static IList FindAuditSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindAuditSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) { - var found = FindConfigurationMethods(configurationAssemblies, typeof(LoggerAuditSinkConfiguration)); - + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerAuditSinkConfiguration)); + if (configurationAssemblies.Contains(typeof(LoggerAuditSinkConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.AuditTo); return found; } - internal static IList FindFilterConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindFilterConfigurationMethods(IReadOnlyCollection configurationAssemblies) { - var found = FindConfigurationMethods(configurationAssemblies, typeof(LoggerFilterConfiguration)); + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerFilterConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerFilterConfiguration).GetTypeInfo().Assembly)) - found.Add(GetSurrogateConfigurationMethod((c, f, _) => With(c, f))); + found.AddRange(SurrogateConfigurationMethods.Filter); return found; } - internal static IList FindEventEnricherConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindDestructureConfigurationMethods(IReadOnlyCollection configurationAssemblies) { - var found = FindConfigurationMethods(configurationAssemblies, typeof(LoggerEnrichmentConfiguration)); + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerDestructuringConfiguration)); + if (configurationAssemblies.Contains(typeof(LoggerDestructuringConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.Destructure); + + return found; + } + + static IList FindEventEnricherConfigurationMethods(IReadOnlyCollection configurationAssemblies) + { + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerEnrichmentConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerEnrichmentConfiguration).GetTypeInfo().Assembly)) - found.Add(GetSurrogateConfigurationMethod((c, _, __) => FromLogContext(c))); + found.AddRange(SurrogateConfigurationMethods.Enrich); return found; } - internal static IList FindConfigurationMethods(IReadOnlyCollection configurationAssemblies, Type configType) + static List FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType) { return configurationAssemblies .SelectMany(a => a.ExportedTypes @@ -371,35 +418,17 @@ internal static IList FindConfigurationMethods(IReadOnlyCollection loggerFilterConfiguration.With(filter); - - // Unlike the other configuration methods, FromLogContext is an instance method rather than an extension. - internal static LoggerConfiguration FromLogContext(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) - => loggerEnrichmentConfiguration.FromLogContext(); - - // Unlike the other configuration methods, Logger is an instance method rather than an extension. - internal static LoggerConfiguration Logger( - LoggerSinkConfiguration loggerSinkConfiguration, - Action configureLogger, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) - => loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); - - internal static MethodInfo GetSurrogateConfigurationMethod(Expression> method) - => (method.Body as MethodCallExpression)?.Method; - internal static bool IsValidSwitchName(string input) { return Regex.IsMatch(input, LevelSwitchNameRegex); } - internal static LogEventLevel ParseLogEventLevel(string value) + static LogEventLevel ParseLogEventLevel(string value) { if (!Enum.TryParse(value, out LogEventLevel parsedLevel)) throw new InvalidOperationException($"The value {value} is not a valid Serilog level."); return parsedLevel; } + } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationSectionArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationSectionArgumentValue.cs deleted file mode 100644 index b7f6825..0000000 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationSectionArgumentValue.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Serilog.Configuration; -using System; -using System.Collections.Generic; -using System.Reflection; -using Serilog.Core; - -namespace Serilog.Settings.Configuration -{ - class ConfigurationSectionArgumentValue : IConfigurationArgumentValue - { - readonly IConfigurationReader _configReader; - - public ConfigurationSectionArgumentValue(IConfigurationReader configReader) - { - _configReader = configReader ?? throw new ArgumentNullException(nameof(configReader)); - } - - public object ConvertTo(Type toType, IReadOnlyDictionary declaredLevelSwitches) - { - var typeInfo = toType.GetTypeInfo(); - if (!typeInfo.IsGenericType || - typeInfo.GetGenericTypeDefinition() is Type genericType && genericType != typeof(Action<>)) - { - throw new InvalidOperationException("Argument value should be of type Action<>."); - } - - var configurationType = typeInfo.GenericTypeArguments[0]; - if (configurationType == typeof(LoggerSinkConfiguration)) - { - return new Action(loggerSinkConfig => _configReader.ApplySinks(loggerSinkConfig, declaredLevelSwitches)); - } - - if (configurationType == typeof(LoggerConfiguration)) - { - return new Action(_configReader.Configure); - } - - throw new ArgumentException($"Handling {configurationType} is not implemented."); - } - } -} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationValueSyntax.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationValueSyntax.cs deleted file mode 100644 index 0569531..0000000 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationValueSyntax.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Serilog.Settings.Configuration -{ - static class ConfigurationValueSyntax - { - const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$"; - - public static bool IsValidSwitchName(string input) - { - return Regex.IsMatch(input, LevelSwitchNameRegex); - } - } -} \ No newline at end of file diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs new file mode 100644 index 0000000..8242476 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyModel; +using Serilog.Configuration; +using Serilog.Core; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Serilog.Settings.Configuration +{ + class ObjectArgumentValue : IConfigurationArgumentValue + { + readonly IConfigurationSection section; + readonly IReadOnlyCollection configurationAssemblies; + readonly DependencyContext dependencyContext; + + public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection configurationAssemblies, DependencyContext dependencyContext) + { + this.section = section; + + // used by nested logger configurations to feed a new pass by ConfigurationReader + this.configurationAssemblies = configurationAssemblies; + this.dependencyContext = dependencyContext; + } + + public object ConvertTo(Type toType, IReadOnlyDictionary declaredLevelSwitches) + { + // return the entire section for internal processing + if(toType == typeof(IConfigurationSection)) return section; + + // process a nested configuration to populate an Action<> logger/sink config parameter? + var typeInfo = toType.GetTypeInfo(); + if(typeInfo.IsGenericType && + typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>)) + { + var configType = typeInfo.GenericTypeArguments[0]; + if(configType != typeof(LoggerConfiguration) && configType != typeof(LoggerSinkConfiguration)) + throw new ArgumentException($"Configuration for Action<{configType}> is not implemented."); + + IConfigurationReader configReader = new ConfigurationReader(section, configurationAssemblies, dependencyContext); + + if(configType == typeof(LoggerConfiguration)) + { + return new Action(configReader.Configure); + } + + if(configType == typeof(LoggerSinkConfiguration)) + { + return new Action(loggerSinkConfig => configReader.ApplySinks(loggerSinkConfig, declaredLevelSwitches)); + } + } + + // MS Config binding + return section.Get(toType); + } + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs index e5f79b0..5291f2a 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs @@ -27,7 +27,8 @@ public StringArgumentValue(Func valueProducer, Func change static readonly Dictionary> ExtendedTypeConversions = new Dictionary> { { typeof(Uri), s => new Uri(s) }, - { typeof(TimeSpan), s => TimeSpan.Parse(s) } + { typeof(TimeSpan), s => TimeSpan.Parse(s) }, + { typeof(Type), s => Type.GetType(s, throwOnError:true) }, }; public object ConvertTo(Type toType, IReadOnlyDictionary declaredLevelSwitches) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs new file mode 100644 index 0000000..6350eca --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Settings.Configuration +{ + /// + /// Contains "fake extension" methods for the Serilog configuration API. + /// By default the settings know how to find extension methods, but some configuration + /// are actually "regular" method calls and would not be found otherwise. + /// + /// This static class contains internal methods that can be used instead. + /// + /// + static class SurrogateConfigurationMethods + { + static readonly Dictionary SurrogateMethodCandidates = typeof(SurrogateConfigurationMethods) + .GetTypeInfo().DeclaredMethods + .GroupBy(m => m.GetParameters().First().ParameterType) + .ToDictionary(g => g.Key, g => g.ToArray()); + + + internal static readonly MethodInfo[] WriteTo = SurrogateMethodCandidates[typeof(LoggerSinkConfiguration)]; + internal static readonly MethodInfo[] AuditTo = SurrogateMethodCandidates[typeof(LoggerAuditSinkConfiguration)]; + internal static readonly MethodInfo[] Enrich = SurrogateMethodCandidates[typeof(LoggerEnrichmentConfiguration)]; + internal static readonly MethodInfo[] Destructure = SurrogateMethodCandidates[typeof(LoggerDestructuringConfiguration)]; + internal static readonly MethodInfo[] Filter = SurrogateMethodCandidates[typeof(LoggerFilterConfiguration)]; + + /* + Pass-through calls to various Serilog config methods which are + implemented as instance methods rather than extension methods. + ConfigurationReader adds those to the already discovered extension methods + so they can be invoked as well. + */ + + // ReSharper disable UnusedMember.Local + // those methods are discovered through reflection by `SurrogateMethodCandidates` + // ReSharper has no way to see that they are actually used ... + + // .WriteTo... + // ======== + static LoggerConfiguration Sink( + LoggerSinkConfiguration loggerSinkConfiguration, + ILogEventSink sink, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + => loggerSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + + static LoggerConfiguration Logger( + LoggerSinkConfiguration loggerSinkConfiguration, + Action configureLogger, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + => loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); + + // .AuditTo... + // ======== + static LoggerConfiguration Sink( + LoggerAuditSinkConfiguration auditSinkConfiguration, + ILogEventSink sink, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + => auditSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + + // .Filter... + // ======= + // TODO: add overload for array argument (ILogEventEnricher[]) + // expose `With(params ILogEventFilter[] filters)` as if it was `With(ILogEventFilter filter)` + static LoggerConfiguration With(LoggerFilterConfiguration loggerFilterConfiguration, ILogEventFilter filter) + => loggerFilterConfiguration.With(filter); + + // .Destructure... + // ============ + // TODO: add overload for array argument (IDestructuringPolicy[]) + // expose `With(params IDestructuringPolicy[] destructuringPolicies)` as if it was `With(IDestructuringPolicy policy)` + static LoggerConfiguration With(LoggerDestructuringConfiguration loggerDestructuringConfiguration, IDestructuringPolicy policy) + => loggerDestructuringConfiguration.With(policy); + + static LoggerConfiguration ToMaximumDepth(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumDestructuringDepth) + => loggerDestructuringConfiguration.ToMaximumDepth(maximumDestructuringDepth); + + static LoggerConfiguration ToMaximumStringLength(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumStringLength) + => loggerDestructuringConfiguration.ToMaximumStringLength(maximumStringLength); + + static LoggerConfiguration ToMaximumCollectionCount(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumCollectionCount) + => loggerDestructuringConfiguration.ToMaximumCollectionCount(maximumCollectionCount); + + static LoggerConfiguration AsScalar(LoggerDestructuringConfiguration loggerDestructuringConfiguration, Type scalarType) + => loggerDestructuringConfiguration.AsScalar(scalarType); + + // .Enrich... + // ======= + // expose `With(params ILogEventEnricher[] enrichers)` as if it was `With(ILogEventEnricher enricher)` + static LoggerConfiguration With( + LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, + ILogEventEnricher enricher) + => loggerEnrichmentConfiguration.With(enricher); + + static LoggerConfiguration FromLogContext(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + => loggerEnrichmentConfiguration.FromLogContext(); + + // ReSharper restore UnusedMember.Local + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs index 79df523..701cfee 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs @@ -165,5 +165,20 @@ public void MethodsAreSelectedBasedOnCountOfMatchedArguments() var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArguments); Assert.Equal(typeof(ITextFormatter), selected.GetParameters()[1].ParameterType); } + + [Fact] + public void MethodsAreSelectedBasedOnCountOfMatchedArgumentsAndThenStringType() + { + var options = typeof(DummyLoggerConfigurationWithMultipleMethodsExtensions).GetTypeInfo().DeclaredMethods.ToList(); + Assert.Equal(3, options.Count(mi => mi.Name == "DummyRollingFile")); + var suppliedArguments = new Dictionary() + { + { "pathFormat", new StringArgumentValue(() => "C:\\") }, + { "formatter", new StringArgumentValue(() => "SomeFormatter, SomeAssembly") } + }; + + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArguments); + Assert.Equal(typeof(string), selected.GetParameters()[2].ParameterType); + } } } diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index 5a627a7..f8f7a95 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -12,9 +12,12 @@ namespace Serilog.Settings.Configuration.Tests { public class ConfigurationSettingsTests { - static LoggerConfiguration ConfigFromJson(string jsonString) + static LoggerConfiguration ConfigFromJson(string jsonString, string secondJsonSource = null) { - var config = new ConfigurationBuilder().AddJsonString(jsonString).Build(); + var builder = new ConfigurationBuilder().AddJsonString(jsonString); + if (secondJsonSource != null) + builder.AddJsonString(secondJsonSource); + var config = builder.Build(); return new LoggerConfiguration() .ReadFrom.Configuration(config); } @@ -31,7 +34,7 @@ public void PropertyEnrichmentIsApplied() } } }"; - + var log = ConfigFromJson(json) .WriteTo.Sink(new DelegatingSink(e => evt = e)) .CreateLogger(); @@ -74,6 +77,31 @@ public void ParameterlessSinksAreConfigured(string syntax, string json) Assert.Equal(1, DummyWithLevelSwitchSink.Emitted.Count); } + [Fact] + public void ConfigurationAssembliesFromDllScanning() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [""DummyConsole""] + } + }"; + + var builder = new ConfigurationBuilder().AddJsonString(json); + var config = builder.Build(); + var log = new LoggerConfiguration() + .ReadFrom.Configuration( + configuration: config, + configurationAssemblySource: ConfigurationAssemblySource.AlwaysScanDllFiles) + .CreateLogger(); + + DummyConsoleSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(1, DummyConsoleSink.Emitted.Count); + } + [Fact] public void SinksAreConfigured() { @@ -90,8 +118,8 @@ public void SinksAreConfigured() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); - DummyRollingFileAuditSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); log.Write(Some.InformationEvent()); @@ -114,9 +142,9 @@ public void AuditSinksAreConfigured() var log = ConfigFromJson(json) .CreateLogger(); - - DummyRollingFileSink.Emitted.Clear(); - DummyRollingFileAuditSink.Emitted.Clear(); + + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); log.Write(Some.InformationEvent()); @@ -199,7 +227,6 @@ public void SinksAreConfiguredWithStaticMember() Assert.Equal(ConsoleThemes.Theme1, DummyConsoleSink.Theme); } - [Theory] [InlineData("$switchName", true)] [InlineData("$SwitchName", true)] @@ -228,7 +255,7 @@ public void LoggingLevelSwitchWithInvalidNameThrowsFormatException() ""LevelSwitches"": {""switchNameNotStartingWithDollar"" : ""Warning"" } } }"; - + var ex = Assert.Throws(() => ConfigFromJson(json)); Assert.Contains("\"switchNameNotStartingWithDollar\"", ex.Message); @@ -272,7 +299,7 @@ public void SettingMinimumLevelControlledByToAnUndeclaredSwitchThrows() } } }"; - + var ex = Assert.Throws(() => ConfigFromJson(json) .CreateLogger()); @@ -333,7 +360,7 @@ public void ReferencingAnUndeclaredSwitchInSinkThrows() }] } }"; - + var ex = Assert.Throws(() => ConfigFromJson(json) .CreateLogger()); @@ -390,5 +417,604 @@ public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides() Assert.False(evt is null, "LoggingLevelSwitch level was changed to Information for logger System.*. It should now log Information events for SourceContext System.Bar."); // ReSharper restore HeuristicUnreachableCode } + + [Fact] + public void SinkWithIConfigurationArguments() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\"", + ""configurationSection"" : { ""foo"" : ""bar"" } } + }] + } + }"; + + // IConfiguration and IConfigurationSection arguments do not have + // default values so they will throw if they are not populated + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Fact] + public void SinkWithConfigurationBindingArgument() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\"", + ""objectBinding"" : [ { ""foo"" : ""bar"" }, { ""abc"" : ""xyz"" } ] } + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Fact] + public void SinkWithStringArrayArgument() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\"", + ""stringArrayBinding"" : [ ""foo"", ""bar"", ""baz"" ] } + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Fact] + public void SinkWithIntArrayArgument() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\"", + ""intArrayBinding"" : [ 1,2,3,4,5 ] } + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Trait("Bugfix", "#111")] + [Fact] + public void CaseInsensitiveArgumentNameMatching() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""PATHFORMAT"" : ""C:\\""} + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Trait("Bugfix", "#91")] + [Fact] + public void WriteToLoggerWithRestrictedToMinimumLevelIsSupported() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""Logger"", + ""Args"": { + ""configureLogger"" : { + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\""} + }]}, + ""restrictedToMinimumLevel"": ""Warning"" + } + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Trait("Bugfix", "#91")] + [Fact] + public void WriteToSubLoggerWithLevelSwitchIsSupported() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""LevelSwitches"": {""$switch1"" : ""Warning"" }, + ""MinimumLevel"" : { + ""ControlledBy"" : ""$switch1"" + }, + ""WriteTo"": [{ + ""Name"": ""Logger"", + ""Args"": { + ""configureLogger"" : { + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\""} + }]} + } + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Equal(1, DummyRollingFileSink.Emitted.Count); + } + + [Trait("Bugfix", "#103")] + [Fact] + public void InconsistentComplexVsScalarArgumentValuesThrowsIOE() + { + var jsonDiscreteValue = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\""} + }] + } + }"; + + var jsonComplexValue = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""WriteTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : { ""foo"" : ""bar"" } } + }] + } + }"; + + // These will combine into a ConfigurationSection object that has both + // Value == "C:\" and GetChildren() == List. No configuration + // extension matching this exists (in theory an "object" argument could + // accept either value). ConfigurationReader should throw as soon as + // the multiple values are recognized; it will never attempt to locate + // a matching argument. + + var ex = Assert.Throws(() + => ConfigFromJson(jsonDiscreteValue, jsonComplexValue)); + + Assert.Contains("The value for the argument", ex.Message); + Assert.Contains("'Serilog:WriteTo:0:Args:pathFormat'", ex.Message); + } + + [Fact] + public void DestructureLimitsNestingDepth() + { + var json = @"{ + ""Serilog"": { + ""Destructure"": [ + { + ""Name"": ""ToMaximumDepth"", + ""Args"": { ""maximumDestructuringDepth"": 3 } + }] + } + }"; + + var NestedObject = new + { + A = new + { + B = new + { + C = new + { + D = "F" + } + } + } + }; + + var msg = GetDestructuredProperty(NestedObject, json); + + Assert.Contains("C", msg); + Assert.DoesNotContain("D", msg); + } + + [Fact] + public void DestructureLimitsStringLength() + { + var json = @"{ + ""Serilog"": { + ""Destructure"": [ + { + ""Name"": ""ToMaximumStringLength"", + ""Args"": { ""maximumStringLength"": 3 } + }] + } + }"; + + var inputString = "ABCDEFGH"; + var msg = GetDestructuredProperty(inputString, json); + + Assert.Equal("\"AB…\"", msg); + } + + [Fact] + public void DestructureLimitsCollectionCount() + { + var json = @"{ + ""Serilog"": { + ""Destructure"": [ + { + ""Name"": ""ToMaximumCollectionCount"", + ""Args"": { ""maximumCollectionCount"": 3 } + }] + } + }"; + + var collection = new[] { 1, 2, 3, 4, 5, 6 }; + var msg = GetDestructuredProperty(collection, json); + + Assert.Contains("3", msg); + Assert.DoesNotContain("4", msg); + } + + private string GetDestructuredProperty(object x, string json) + { + LogEvent evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + log.Information("{@X}", x); + var result = evt.Properties["X"].ToString(); + return result; + } + + [Fact] + public void DestructuringWithCustomExtensionMethodIsApplied() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""Destructure"": [ + { + ""Name"": ""WithDummyHardCodedString"", + ""Args"": { ""hardCodedString"": ""hardcoded"" } + }] + } + }"; + + LogEvent evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + log.Information("Destructuring with hard-coded policy {@Input}", new { Foo = "Bar" }); + var formattedProperty = evt.Properties["Input"].ToString(); + + Assert.Equal("\"hardcoded\"", formattedProperty); + } + + [Fact] + public void DestructuringAsScalarIsAppliedWithShortTypeName() + { + var json = @"{ + ""Serilog"": { + ""Destructure"": [ + { + ""Name"": ""AsScalar"", + ""Args"": { ""scalarType"": ""System.Version"" } + }] + } + }"; + + LogEvent evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); + var prop = evt.Properties["Scalarized"]; + + Assert.IsType(prop); + } + + [Fact] + public void DestructuringAsScalarIsAppliedWithAssemblyQualifiedName() + { + var json = $@"{{ + ""Serilog"": {{ + ""Destructure"": [ + {{ + ""Name"": ""AsScalar"", + ""Args"": {{ ""scalarType"": ""{typeof(Version).AssemblyQualifiedName}"" }} + }}] + }} + }}"; + + LogEvent evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); + var prop = evt.Properties["Scalarized"]; + + Assert.IsType(prop); + } + + [Fact] + public void WriteToSinkIsAppliedWithCustomSink() + { + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""WriteTo"": [ + {{ + ""Name"": ""Sink"", + ""Args"": {{ + ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void WriteToSinkIsAppliedWithCustomSinkAndMinimumLevel() + { + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""WriteTo"": [ + {{ + ""Name"": ""Sink"", + ""Args"": {{ + ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", + ""restrictedToMinimumLevel"": ""Warning"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void WriteToSinkIsAppliedWithCustomSinkAndLevelSwitch() + { + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""LevelSwitches"": {{""$switch1"": ""Warning"" }}, + ""WriteTo"": [ + {{ + ""Name"": ""Sink"", + ""Args"": {{ + ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", + ""levelSwitch"": ""$switch1"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void AuditToSinkIsAppliedWithCustomSink() + { + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""AuditTo"": [ + {{ + ""Name"": ""Sink"", + ""Args"": {{ + ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void AuditToSinkIsAppliedWithCustomSinkAndMinimumLevel() + { + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""AuditTo"": [ + {{ + ""Name"": ""Sink"", + ""Args"": {{ + ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", + ""restrictedToMinimumLevel"": ""Warning"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void AuditToSinkIsAppliedWithCustomSinkAndLevelSwitch() + { + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""LevelSwitches"": {{""$switch1"": ""Warning"" }}, + ""AuditTo"": [ + {{ + ""Name"": ""Sink"", + ""Args"": {{ + ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", + ""levelSwitch"": ""$switch1"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void EnrichWithIsAppliedWithCustomEnricher() + { + LogEvent evt = null; + + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""Enrich"": [ + {{ + ""Name"": ""With"", + ""Args"": {{ + ""enricher"": ""{typeof(DummyThreadIdEnricher).AssemblyQualifiedName}"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Write(Some.InformationEvent()); + + Assert.NotNull(evt); + Assert.True(evt.Properties.ContainsKey("ThreadId"), "Event should have enriched property ThreadId"); + } + + [Fact] + public void FilterWithIsAppliedWithCustomFilter() + { + LogEvent evt = null; + + var json = $@"{{ + ""Serilog"": {{ + ""Using"": [""TestDummies""], + ""Filter"": [ + {{ + ""Name"": ""With"", + ""Args"": {{ + ""filter"": ""{typeof(DummyAnonymousUserFilter).AssemblyQualifiedName}"" + }} + }}] + }} + }}"; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.ForContext("User", "anonymous").Write(Some.InformationEvent()); + Assert.Null(evt); + log.ForContext("User", "the user").Write(Some.InformationEvent()); + Assert.NotNull(evt); + } } } diff --git a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs new file mode 100644 index 0000000..b6be570 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs @@ -0,0 +1,42 @@ +using System; +using Serilog.Configuration; +using Serilog.Events; +using Serilog.Formatting; + +namespace Serilog.Settings.Configuration.Tests +{ + using System.Collections.Generic; + + static class DummyLoggerConfigurationWithMultipleMethodsExtensions + { + public static LoggerConfiguration DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + ITextFormatter formatter, + IEnumerable pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string outputTemplate = null, + IFormatProvider formatProvider = null) + { + return null; + } + + public static LoggerConfiguration DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + ITextFormatter formatter, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string outputTemplate = null, + IFormatProvider formatProvider = null) + { + return null; + } + + public static LoggerConfiguration DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return null; + } + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs index e725f6d..b00c26a 100644 --- a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs @@ -1,5 +1,5 @@ -using Microsoft.Extensions.Configuration; -using System; +using System; +using Microsoft.Extensions.Configuration; using Xunit; namespace Serilog.Settings.Configuration.Tests diff --git a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj index a7ff7c7..3a88c1b 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -1,7 +1,18 @@  + + - net452;netcoreapp1.0 + net452;netcoreapp2.0 Serilog.Settings.Configuration.Tests ../../assets/Serilog.snk true @@ -13,12 +24,20 @@ + + + + + + + + + + - - diff --git a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs index 4c1811a..e432eda 100644 --- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs @@ -163,5 +163,27 @@ public void ReferencingUndeclaredLevelSwitchThrows() Assert.Contains("$mySwitch", ex.Message); Assert.Contains("\"LevelSwitches\":{\"$mySwitch\":", ex.Message); } + + [Fact] + public void StringValuesConvertToTypeFromShortTypeName() + { + var shortTypeName = "System.Version"; + var stringArgumentValue = new StringArgumentValue(() => shortTypeName); + + var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new Dictionary()); + + Assert.Equal(typeof(Version), actual); + } + + [Fact] + public void StringValuesConvertToTypeFromAssemblyQualifiedName() + { + var assemblyQualifiedName = typeof(Version).AssemblyQualifiedName; + var stringArgumentValue = new StringArgumentValue(() => assemblyQualifiedName); + + var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new Dictionary()); + + Assert.Equal(typeof(Version), actual); + } } -} \ No newline at end of file +} diff --git a/test/TestDummies/DummyAnonymousUserFilter.cs b/test/TestDummies/DummyAnonymousUserFilter.cs new file mode 100644 index 0000000..a47dd11 --- /dev/null +++ b/test/TestDummies/DummyAnonymousUserFilter.cs @@ -0,0 +1,25 @@ + +using Serilog.Core; +using Serilog.Events; + +namespace TestDummies +{ + public class DummyAnonymousUserFilter : ILogEventFilter + { + public bool IsEnabled(LogEvent logEvent) + { + if (logEvent.Properties.ContainsKey("User")) + { + if (logEvent.Properties["User"] is ScalarValue sv) + { + if (sv.Value is string s && s == "anonymous") + { + return false; + } + } + } + + return true; + } + } +} diff --git a/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs b/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs new file mode 100644 index 0000000..25d2724 --- /dev/null +++ b/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs @@ -0,0 +1,22 @@ +using System; +using Serilog.Core; +using Serilog.Events; + +namespace TestDummies +{ + public class DummyHardCodedStringDestructuringPolicy : IDestructuringPolicy + { + readonly string _hardCodedString; + + public DummyHardCodedStringDestructuringPolicy(string hardCodedString) + { + _hardCodedString = hardCodedString ?? throw new ArgumentNullException(nameof(hardCodedString)); + } + + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) + { + result = new ScalarValue(_hardCodedString); + return true; + } + } +} diff --git a/test/TestDummies/DummyLoggerConfigurationExtensions.cs b/test/TestDummies/DummyLoggerConfigurationExtensions.cs index b01c422..c5ff18c 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -6,6 +6,8 @@ using Serilog.Core; using TestDummies.Console; using TestDummies.Console.Themes; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; namespace TestDummies { @@ -35,6 +37,43 @@ public static LoggerConfiguration DummyRollingFile( return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); } + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + IConfiguration appConfiguration, + IConfigurationSection configurationSection, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + List objectBinding, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + string[] stringArrayBinding, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + int[] intArrayBinding, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } + public static LoggerConfiguration DummyRollingFile( this LoggerAuditSinkConfiguration loggerSinkConfiguration, string pathFormat, @@ -70,6 +109,14 @@ public static LoggerConfiguration Dummy( s => new DummyWrappingSink(s), wrappedSinkAction); } - + + public static LoggerConfiguration WithDummyHardCodedString( + this LoggerDestructuringConfiguration loggerDestructuringConfiguration, + string hardCodedString + ) + { + return loggerDestructuringConfiguration.With(new DummyHardCodedStringDestructuringPolicy(hardCodedString)); + } + } } diff --git a/test/TestDummies/DummyRollingFileAuditSink.cs b/test/TestDummies/DummyRollingFileAuditSink.cs index e8618d0..2ba17e3 100644 --- a/test/TestDummies/DummyRollingFileAuditSink.cs +++ b/test/TestDummies/DummyRollingFileAuditSink.cs @@ -8,13 +8,18 @@ namespace TestDummies public class DummyRollingFileAuditSink : ILogEventSink { [ThreadStatic] - // ReSharper disable ThreadStaticFieldHasInitializer - public static List Emitted = new List(); - // ReSharper restore ThreadStaticFieldHasInitializer + static List _emitted; + + public static List Emitted => _emitted ?? (_emitted = new List()); public void Emit(LogEvent logEvent) { Emitted.Add(logEvent); } + + public static void Reset() + { + _emitted = null; + } } } diff --git a/test/TestDummies/DummyRollingFileSink.cs b/test/TestDummies/DummyRollingFileSink.cs index bc98ed2..2f6f229 100644 --- a/test/TestDummies/DummyRollingFileSink.cs +++ b/test/TestDummies/DummyRollingFileSink.cs @@ -8,13 +8,18 @@ namespace TestDummies public class DummyRollingFileSink : ILogEventSink { [ThreadStatic] - // ReSharper disable ThreadStaticFieldHasInitializer - public static List Emitted = new List(); - // ReSharper restore ThreadStaticFieldHasInitializer + static List _emitted; + + public static List Emitted => _emitted ?? (_emitted = new List()); public void Emit(LogEvent logEvent) { Emitted.Add(logEvent); } + + public static void Reset() + { + _emitted = null; + } } } diff --git a/test/TestDummies/DummyThreadIdEnricher.cs b/test/TestDummies/DummyThreadIdEnricher.cs index 4ba740e..a640d55 100644 --- a/test/TestDummies/DummyThreadIdEnricher.cs +++ b/test/TestDummies/DummyThreadIdEnricher.cs @@ -6,7 +6,9 @@ namespace TestDummies public class DummyThreadIdEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { + { + logEvent.AddPropertyIfAbsent(propertyFactory + .CreateProperty("ThreadId", "SomeId")); } } } diff --git a/test/TestDummies/DummyWrappingSink.cs b/test/TestDummies/DummyWrappingSink.cs index cd15d8c..cb2f048 100644 --- a/test/TestDummies/DummyWrappingSink.cs +++ b/test/TestDummies/DummyWrappingSink.cs @@ -8,11 +8,11 @@ namespace TestDummies public class DummyWrappingSink : ILogEventSink { [ThreadStatic] - // ReSharper disable ThreadStaticFieldHasInitializer - public static List Emitted = new List(); - // ReSharper restore ThreadStaticFieldHasInitializer + static List _emitted; - private readonly ILogEventSink _sink; + public static List Emitted => _emitted ?? (_emitted = new List()); + + readonly ILogEventSink _sink; public DummyWrappingSink(ILogEventSink sink) { @@ -24,5 +24,10 @@ public void Emit(LogEvent logEvent) Emitted.Add(logEvent); _sink.Emit(logEvent); } + + public static void Reset() + { + _emitted = null; + } } } diff --git a/test/TestDummies/TestDummies.csproj b/test/TestDummies/TestDummies.csproj index d1e5399..5c212f5 100644 --- a/test/TestDummies/TestDummies.csproj +++ b/test/TestDummies/TestDummies.csproj @@ -1,7 +1,7 @@  - net452;netstandard1.3 + net452;netstandard2.0 TestDummies ../../assets/Serilog.snk true @@ -16,9 +16,14 @@ + + + + +