diff --git a/src/Serilog/Configuration/LoggerMinimumLevelConfiguration.cs b/src/Serilog/Configuration/LoggerMinimumLevelConfiguration.cs index 517d7cb74..8b1089cf8 100644 --- a/src/Serilog/Configuration/LoggerMinimumLevelConfiguration.cs +++ b/src/Serilog/Configuration/LoggerMinimumLevelConfiguration.cs @@ -26,14 +26,18 @@ public class LoggerMinimumLevelConfiguration readonly LoggerConfiguration _loggerConfiguration; readonly Action _setMinimum; readonly Action _setLevelSwitch; + readonly Action _addOverride; - internal LoggerMinimumLevelConfiguration(LoggerConfiguration loggerConfiguration, Action setMinimum, Action setLevelSwitch) + internal LoggerMinimumLevelConfiguration(LoggerConfiguration loggerConfiguration, Action setMinimum, + Action setLevelSwitch, Action addOverride) { if (loggerConfiguration == null) throw new ArgumentNullException(nameof(loggerConfiguration)); if (setMinimum == null) throw new ArgumentNullException(nameof(setMinimum)); + if (addOverride == null) throw new ArgumentNullException(nameof(addOverride)); _loggerConfiguration = loggerConfiguration; _setMinimum = setMinimum; _setLevelSwitch = setLevelSwitch; + _addOverride = addOverride; } /// @@ -51,7 +55,8 @@ public LoggerConfiguration Is(LogEventLevel minimumLevel) /// Sets the minimum level to be dynamically controlled by the provided switch. /// /// The switch. - /// Configuration object allowing method chaining. + /// Configuration object allowing method chaining. + // ReSharper disable once UnusedMethodReturnValue.Global public LoggerConfiguration ControlledBy(LoggingLevelSwitch levelSwitch) { if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); @@ -63,6 +68,7 @@ public LoggerConfiguration ControlledBy(LoggingLevelSwitch levelSwitch) /// Anything and everything you might want to know about /// a running block of code. /// + /// Configuration object allowing method chaining. public LoggerConfiguration Verbose() { return Is(LogEventLevel.Verbose); @@ -72,6 +78,7 @@ public LoggerConfiguration Verbose() /// Internal system events that aren't necessarily /// observable from the outside. /// + /// Configuration object allowing method chaining. public LoggerConfiguration Debug() { return Is(LogEventLevel.Debug); @@ -81,6 +88,7 @@ public LoggerConfiguration Debug() /// The lifeblood of operational intelligence - things /// happen. /// + /// Configuration object allowing method chaining. public LoggerConfiguration Information() { return Is(LogEventLevel.Information); @@ -89,6 +97,7 @@ public LoggerConfiguration Information() /// /// Service is degraded or endangered. /// + /// Configuration object allowing method chaining. public LoggerConfiguration Warning() { return Is(LogEventLevel.Warning); @@ -98,6 +107,7 @@ public LoggerConfiguration Warning() /// Functionality is unavailable, invariants are broken /// or data is lost. /// + /// Configuration object allowing method chaining. public LoggerConfiguration Error() { return Is(LogEventLevel.Error); @@ -107,9 +117,40 @@ public LoggerConfiguration Error() /// If you have a pager, it goes off when one of these /// occurs. /// + /// Configuration object allowing method chaining. public LoggerConfiguration Fatal() { return Is(LogEventLevel.Fatal); } + + /// + /// Override the minimum level for events from a specific namespace or type name. + /// + /// The (partial) namespace or type name to set the override for. + /// The switch controlling loggers for matching sources. + /// Configuration object allowing method chaining. + public LoggerConfiguration Override(string source, LoggingLevelSwitch levelSwitch) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); + + var trimmed = source.Trim(); + if (trimmed.Length == 0) + throw new ArgumentException("A source name must be provided.", nameof(source)); + + _addOverride(trimmed, levelSwitch); + return _loggerConfiguration; + } + + /// + /// Override the minimum level for events from a specific namespace or type name. + /// + /// The (partial) namespace or type name to set the override for. + /// The minimum level applied to loggers for matching sources. + /// Configuration object allowing method chaining. + public LoggerConfiguration Override(string source, LogEventLevel minimumLevel) + { + return Override(source, new LoggingLevelSwitch(minimumLevel)); + } } } diff --git a/src/Serilog/Core/LevelOverrideMap.cs b/src/Serilog/Core/LevelOverrideMap.cs new file mode 100644 index 000000000..cf69a87fb --- /dev/null +++ b/src/Serilog/Core/LevelOverrideMap.cs @@ -0,0 +1,81 @@ +// Copyright 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using Serilog.Events; + +namespace Serilog.Core +{ + class LevelOverrideMap + { + readonly LogEventLevel _defaultMinimumLevel; + readonly LoggingLevelSwitch _defaultLevelSwitch; + + struct LevelOverride + { + public LevelOverride(string context, LoggingLevelSwitch levelSwitch) + { + Context = context; + ContextPrefix = context + "."; + LevelSwitch = levelSwitch; + } + + public string Context { get; } + public string ContextPrefix { get; } + public LoggingLevelSwitch LevelSwitch { get; } + } + + // There are two possible strategies to apply: + // 1. Keep some bookkeeping data to consult when a new context is encountered, and a concurrent dictionary + // for exact matching ~ O(1), but slow and requires fences/locks; or, + // 2. O(n) search over the raw configuration data every time (fast for small sets of overrides). + // This implementation assumes there will only be a few overrides in each application, so chooses (2). This + // is an assumption that's up for debate. + readonly LevelOverride[] _overrides; + + public LevelOverrideMap( + IDictionary overrides, + LogEventLevel defaultMinimumLevel, + LoggingLevelSwitch defaultLevelSwitch) + { + if (overrides == null) throw new ArgumentNullException(nameof(overrides)); + _defaultLevelSwitch = defaultLevelSwitch; + _defaultMinimumLevel = defaultLevelSwitch != null ? LevelAlias.Minimum : defaultMinimumLevel; + + // Descending order means that if we have a match, we're sure about it being the most specific. + _overrides = overrides + .OrderByDescending(o => o.Key) + .Select(o => new LevelOverride(o.Key, o.Value)) + .ToArray(); + } + + public void GetEffectiveLevel(string context, out LogEventLevel minimumLevel, out LoggingLevelSwitch levelSwitch) + { + foreach (var levelOverride in _overrides) + { + if (context.StartsWith(levelOverride.ContextPrefix) || context == levelOverride.Context) + { + minimumLevel = LevelAlias.Minimum; + levelSwitch = levelOverride.LevelSwitch; + return; + } + } + + minimumLevel = _defaultMinimumLevel; + levelSwitch = _defaultLevelSwitch; + } + } +} diff --git a/src/Serilog/Core/Logger.cs b/src/Serilog/Core/Logger.cs index 78a7329a5..9301e0ded 100644 --- a/src/Serilog/Core/Logger.cs +++ b/src/Serilog/Core/Logger.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2015 Serilog Contributors +// 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. @@ -42,14 +42,16 @@ public sealed class Logger : ILogger, ILogEventSink, IDisposable // to its lower limit and fall through to the secondary check. readonly LogEventLevel _minimumLevel; readonly LoggingLevelSwitch _levelSwitch; + readonly LevelOverrideMap _overrideMap; internal Logger( MessageTemplateProcessor messageTemplateProcessor, LogEventLevel minimumLevel, ILogEventSink sink, ILogEventEnricher enricher, - Action dispose = null) - : this(messageTemplateProcessor, minimumLevel, sink, enricher, dispose, null) + Action dispose = null, + LevelOverrideMap overrideMap = null) + : this(messageTemplateProcessor, minimumLevel, sink, enricher, dispose, null, overrideMap) { } @@ -58,8 +60,9 @@ internal Logger( LoggingLevelSwitch levelSwitch, ILogEventSink sink, ILogEventEnricher enricher, - Action dispose = null) - : this(messageTemplateProcessor, LevelAlias.Minimum, sink, enricher, dispose, levelSwitch) + Action dispose = null, + LevelOverrideMap overrideMap = null) + : this(messageTemplateProcessor, LevelAlias.Minimum, sink, enricher, dispose, levelSwitch, overrideMap) { } @@ -71,13 +74,15 @@ internal Logger( ILogEventSink sink, ILogEventEnricher enricher, Action dispose = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + LevelOverrideMap overrideMap = null) { _messageTemplateProcessor = messageTemplateProcessor; _minimumLevel = minimumLevel; _sink = sink; _dispose = dispose; _levelSwitch = levelSwitch; + _overrideMap = overrideMap; _enricher = enricher; } @@ -97,7 +102,8 @@ public ILogger ForContext(ILogEventEnricher enricher) this, enricher, null, - _levelSwitch); + _levelSwitch, + _overrideMap); } /// @@ -133,8 +139,25 @@ public ILogger ForContext(string propertyName, object value, bool destructureObj // now and the first log event written... // A future optimization opportunity may be to implement ILogEventEnricher on LogEventProperty to // remove one more allocation. - return ForContext(new FixedPropertyEnricher( - _messageTemplateProcessor.CreateProperty(propertyName, value, destructureObjects))); + var enricher = new FixedPropertyEnricher(_messageTemplateProcessor.CreateProperty(propertyName, value, destructureObjects)); + + var minimumLevel = _minimumLevel; + var levelSwitch = _levelSwitch; + if (_overrideMap != null && propertyName == Constants.SourceContextPropertyName) + { + var context = value as string; + if (context != null) + _overrideMap.GetEffectiveLevel(context, out minimumLevel, out levelSwitch); + } + + return new Logger( + _messageTemplateProcessor, + minimumLevel, + this, + enricher, + null, + levelSwitch, + _overrideMap); } /// diff --git a/src/Serilog/LoggerConfiguration.cs b/src/Serilog/LoggerConfiguration.cs index cc427276f..3d8b0b646 100644 --- a/src/Serilog/LoggerConfiguration.cs +++ b/src/Serilog/LoggerConfiguration.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2015 Serilog Contributors +// 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. @@ -34,7 +34,7 @@ public class LoggerConfiguration readonly List _filters = new List(); readonly List _additionalScalarTypes = new List(); readonly List _additionalDestructuringPolicies = new List(); - + readonly Dictionary _overrides = new Dictionary(); LogEventLevel _minimumLevel = LogEventLevel.Information; LoggingLevelSwitch _levelSwitch; int _maximumDestructuringDepth = 10; @@ -72,7 +72,8 @@ public LoggerMinimumLevelConfiguration MinimumLevel _minimumLevel = l; _levelSwitch = null; }, - sw => _levelSwitch = sw); + sw => _levelSwitch = sw, + (s, lls) => _overrides[s] = lls); } } @@ -80,24 +81,12 @@ public LoggerMinimumLevelConfiguration MinimumLevel /// Configures enrichment of s. Enrichers can add, remove and /// modify the properties associated with events. /// - public LoggerEnrichmentConfiguration Enrich - { - get - { - return new LoggerEnrichmentConfiguration(this, e => _enrichers.Add(e)); - } - } + public LoggerEnrichmentConfiguration Enrich => new LoggerEnrichmentConfiguration(this, e => _enrichers.Add(e)); /// /// Configures global filtering of s. /// - public LoggerFilterConfiguration Filter - { - get - { - return new LoggerFilterConfiguration(this, f => _filters.Add(f)); - } - } + public LoggerFilterConfiguration Filter => new LoggerFilterConfiguration(this, f => _filters.Add(f)); /// /// Configures destructuring of message template parameters. @@ -117,13 +106,7 @@ public LoggerDestructuringConfiguration Destructure /// /// Apply external settings to the logger configuration. /// - public LoggerSettingsConfiguration ReadFrom - { - get - { - return new LoggerSettingsConfiguration(this); - } - } + public LoggerSettingsConfiguration ReadFrom => new LoggerSettingsConfiguration(this); /// /// Create a logger using the configured sinks, enrichers and minimum level. @@ -167,9 +150,15 @@ public Logger CreateLogger() break; } + LevelOverrideMap overrideMap = null; + if (_overrides.Count != 0) + { + overrideMap = new LevelOverrideMap(_overrides, _minimumLevel, _levelSwitch); + } + return _levelSwitch == null ? - new Logger(processor, _minimumLevel, sink, enricher, dispose) : - new Logger(processor, _levelSwitch, sink, enricher, dispose); + new Logger(processor, _minimumLevel, sink, enricher, dispose, overrideMap) : + new Logger(processor, _levelSwitch, sink, enricher, dispose, overrideMap); } } } diff --git a/test/Serilog.Tests/Core/LevelOverrideMapTests.cs b/test/Serilog.Tests/Core/LevelOverrideMapTests.cs new file mode 100644 index 000000000..dc85bdc2e --- /dev/null +++ b/test/Serilog.Tests/Core/LevelOverrideMapTests.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Serilog.Core; +using Serilog.Events; +using Xunit; + +namespace Serilog.Tests.Core +{ + public class LevelOverrideMapTests + { + [Theory] + [InlineData("Serilog", false, LevelAlias.Minimum)] + [InlineData("MyApp", true, LogEventLevel.Debug)] + [InlineData("MyAppSomething", false, LevelAlias.Minimum)] + [InlineData("MyOtherApp", false, LevelAlias.Minimum)] + [InlineData("MyApp.Something", true, LogEventLevel.Debug)] + [InlineData("MyApp.Api.Models.Person", true, LogEventLevel.Error)] + [InlineData("MyApp.Api.Controllers.AboutController", true, LogEventLevel.Information)] + [InlineData("MyApp.Api.Controllers.HomeController", true, LogEventLevel.Warning)] + [InlineData("Api.Controllers.HomeController", false, LevelAlias.Minimum)] + public void OverrideScenarios(string context, bool overrideExpected, LogEventLevel expected) + { + var overrides = new Dictionary + { + ["MyApp"] = new LoggingLevelSwitch(LogEventLevel.Debug), + ["MyApp.Api.Controllers"] = new LoggingLevelSwitch(LogEventLevel.Information), + ["MyApp.Api.Controllers.HomeController"] = new LoggingLevelSwitch(LogEventLevel.Warning), + ["MyApp.Api"] = new LoggingLevelSwitch(LogEventLevel.Error) + }; + + var lom = new LevelOverrideMap(overrides, LogEventLevel.Fatal, null); + + LoggingLevelSwitch overriddenSwitch; + LogEventLevel overriddenLevel; + lom.GetEffectiveLevel(context, out overriddenLevel, out overriddenSwitch); + + if (overrideExpected) + { + Assert.NotNull(overriddenSwitch); + Assert.Equal(expected, overriddenSwitch.MinimumLevel); + Assert.Equal(LevelAlias.Minimum, overriddenLevel); + } + else + { + Assert.Equal(LogEventLevel.Fatal, overriddenLevel); + Assert.Null(overriddenSwitch); + } + } + } +} diff --git a/test/Serilog.Tests/LoggerConfigurationTests.cs b/test/Serilog.Tests/LoggerConfigurationTests.cs index 57b4213da..abbc02716 100644 --- a/test/Serilog.Tests/LoggerConfigurationTests.cs +++ b/test/Serilog.Tests/LoggerConfigurationTests.cs @@ -327,5 +327,23 @@ public void LastMinimumLevelConfigurationWins() Assert.Equal(1, sink.Events.Count); } + + [Fact] + public void LevelOverridesArePropagated() + { + var sink = new CollectingSink(); + + var logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .WriteTo.Sink(sink) + .CreateLogger(); + + logger.Write(Some.InformationEvent()); + logger.ForContext(Serilog.Core.Constants.SourceContextPropertyName, "Microsoft.AspNet.Something").Write(Some.InformationEvent()); + logger.ForContext().Write(Some.InformationEvent()); + + Assert.Equal(2, sink.Events.Count); + } } } \ No newline at end of file