diff --git a/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs b/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs index 7b32878bd..3c6ad9cc4 100644 --- a/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs +++ b/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs @@ -32,9 +32,11 @@ class KeyValuePairSettings : ILoggerSettings const string UsingDirective = "using"; const string WriteToDirective = "write-to"; const string MinimumLevelDirective = "minimum-level"; + const string EnrichWithDirective = "enrich"; const string EnrichWithPropertyDirective = "enrich:with-property"; const string UsingDirectiveFullFormPrefix = "using:"; + const string EnrichWithEventEnricherPrefix = "enrich:"; const string EnrichWithPropertyDirectivePrefix = "enrich:with-property:"; const string WriteToDirectiveRegex = @"^write-to:(?[A-Za-z0-9]*)(\.(?[A-Za-z0-9]*)){0,1}$"; @@ -44,7 +46,8 @@ class KeyValuePairSettings : ILoggerSettings UsingDirective, WriteToDirective, MinimumLevelDirective, - EnrichWithPropertyDirective + EnrichWithPropertyDirective, + EnrichWithDirective }; readonly Dictionary _settings; @@ -71,11 +74,11 @@ public void Configure(LoggerConfiguration loggerConfiguration) loggerConfiguration.MinimumLevel.Is(minimumLevel); } - foreach (var enrichDirective in directives.Where(dir => + foreach (var enrichProperyDirective in directives.Where(dir => dir.Key.StartsWith(EnrichWithPropertyDirectivePrefix) && dir.Key.Length > EnrichWithPropertyDirectivePrefix.Length)) { - var name = enrichDirective.Key.Substring(EnrichWithPropertyDirectivePrefix.Length); - loggerConfiguration.Enrich.WithProperty(name, enrichDirective.Value); + var name = enrichProperyDirective.Key.Substring(EnrichWithPropertyDirectivePrefix.Length); + loggerConfiguration.Enrich.WithProperty(name, enrichProperyDirective.Value); } var splitWriteTo = new Regex(WriteToDirectiveRegex); @@ -83,45 +86,65 @@ public void Configure(LoggerConfiguration loggerConfiguration) var sinkDirectives = (from wt in directives where splitWriteTo.IsMatch(wt.Key) let match = splitWriteTo.Match(wt.Key) - let call = new + let call = new MethodArgumentValue { Method = match.Groups["method"].Value, Argument = match.Groups["argument"].Value, - wt.Value + Value = wt.Value } group call by call.Method).ToList(); - if (sinkDirectives.Any()) + var eventEnricherDirectives = (from er in directives + where er.Key.StartsWith(EnrichWithEventEnricherPrefix) && !er.Key.StartsWith(EnrichWithPropertyDirectivePrefix) && er.Key.Length > EnrichWithEventEnricherPrefix.Length + let match = er.Key.Substring(EnrichWithEventEnricherPrefix.Length) + let call = new MethodArgumentValue + { + Method = match + } + group call by call.Method).ToList(); + + if (sinkDirectives.Any() || eventEnricherDirectives.Any()) { var configurationAssemblies = LoadConfigurationAssemblies(directives); - var sinkConfigurationMethods = FindSinkConfigurationMethods(configurationAssemblies); - foreach (var sinkDirective in sinkDirectives) + if (sinkDirectives.Any()) { - var target = sinkConfigurationMethods - .Where(m => m.Name == sinkDirective.Key && - m.GetParameters().Skip(1).All(p => + ApplyDirectives(sinkDirectives, FindSinkConfigurationMethods(configurationAssemblies), loggerConfiguration.WriteTo); + } + + if (eventEnricherDirectives.Any()) + { + ApplyDirectives(eventEnricherDirectives, FindEventEnricherConfigurationMethods(configurationAssemblies), loggerConfiguration.Enrich); + } + } + } + + private static void ApplyDirectives(List> directives, IList configurationMethods, object loggerConfigMethod) + { + foreach (var directiveInfo in directives) + { + var target = configurationMethods + .Where(m => m.Name == directiveInfo.Key && + m.GetParameters().Skip(1).All(p => #if NET40 - (p.Attributes & ParameterAttributes.HasDefault) != ParameterAttributes.None + (p.Attributes & ParameterAttributes.HasDefault) != ParameterAttributes.None #else - p.HasDefaultValue + p.HasDefaultValue #endif - || sinkDirective.Any(s => s.Argument == p.Name))) - .OrderByDescending(m => m.GetParameters().Length) - .FirstOrDefault(); + || directiveInfo.Any(s => s.Argument == p.Name))) + .OrderByDescending(m => m.GetParameters().Length) + .FirstOrDefault(); - if (target != null) - { - var config = loggerConfiguration.WriteTo; + if (target != null) + { - var call = (from p in target.GetParameters().Skip(1) - let directive = sinkDirective.FirstOrDefault(s => s.Argument == p.Name) - select directive == null ? p.DefaultValue : ConvertToType(directive.Value, p.ParameterType)).ToList(); + var call = (from p in target.GetParameters().Skip(1) + let directive = directiveInfo.FirstOrDefault(s => s.Argument == p.Name) + select directive == null ? p.DefaultValue : ConvertToType(directive.Value, p.ParameterType)).ToList(); - call.Insert(0, config); + call.Insert(0, loggerConfigMethod); - target.Invoke(null, call.ToArray()); - } + target.Invoke(null, call.ToArray()); } } } @@ -170,6 +193,16 @@ internal static object ConvertToType(string value, Type toType) } internal static IList FindSinkConfigurationMethods(IEnumerable configurationAssemblies) + { + return FindConfigurationMethods(configurationAssemblies, typeof(LoggerSinkConfiguration)); + } + + internal static IList FindEventEnricherConfigurationMethods(IEnumerable configurationAssemblies) + { + return FindConfigurationMethods(configurationAssemblies, typeof(LoggerEnrichmentConfiguration)); + } + + internal static IList FindConfigurationMethods(IEnumerable configurationAssemblies, Type configType) { return configurationAssemblies .SelectMany(a => a. @@ -181,8 +214,15 @@ internal static IList FindSinkConfigurationMethods(IEnumerable t.GetTypeInfo()).Where(t => t.IsSealed && t.IsAbstract && !t.IsNested)) .SelectMany(t => t.DeclaredMethods) .Where(m => m.IsStatic && m.IsPublic && m.IsDefined(typeof(ExtensionAttribute), false)) - .Where(m => m.GetParameters()[0].ParameterType == typeof(LoggerSinkConfiguration)) + .Where(m => m.GetParameters()[0].ParameterType == configType) .ToList(); } + + private class MethodArgumentValue + { + public string Method { get; set; } + public string Argument { get; set; } + public string Value { get; set; } + } } } diff --git a/test/Serilog.Tests/Settings/AppSettingsTests.cs b/test/Serilog.Tests/Settings/AppSettingsTests.cs index 10d855160..be4b6a3cb 100644 --- a/test/Serilog.Tests/Settings/AppSettingsTests.cs +++ b/test/Serilog.Tests/Settings/AppSettingsTests.cs @@ -4,6 +4,7 @@ using Xunit; using Serilog.Events; using Serilog.Tests.Support; +using Serilog.Context; namespace Serilog.Tests.AppSettings.Tests { @@ -68,6 +69,110 @@ public void CustomPrefixCannotBeSerilog() Assert.Throws(() => new LoggerConfiguration().ReadFrom.AppSettings("serilog")); } + + [Fact] + public void ThreadIdEnricherIsApplied() + { + // Make sure we have the expected key in the App.config + Assert.NotNull(ConfigurationManager.AppSettings["serilog:enrich:WithThreadId"]); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Has a ThreadId property with value generated by ThreadIdEnricher"); + + Assert.NotNull(evt); + Assert.NotNull(evt.Properties["ThreadId"]); + Assert.NotNull(evt.Properties["ThreadId"].LiteralValue() as int?); + } +#if !DOTNET5_1 + [Fact] + public void MachineNameEnricherIsApplied() + { + // Make sure we have the expected key in the App.config + Assert.NotNull(ConfigurationManager.AppSettings["serilog:enrich:WithMachineName"]); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Has a MachineName property with value generated by MachineNameEnricher"); + + Assert.NotNull(evt); + Assert.NotNull(evt.Properties["MachineName"]); + Assert.NotEmpty((string)evt.Properties["MachineName"].LiteralValue()); + } + + [Fact] + public void EnrivonmentUserNameEnricherIsApplied() + { + // Make sure we have the expected key in the App.config + Assert.NotNull(ConfigurationManager.AppSettings["serilog:enrich:WithEnvironmentUserName"]); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Has a EnrivonmentUserName property with value generated by EnrivonmentUserNameEnricher"); + + Assert.NotNull(evt); + Assert.NotNull(evt.Properties["EnvironmentUserName"]); + Assert.NotEmpty((string)evt.Properties["EnvironmentUserName"].LiteralValue()); + } +#endif + +#if PROCESS + [Fact] + public void ProcessIdEnricherIsApplied() + { + // Make sure we have the expected key in the App.config + Assert.NotNull(ConfigurationManager.AppSettings["serilog:enrich:WithProcessId"]); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Has a ProcessId property with value generated by ProcessIdEnricher"); + + Assert.NotNull(evt); + Assert.NotNull(evt.Properties["ProcessId"]); + Assert.NotNull(evt.Properties["ProcessId"].LiteralValue() as int?); + } +#endif + +#if LOGCONTEXT + [Fact] + public void LogContextEnricherIsApplied() + { + // Make sure we have the expected key in the App.config + Assert.NotNull(ConfigurationManager.AppSettings["serilog:enrich:FromLogContext"]); + + LogEvent evt = null; + var log = new LoggerConfiguration() + .ReadFrom.AppSettings() + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + using (LogContext.PushProperty("A", 1)) + { + log.Information("Has a LogContext property with value generated by LogContextEnricher"); + } + + Assert.NotNull(evt); + Assert.NotNull(evt.Properties["A"]); + Assert.NotNull(evt.Properties["A"].LiteralValue() as int?); + Assert.Equal(1, (int)evt.Properties["A"].LiteralValue()); + } +#endif } } #endif diff --git a/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs b/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs index 089f46171..26059b6b3 100644 --- a/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs +++ b/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs @@ -80,6 +80,34 @@ public void FindsConfigurationMethodsWithinAnAssembly() Assert.True(configurationMethods.Contains("Trace")); } + [Fact] + public void FindsEventEnrichersWithinAnAssembly() + { + var eventEnrichers = KeyValuePairSettings + .FindEventEnricherConfigurationMethods(new[] { typeof(RollingFileSink) +#if DNXCORE50 + .GetTypeInfo() +#endif + .Assembly + }) + .Select(m => m.Name) + .Distinct() + .ToList(); + + +#if LOGCONTEXT + Assert.True(eventEnrichers.Contains("FromLogContext")); +#endif +#if !DOTNET5_1 + Assert.True(eventEnrichers.Contains("WithEnvironmentUserName")); + Assert.True(eventEnrichers.Contains("WithMachineName")); +#endif +#if PROCESS + Assert.True(eventEnrichers.Contains("WithProcessId")); +#endif + Assert.True(eventEnrichers.Contains("WithThreadId")); + } + [Fact] public void PropertyEnrichmentIsApplied() { diff --git a/test/Serilog.Tests/app.config b/test/Serilog.Tests/app.config index 346514b7e..9a9dafa57 100644 --- a/test/Serilog.Tests/app.config +++ b/test/Serilog.Tests/app.config @@ -4,5 +4,10 @@ + + + + + \ No newline at end of file diff --git a/test/Serilog.Tests/project.json b/test/Serilog.Tests/project.json index a28098071..0aa01e790 100644 --- a/test/Serilog.Tests/project.json +++ b/test/Serilog.Tests/project.json @@ -17,7 +17,7 @@ "dnx451": { "compilationOptions": { "keyFile": "../../assets/Serilog.snk", - "define": [ "APPSETTINGS", "LOGCONTEXT", "FILE_IO", "PERIODIC_BATCHING", "INTERNAL_TESTS" ] + "define": [ "APPSETTINGS", "LOGCONTEXT", "PROCESS", "FILE_IO", "PERIODIC_BATCHING", "INTERNAL_TESTS" ] }, "frameworkAssemblies": { "System.Configuration": ""