From bd6334ecdc95234699e3ac4fbbda622237c6860f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 28 Feb 2022 07:48:10 +0000 Subject: [PATCH] Dependancy Update: Switch to Serilog.Expressions away from deprecated Serilog.Filters.Expressions (#12000) * Uninstall Serilog.Filters.Expressions as it is deprecated by the Serilog team NOTE: This package brought in a dependenacy on a package called SuperPower from Datalust * Install replacement package Serilog.Expressions * Update implementation of Serilog.Expressions * Add new test cases to verify expressions still working Currently Serilog.Expressions only supports short names such as @x @l @m and @mt as opposed @Exception @Level @Message and @MessageTemplate * Use Serilog.Expressions NamedFilters extension point, to plug the missing Has() function in expressions that was not ported across by the library * Update to dev build of Serilog.Expressions to verify new method we can override works * Update to release build of Serilog.Expressions 3.3.0 now its been pushed to Nuget --- .../Logging/Viewer/ExpressionFilter.cs | 37 +++++++++++++----- .../Viewer/SerilogExpressionsFunctions.cs | 14 +++++++ .../Viewer/SerilogLegacyNameResolver.cs | 38 +++++++++++++++++++ .../Umbraco.Infrastructure.csproj | 2 +- .../Logging/LogviewerTests.cs | 5 ++- 5 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs create mode 100644 src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs index 7327262ec5..c4537b04ca 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Linq; using Serilog.Events; -using Serilog.Filters.Expressions; +using Serilog.Expressions; +using Umbraco.Cms.Infrastructure.Logging.Viewer; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Logging.Viewer @@ -10,32 +11,43 @@ namespace Umbraco.Cms.Core.Logging.Viewer internal class ExpressionFilter : ILogFilter { private readonly Func _filter; - private const string ExpressionOperators = "()+=*<>%-"; + private const string s_expressionOperators = "()+=*<>%-"; public ExpressionFilter(string filterExpression) { Func filter; + // Our custom Serilog Functions to extend Serilog.Expressions + // In this case we are plugging the gap for the missing Has() + // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions + // Along with patching support for the more verbose built in property names + var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); + if (string.IsNullOrEmpty(filterExpression)) { return; } // If the expression is one word and doesn't contain a serilog operator then we can perform a like search - if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c))) + if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c))) { filter = PerformMessageLikeFilter(filterExpression); } else // check if it's a valid expression { // If the expression evaluates then make it into a filter - if (FilterLanguage.TryCreateFilter(filterExpression, out var eval, out _)) + if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression compiled, out var error)) { - filter = evt => true.Equals(eval(evt)); + filter = evt => + { + LogEventPropertyValue result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; } else { - //Assume the expression was a search string and make a Like filter from that + // 'error' describes a syntax error, where it was unable to compile an expression + // Assume the expression was a search string and make a Like filter from that filter = PerformMessageLikeFilter(filterExpression); } } @@ -50,10 +62,15 @@ namespace Umbraco.Cms.Core.Logging.Viewer private Func PerformMessageLikeFilter(string filterExpression) { - var filterSearch = $"@Message like '%{FilterLanguage.EscapeLikeExpressionContent(filterExpression)}%'"; - if (FilterLanguage.TryCreateFilter(filterSearch, out var eval, out _)) + var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; + if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression compiled, out var error)) { - return evt => true.Equals(eval(evt)); + // `compiled` is a function that can be executed against `LogEvent`s: + return evt => + { + LogEventPropertyValue result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; } return null; diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs new file mode 100644 index 0000000000..92b16b9729 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs @@ -0,0 +1,14 @@ +using Serilog.Events; + +namespace Umbraco.Cms.Infrastructure.Logging.Viewer +{ + public class SerilogExpressionsFunctions + { + // This Has() code is the same as the renamed IsDefined() function + // Added this to help backport and ensure saved queries continue to work if using Has() + public static LogEventPropertyValue? Has(LogEventPropertyValue? value) + { + return new ScalarValue(value != null); + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs new file mode 100644 index 0000000000..0472a8ea16 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs @@ -0,0 +1,38 @@ +using System; +using Serilog.Expressions; + +namespace Umbraco.Cms.Infrastructure.Logging.Viewer +{ + /// + /// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality + /// Of easily allowing any static methods definied in the passed in class/type + /// To extend as functions to use for filtering logs such as Has() and any other custom ones + /// + public class SerilogLegacyNameResolver : StaticMemberNameResolver + { + public SerilogLegacyNameResolver(Type type) : base(type) + { + } + + /// + /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions + /// So we can still support the more verbose built in property names such as + /// Exception, Level, MessageTemplate etc + /// + public override bool TryResolveBuiltInPropertyName(string alias, out string target) + { + target = alias switch + { + "Exception" => "x", + "Level" => "l", + "Message" => "m", + "MessageTemplate" => "mt", + "Properties" => "p", + "Timestamp" => "t", + _ => null + }; + + return target != null; + } + } +} diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 0a50a7862c..8bc4956c2f 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -40,8 +40,8 @@ + - diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs index 755e42b15d..ab1c2151e9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs @@ -186,10 +186,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging [TestCase("", 102)] [TestCase("Has(@Exception)", 1)] + [TestCase("Has(@x)", 1)] [TestCase("Has(Duration) and Duration > 1000", 2)] - [TestCase("Not(@Level = 'Verbose') and Not(@Level= 'Debug')", 45)] + [TestCase("Not(@Level = 'Verbose') and Not(@Level = 'Debug')", 45)] + [TestCase("Not(@l = 'Verbose') and Not(@l = 'Debug')", 45)] [TestCase("StartsWith(SourceContext, 'Umbraco.Core')", 86)] [TestCase("@MessageTemplate = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 26)] + [TestCase("@mt = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 26)] [TestCase("SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", 1)] [TestCase("Contains(SortedComponentTypes[?], 'DatabaseServer')", 1)] [Test]