diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index 3ef88b8..87359bb 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -362,7 +362,7 @@ internal static MethodInfo SelectConfigurationMethod(IEnumerable can .FirstOrDefault(); } - internal static IList FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) { var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerSinkConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerSinkConfiguration).GetTypeInfo().Assembly)) @@ -371,14 +371,15 @@ internal static IList FindSinkConfigurationMethods(IReadOnlyCollecti return found; } - internal static IList FindAuditSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindAuditSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) { 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 = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerFilterConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerFilterConfiguration).GetTypeInfo().Assembly)) @@ -387,7 +388,7 @@ internal static IList FindFilterConfigurationMethods(IReadOnlyCollec return found; } - internal static IList FindDestructureConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindDestructureConfigurationMethods(IReadOnlyCollection configurationAssemblies) { var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerDestructuringConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerDestructuringConfiguration).GetTypeInfo().Assembly)) @@ -396,7 +397,7 @@ internal static IList FindDestructureConfigurationMethods(IReadOnlyC return found; } - internal static IList FindEventEnricherConfigurationMethods(IReadOnlyCollection configurationAssemblies) + static IList FindEventEnricherConfigurationMethods(IReadOnlyCollection configurationAssemblies) { var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerEnrichmentConfiguration)); if (configurationAssemblies.Contains(typeof(LoggerEnrichmentConfiguration).GetTypeInfo().Assembly)) @@ -405,7 +406,7 @@ internal static IList FindEventEnricherConfigurationMethods(IReadOnl return found; } - internal static List FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType) + static List FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType) { return configurationAssemblies .SelectMany(a => a.ExportedTypes @@ -422,7 +423,7 @@ 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."); diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs index 71faa7f..6350eca 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq.Expressions; +using System.Linq; using System.Reflection; using Serilog.Configuration; using Serilog.Core; @@ -10,7 +10,7 @@ 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 + /// 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. @@ -18,58 +18,65 @@ namespace Serilog.Settings.Configuration /// 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; + 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. 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. + 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); @@ -85,15 +92,17 @@ static LoggerConfiguration ToMaximumCollectionCount(LoggerDestructuringConfigura 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(); - // 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); + // ReSharper restore UnusedMember.Local } } diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index 1cd0d70..f8f7a95 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -118,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()); @@ -143,8 +143,8 @@ public void AuditSinksAreConfigured() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); - DummyRollingFileAuditSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); log.Write(Some.InformationEvent()); @@ -438,7 +438,7 @@ public void SinkWithIConfigurationArguments() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); @@ -462,7 +462,7 @@ public void SinkWithConfigurationBindingArgument() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); @@ -486,7 +486,7 @@ public void SinkWithStringArrayArgument() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); @@ -510,7 +510,7 @@ public void SinkWithIntArrayArgument() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); @@ -534,7 +534,7 @@ public void CaseInsensitiveArgumentNameMatching() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); @@ -565,7 +565,7 @@ public void WriteToLoggerWithRestrictedToMinimumLevelIsSupported() var log = ConfigFromJson(json) .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); log.Write(Some.WarningEvent()); @@ -598,9 +598,9 @@ public void WriteToSubLoggerWithLevelSwitchIsSupported() }"; var log = ConfigFromJson(json) - .CreateLogger(); + .CreateLogger(); - DummyRollingFileSink.Emitted.Clear(); + DummyRollingFileSink.Reset(); log.Write(Some.InformationEvent()); log.Write(Some.WarningEvent()); @@ -771,7 +771,7 @@ public void DestructuringAsScalarIsAppliedWithShortTypeName() .WriteTo.Sink(new DelegatingSink(e => evt = e)) .CreateLogger(); - log.Information("Destructuring as scalar {@Scalarized}", new Version(2,3)); + log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); var prop = evt.Properties["Scalarized"]; Assert.IsType(prop); @@ -795,10 +795,226 @@ public void DestructuringAsScalarIsAppliedWithAssemblyQualifiedName() .WriteTo.Sink(new DelegatingSink(e => evt = e)) .CreateLogger(); - log.Information("Destructuring as scalar {@Scalarized}", new Version(2,3)); + 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/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/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; + } } }