Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LoggingFilterSwitch support #162

Merged
merged 6 commits into from
Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sample/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using Serilog;
using Serilog.Core;
using Serilog.Events;
using System.Collections.Generic;
using Serilog.Debugging;

namespace Sample
{
Expand Down
5 changes: 3 additions & 2 deletions sample/Sample/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"Serilog": {
"Using": [ "Serilog.Sinks.Console" ],
"LevelSwitches": { "$controlSwitch": "Verbose" },
"FilterSwitches": { "$filterSwitch": "Application = 'Sample'" },
"MinimumLevel": {
"Default": "Debug",
"Override": {
Expand Down Expand Up @@ -97,9 +98,9 @@
],
"Filter": [
{
"Name": "ByIncludingOnly",
"Name": "ControlledBy",
"Args": {
"expression": "Application = 'Sample'"
"switch": "$filterSwitch"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyColle
public void Configure(LoggerConfiguration loggerConfiguration)
{
ProcessLevelSwitchDeclarations();
ProcessFilterSwitchDeclarations();

ApplyMinimumLevel(loggerConfiguration);
ApplyEnrichment(loggerConfiguration);
Expand All @@ -50,6 +51,63 @@ public void Configure(LoggerConfiguration loggerConfiguration)
ApplyAuditSinks(loggerConfiguration);
}

void ProcessFilterSwitchDeclarations()
{
var filterSwitchesDirective = _section.GetSection("FilterSwitches");

foreach (var filterSwitchDeclaration in filterSwitchesDirective.GetChildren())
{
var filterSwitch = LoggingFilterSwitchProxy.Create();
if (filterSwitch == null)
{
SelfLog.WriteLine($"FilterSwitches section found, but Serilog.Filters.Expressions isn't referenced.");
break;
}

var switchName = filterSwitchDeclaration.Key;
// switchName must be something like $switch to avoid ambiguities
if (!IsValidSwitchName(switchName))
{
throw new FormatException($"\"{switchName}\" is not a valid name for a Filter Switch declaration. Filter switch must be declared with a '$' sign, like \"FilterSwitches\" : {{\"$switchName\" : \"{{FilterExpression}}\"}}");
}

SetFilterSwitch(throwOnError: true);
SubscribeToFilterExpressionChanges();

_resolutionContext.AddFilterSwitch(switchName, filterSwitch);

void SubscribeToFilterExpressionChanges()
{
ChangeToken.OnChange(filterSwitchDeclaration.GetReloadToken, () => SetFilterSwitch(throwOnError: false));
}

void SetFilterSwitch(bool throwOnError)
{
var filterExpr = filterSwitchDeclaration.Value;
if (string.IsNullOrWhiteSpace(filterExpr))
{
filterSwitch.Expression = null;
return;
}

try
{
filterSwitch.Expression = filterExpr;
}
catch (Exception e)
{
var errMsg = $"The expression '{filterExpr}' is invalid filter expression: {e.Message}.";
if (throwOnError)
{
throw new InvalidOperationException(errMsg, e);
}

SelfLog.WriteLine(errMsg);
}
}
}
}

void ProcessLevelSwitchDeclarations()
{
var levelSwitchesDirective = _section.GetSection("LevelSwitches");
Expand Down Expand Up @@ -94,7 +152,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration)
var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy");
if (minLevelControlledByDirective.Value != null)
{
var globalMinimumLevelSwitch = _resolutionContext.LookUpSwitchByName(minLevelControlledByDirective.Value);
var globalMinimumLevelSwitch = _resolutionContext.LookUpLevelSwitchByName(minLevelControlledByDirective.Value);
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch);
}
Expand All @@ -109,7 +167,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration)
}
else
{
var overrideSwitch = _resolutionContext.LookUpSwitchByName(overridenLevelOrSwitch);
var overrideSwitch = _resolutionContext.LookUpLevelSwitchByName(overridenLevelOrSwitch);
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;

namespace Serilog.Settings.Configuration
{
class LoggingFilterSwitchProxy
{
readonly Action<string> _setProxy;
readonly Func<string> _getProxy;

LoggingFilterSwitchProxy(object realSwitch)
{
RealSwitch = realSwitch ?? throw new ArgumentNullException(nameof(realSwitch));

var expressionProperty = realSwitch.GetType().GetProperty("Expression");

_setProxy = (Action<string>)Delegate.CreateDelegate(
typeof(Action<string>),
realSwitch,
expressionProperty.GetSetMethod());

_getProxy = (Func<string>)Delegate.CreateDelegate(
typeof(Func<string>),
realSwitch,
expressionProperty.GetGetMethod());
}

public object RealSwitch { get; }

public string Expression
{
get => _getProxy();
set => _setProxy(value);
}

public static LoggingFilterSwitchProxy Create(string expression = null)
{
var filterSwitchType =
Type.GetType("Serilog.Expressions.LoggingFilterSwitch, Serilog.Expressions") ??
Type.GetType("Serilog.Filters.Expressions.LoggingFilterSwitch, Serilog.Filters.Expressions");

if (filterSwitchType is null)
{
return null;
}

return new LoggingFilterSwitchProxy(Activator.CreateInstance(filterSwitchType, expression));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ namespace Serilog.Settings.Configuration
sealed class ResolutionContext
{
readonly IDictionary<string, LoggingLevelSwitch> _declaredLevelSwitches;
readonly IDictionary<string, LoggingFilterSwitchProxy> _declaredFilterSwitches;
readonly IConfiguration _appConfiguration;

public ResolutionContext(IConfiguration appConfiguration = null)
{
_declaredLevelSwitches = new Dictionary<string, LoggingLevelSwitch>();
_declaredFilterSwitches = new Dictionary<string, LoggingFilterSwitchProxy>();
_appConfiguration = appConfiguration;
}

Expand All @@ -26,7 +28,7 @@ public ResolutionContext(IConfiguration appConfiguration = null)
/// <param name="switchName">the name of a switch to look up</param>
/// <returns>the LoggingLevelSwitch registered with the name</returns>
/// <exception cref="InvalidOperationException">if no switch has been registered with <paramref name="switchName"/></exception>
public LoggingLevelSwitch LookUpSwitchByName(string switchName)
public LoggingLevelSwitch LookUpLevelSwitchByName(string switchName)
{
if (_declaredLevelSwitches.TryGetValue(switchName, out var levelSwitch))
{
Expand All @@ -36,6 +38,16 @@ public LoggingLevelSwitch LookUpSwitchByName(string switchName)
throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}");
}

public LoggingFilterSwitchProxy LookUpFilterSwitchByName(string switchName)
{
if (_declaredFilterSwitches.TryGetValue(switchName, out var filterSwitch))
{
return filterSwitch;
}

throw new InvalidOperationException($"No LoggingFilterSwitch has been declared with name \"{switchName}\". You might be missing a section \"FilterSwitches\":{{\"{switchName}\":\"{{FilterExpression}}\"}}");
}

public bool HasAppConfiguration => _appConfiguration != null;

public IConfiguration AppConfiguration
Expand All @@ -57,5 +69,12 @@ public void AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitc
if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch));
_declaredLevelSwitches[levelSwitchName] = levelSwitch;
}

public void AddFilterSwitch(string filterSwitchName, LoggingFilterSwitchProxy filterSwitch)
{
if (filterSwitchName == null) throw new ArgumentNullException(nameof(filterSwitchName));
if (filterSwitch == null) throw new ArgumentNullException(nameof(filterSwitch));
_declaredFilterSwitches[filterSwitchName] = filterSwitch;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)

if (toType == typeof(LoggingLevelSwitch))
{
return resolutionContext.LookUpSwitchByName(argumentValue);
return resolutionContext.LookUpLevelSwitchByName(argumentValue);
}

if (toType.FullName == "Serilog.Expressions.LoggingFilterSwitch" ||
toType.FullName == "Serilog.Filters.Expressions.LoggingFilterSwitch")
{
return resolutionContext.LookUpFilterSwitchByName(argumentValue).RealSwitch;
}

var toTypeInfo = toType.GetTypeInfo();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,33 @@ public void LoggingLevelSwitchWithInvalidNameThrowsFormatException()
Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message);
}

[Fact]
public void LoggingFilterSwitchIsConfigured()
{
var json = @"{
'Serilog': {
'FilterSwitches': { '$mySwitch': 'Prop = 42' },
'Filter:BySwitch': {
'Name': 'ControlledBy',
'Args': {
'switch': '$mySwitch'
}
}
}
}";
LogEvent evt = null;

var log = ConfigFromJson(json)
.WriteTo.Sink(new DelegatingSink(e => evt = e))
.CreateLogger();

log.Write(Some.InformationEvent());
Assert.Null(evt);

log.ForContext("Prop", 42).Write(Some.InformationEvent());
Assert.NotNull(evt);
}

[Fact]
public void LoggingLevelSwitchIsConfigured()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public class DynamicLevelChangeTests
}
},
'LevelSwitches': { '$mySwitch': 'Information' },
'FilterSwitches': { '$myFilter': null },
'Filter:Dummy': {
'Name': 'ControlledBy',
'Args': {
'switch': '$myFilter'
}
},
'WriteTo:Dummy': {
'Name': 'DummyConsole',
'Args': {
Expand Down Expand Up @@ -64,10 +71,22 @@ public void ShouldRespectDynamicLevelChanges()
UpdateConfig(overrideLevel: LogEventLevel.Debug);
logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent());
Assert.Single(DummyConsoleSink.Emitted);

DummyConsoleSink.Emitted.Clear();
UpdateConfig(filterExpression: "Prop = 'Val_1'");
logger.Write(Some.DebugEvent());
logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent());
Assert.Single(DummyConsoleSink.Emitted);

DummyConsoleSink.Emitted.Clear();
UpdateConfig(filterExpression: "Prop = 'Val_2'");
logger.Write(Some.DebugEvent());
logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent());
Assert.Empty(DummyConsoleSink.Emitted);
}
}

void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null)
void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null, string filterExpression = null)
{
if (minimumLevel.HasValue)
{
Expand All @@ -84,6 +103,11 @@ void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel
_configSource.Set("Serilog:MinimumLevel:Override:Root.Test", overrideLevel.Value.ToString());
}

if (filterExpression != null)
{
_configSource.Set("Serilog:FilterSwitches:$myFilter", filterExpression);
}

_configSource.Reload();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="Serilog.Filters.Expressions" Version="2.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.2.0" />
</ItemGroup>
Expand Down