diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index a213a4b..562e427 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -40,7 +40,7 @@ public static void Main(string[] args) new { TwentyChars = "0123456789abcdefghij" }); logger.Information("Destructure with max collection count:\n{@BigData}", - new { TenItems = new string[] { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" } }); + 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" }); diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index a8b1823..0a5ffbf 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 @@ -155,10 +154,10 @@ void ApplyFilters(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) { - var filterDirective = _section.GetSection("Destructure"); - if(filterDirective.GetChildren().Any()) + var destructureDirective = _section.GetSection("Destructure"); + if (destructureDirective.GetChildren().Any()) { - var methodCalls = GetMethodCalls(filterDirective); + var methodCalls = GetMethodCalls(destructureDirective); CallConfigurationMethods(methodCalls, FindDestructureConfigurationMethods(_configurationAssemblies), loggerConfiguration.Destructure, declaredLevelSwitches); } } @@ -221,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); @@ -330,7 +331,7 @@ static void CallConfigurationMethods(ILookup i.ParameterType == typeof(IConfiguration)); - if(parm != null) call[parm.Position - 1] = _configuration; + if (parm != null) call[parm.Position - 1] = _configuration; call.Insert(0, receiver); @@ -352,7 +353,7 @@ internal static IList FindSinkConfigurationMethods(IReadOnlyCollecti { 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; } @@ -368,7 +369,7 @@ internal static IList FindFilterConfigurationMethods(IReadOnlyCollec { 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; } @@ -376,13 +377,8 @@ internal static IList FindFilterConfigurationMethods(IReadOnlyCollec internal static IList FindDestructureConfigurationMethods(IReadOnlyCollection configurationAssemblies) { var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerDestructuringConfiguration)); - if(configurationAssemblies.Contains(typeof(LoggerDestructuringConfiguration).GetTypeInfo().Assembly)) - { - found.Add(GetSurrogateConfigurationMethod((c, d, _) => With(c, d))); - found.Add(GetSurrogateConfigurationMethod((c, m, _) => ToMaximumDepth(c, m))); - found.Add(GetSurrogateConfigurationMethod((c, m, _) => ToMaximumStringLength(c, m))); - found.Add(GetSurrogateConfigurationMethod((c, m, _) => ToMaximumCollectionCount(c, m))); - } + if (configurationAssemblies.Contains(typeof(LoggerDestructuringConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.Destructure); return found; } @@ -391,12 +387,12 @@ internal static IList FindEventEnricherConfigurationMethods(IReadOnl { 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 FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType) + internal static List FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType) { return configurationAssemblies .SelectMany(a => a.ExportedTypes @@ -408,45 +404,6 @@ internal static IList FindConfigurationExtensionMethods(IReadOnlyCol .ToList(); } - /* - Pass-through calls to various Serilog config methods which are - implemented as instance methods rather than extension methods. The - FindXXXConfigurationMethods calls (above) use these to add method - invocation expressions as surrogates so that SelectConfigurationMethod - has a way to match and invoke these instance methods. - */ - - // TODO: add overload for array argument (ILogEventEnricher[]) - internal static LoggerConfiguration With(LoggerFilterConfiguration loggerFilterConfiguration, ILogEventFilter filter) - => loggerFilterConfiguration.With(filter); - - // TODO: add overload for array argument (IDestructuringPolicy[]) - internal static LoggerConfiguration With(LoggerDestructuringConfiguration loggerDestructuringConfiguration, IDestructuringPolicy policy) - => loggerDestructuringConfiguration.With(policy); - - internal static LoggerConfiguration ToMaximumDepth(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumDestructuringDepth) - => loggerDestructuringConfiguration.ToMaximumDepth(maximumDestructuringDepth); - - internal static LoggerConfiguration ToMaximumStringLength(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumStringLength) - => loggerDestructuringConfiguration.ToMaximumStringLength(maximumStringLength); - - internal static LoggerConfiguration ToMaximumCollectionCount(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumCollectionCount) - => loggerDestructuringConfiguration.ToMaximumCollectionCount(maximumCollectionCount); - - 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); 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..71faa7f --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +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 knows 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 + { + public static IEnumerable WriteTo + { + get + { + yield return GetSurrogateConfigurationMethod, LoggingLevelSwitch>((c, a, s) => Logger(c, a, LevelAlias.Minimum, s)); + } + } + + public static IEnumerable Filter + { + get + { + yield return GetSurrogateConfigurationMethod((c, f, _) => With(c, f)); + } + } + + public static IEnumerable Destructure + { + get + { + yield return GetSurrogateConfigurationMethod((c, d, _) => With(c, d)); + yield return GetSurrogateConfigurationMethod((c, m, _) => ToMaximumDepth(c, m)); + yield return GetSurrogateConfigurationMethod((c, m, _) => ToMaximumStringLength(c, m)); + yield return GetSurrogateConfigurationMethod((c, m, _) => ToMaximumCollectionCount(c, m)); + yield return GetSurrogateConfigurationMethod((c, t, _) => AsScalar(c, t)); + } + } + + public static IEnumerable Enrich + { + get + { + yield return GetSurrogateConfigurationMethod((c, _, __) => FromLogContext(c)); + } + } + + static MethodInfo GetSurrogateConfigurationMethod(Expression> method) + => (method.Body as MethodCallExpression)?.Method; + + /* + Pass-through calls to various Serilog config methods which are + implemented as instance methods rather than extension methods. The + FindXXXConfigurationMethods calls (above) use these to add method + invocation expressions as surrogates so that SelectConfigurationMethod + has a way to match and invoke these instance methods. + */ + + // TODO: add overload for array argument (ILogEventEnricher[]) + static LoggerConfiguration With(LoggerFilterConfiguration loggerFilterConfiguration, ILogEventFilter filter) + => loggerFilterConfiguration.With(filter); + + // TODO: add overload for array argument (IDestructuringPolicy[]) + 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); + + static LoggerConfiguration FromLogContext(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + => loggerEnrichmentConfiguration.FromLogContext(); + + // Unlike the other configuration methods, Logger is an instance method rather than an extension. + static LoggerConfiguration Logger( + LoggerSinkConfiguration loggerSinkConfiguration, + Action configureLogger, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + => loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index f0d8809..85a0bac 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -679,5 +679,77 @@ private string GetDestructuredProperty(object x, string json) 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); + } } } 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/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 5f1e039..c5ff18c 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -109,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)); + } + } }