Skip to content

Default rest() to ignore message templates by default #49

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

Merged
merged 1 commit into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ The built-in properties mirror those available in the CLEF format.
| Array | An array of values, in square brackets | `[1, 'two', null]` |
| Object | A mapping of string keys to values; keys that are valid identifiers do not need to be quoted | `{a: 1, 'b c': 2, d}` |

Array and object literals support the spread operator: `[1, 2, ..rest]`, `{a: 1, ..other}`. Specifying an undefined
Array and object literals support the spread operator: `[1, 2, ..others]`, `{a: 1, ..others}`. Specifying an undefined
property in an object literal will remove it from the result: `{..User, Email: Undefined()}`

### Operators and conditionals
Expand Down Expand Up @@ -196,7 +196,7 @@ calling a function will be undefined if:
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
| `Length(x)` | Returns the length of a string or array. |
| `Now()` | Returns `DateTimeOffset.Now`. |
| `Rest()` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template or the event's message. |
| `Rest([deep])` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template. If `deep` is `true`, also excludes properties referenced in the event's message template. |
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
| `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Serilog.Events;
using Serilog.Expressions.Ast;
using Serilog.Expressions.Compilation.Transformations;
Expand All @@ -27,6 +29,7 @@
using ParameterExpression = System.Linq.Expressions.ParameterExpression;
using LX = System.Linq.Expressions.Expression;
using ExpressionBody = System.Linq.Expressions.Expression;
// ReSharper disable UseIndexFromEndExpression

namespace Serilog.Expressions.Compilation.Linq
{
Expand Down Expand Up @@ -101,11 +104,18 @@ protected override ExpressionBody Transform(CallExpression call)
if (!_nameResolver.TryResolveFunctionName(call.OperatorName, out var m))
throw new ArgumentException($"The function name `{call.OperatorName}` was not recognized.");

var methodParameters = m.GetParameters();
var methodParameters = m.GetParameters()
.Select(info => (pi: info, optional: info.GetCustomAttribute<OptionalAttribute>() != null))
.ToList();

var parameterCount = methodParameters.Count(pi => pi.ParameterType == typeof(LogEventPropertyValue));
if (parameterCount != call.Operands.Length)
throw new ArgumentException($"The function `{call.OperatorName}` requires {parameterCount} arguments.");
var allowedParameters = methodParameters.Where(info => info.pi.ParameterType == typeof(LogEventPropertyValue)).ToList();
var requiredParameterCount = allowedParameters.Count(info => !info.optional);

if (call.Operands.Length < requiredParameterCount || call.Operands.Length > allowedParameters.Count)
{
var requirements = DescribeRequirements(allowedParameters.Select(info => (info.pi.Name!, info.optional)).ToList());
throw new ArgumentException($"The function `{call.OperatorName}` {requirements}.");
}

var operands = new Queue<LX>(call.Operands.Select(Transform));

Expand All @@ -116,11 +126,15 @@ protected override ExpressionBody Transform(CallExpression call)
if (Operators.SameOperator(call.OperatorName, Operators.RuntimeOpOr))
return CompileLogical(LX.OrElse, operands.Dequeue(), operands.Dequeue());

var boundParameters = new List<LX>(methodParameters.Length);
foreach (var pi in methodParameters)
var boundParameters = new List<LX>(methodParameters.Count);
foreach (var (pi, optional) in methodParameters)
{
if (pi.ParameterType == typeof(LogEventPropertyValue))
boundParameters.Add(operands.Dequeue());
{
boundParameters.Add(operands.Count > 0
? operands.Dequeue()
: LX.Constant(null, typeof(LogEventPropertyValue)));
}
else if (pi.ParameterType == typeof(StringComparison))
boundParameters.Add(LX.Constant(call.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
else if (pi.ParameterType == typeof(IFormatProvider))
Expand All @@ -129,13 +143,38 @@ protected override ExpressionBody Transform(CallExpression call)
boundParameters.Add(LX.Property(Context, EvaluationContextLogEventProperty));
else if (_nameResolver.TryBindFunctionParameter(pi, out var binding))
boundParameters.Add(LX.Constant(binding, pi.ParameterType));
else if (optional)
boundParameters.Add(LX.Constant(
pi.GetCustomAttribute<DefaultParameterValueAttribute>()?.Value, pi.ParameterType));
else
throw new ArgumentException($"The method `{m.Name}` implementing function `{call.OperatorName}` has parameter `{pi.Name}` which could not be bound.");
throw new ArgumentException($"The method `{m.Name}` implementing function `{call.OperatorName}` has argument `{pi.Name}` which could not be bound.");
}

return LX.Call(m, boundParameters);
}

static string DescribeRequirements(IReadOnlyList<(string name, bool optional)> parameters)
{
static string DescribeArgument((string name, bool optional) p) =>
$"`{p.name}`" + (p.optional ? " (optional)" : "");

if (parameters.Count == 0)
return "accepts no arguments";

if (parameters.Count == 1)
return $"accepts one argument, {DescribeArgument(parameters[0])}";

if (parameters.Count == 2)
return $"accepts two arguments, {DescribeArgument(parameters[0])} and {DescribeArgument(parameters[1])}";

var result = new StringBuilder("accepts arguments");
for (var i = 0; i < parameters.Count - 1; ++i)
result.Append($" {DescribeArgument(parameters[i])},");

result.Append($" and {DescribeArgument(parameters[parameters.Count - 1])}");
return result.ToString();
}

static ExpressionBody CompileLogical(Func<ExpressionBody, ExpressionBody, ExpressionBody> apply, ExpressionBody lhs, ExpressionBody rhs)
{
return LX.Convert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,11 @@ public static Expression Rewrite(Expression expression)

protected override Expression Transform(CallExpression call)
{
if (Operators.SameOperator(call.OperatorName, Operators.OpSubstring) && call.Operands.Length == 2)
{
var operands = call.Operands
.Select(Transform)
.Concat(new[] {CallUndefined()})
.ToArray();
return new CallExpression(call.IgnoreCase, call.OperatorName, operands);
}

if (Operators.SameOperator(call.OperatorName, Operators.OpCoalesce) ||
Operators.SameOperator(call.OperatorName, Operators.OpConcat))
{
if (call.Operands.Length == 0)
return CallUndefined();
return new CallExpression(false, Operators.OpUndefined);
if (call.Operands.Length == 1)
return Transform(call.Operands.Single());
if (call.Operands.Length > 2)
Expand All @@ -54,18 +45,7 @@ protected override Expression Transform(CallExpression call)
}
}

if (Operators.SameOperator(call.OperatorName, Operators.OpToString) &&
call.Operands.Length == 1)
{
return new CallExpression(call.IgnoreCase, call.OperatorName, call.Operands[0], CallUndefined());
}

return base.Transform(call);
}

static CallExpression CallUndefined()
{
return new(false, Operators.OpUndefined);
}
}
}
58 changes: 29 additions & 29 deletions src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ static bool UnboxedEqualHelper(StringComparison sc, LogEventPropertyValue? left,
return null;
}

public static LogEventPropertyValue? Round(LogEventPropertyValue? value, LogEventPropertyValue? places)
public static LogEventPropertyValue? Round(LogEventPropertyValue? number, LogEventPropertyValue? places)
{
if (!Coerce.Numeric(value, out var v) ||
if (!Coerce.Numeric(number, out var v) ||
!Coerce.Numeric(places, out var p) ||
p < 0 ||
p > 32) // Check my memory, here :D
Expand Down Expand Up @@ -266,57 +266,57 @@ static bool UnboxedEqualHelper(StringComparison sc, LogEventPropertyValue? left,
null;
}

public static LogEventPropertyValue? Contains(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
public static LogEventPropertyValue? Contains(StringComparison sc, LogEventPropertyValue? @string, LogEventPropertyValue? substring)
{
if (!Coerce.String(corpus, out var ctx) ||
!Coerce.String(pattern, out var ptx))
if (!Coerce.String(@string, out var ctx) ||
!Coerce.String(substring, out var ptx))
return null;

return ScalarBoolean(ctx.Contains(ptx, sc));
}

public static LogEventPropertyValue? IndexOf(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
public static LogEventPropertyValue? IndexOf(StringComparison sc, LogEventPropertyValue? @string, LogEventPropertyValue? substring)
{
if (!Coerce.String(corpus, out var ctx) ||
!Coerce.String(pattern, out var ptx))
if (!Coerce.String(@string, out var ctx) ||
!Coerce.String(substring, out var ptx))
return null;

return new ScalarValue(ctx.IndexOf(ptx, sc));
}

public static LogEventPropertyValue? LastIndexOf(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
public static LogEventPropertyValue? LastIndexOf(StringComparison sc, LogEventPropertyValue? @string, LogEventPropertyValue? substring)
{
if (!Coerce.String(corpus, out var ctx) ||
!Coerce.String(pattern, out var ptx))
if (!Coerce.String(@string, out var ctx) ||
!Coerce.String(substring, out var ptx))
return null;

return new ScalarValue(ctx.LastIndexOf(ptx, sc));
}

public static LogEventPropertyValue? Length(LogEventPropertyValue? arg)
public static LogEventPropertyValue? Length(LogEventPropertyValue? value)
{
if (Coerce.String(arg, out var s))
if (Coerce.String(value, out var s))
return new ScalarValue(s.Length);

if (arg is SequenceValue seq)
if (value is SequenceValue seq)
return new ScalarValue(seq.Elements.Count);

return null;
}

public static LogEventPropertyValue? StartsWith(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
public static LogEventPropertyValue? StartsWith(StringComparison sc, LogEventPropertyValue? value, LogEventPropertyValue? substring)
{
if (!Coerce.String(corpus, out var ctx) ||
!Coerce.String(pattern, out var ptx))
if (!Coerce.String(value, out var ctx) ||
!Coerce.String(substring, out var ptx))
return null;

return ScalarBoolean(ctx.StartsWith(ptx, sc));
}

public static LogEventPropertyValue? EndsWith(StringComparison sc, LogEventPropertyValue? corpus, LogEventPropertyValue? pattern)
public static LogEventPropertyValue? EndsWith(StringComparison sc, LogEventPropertyValue? value, LogEventPropertyValue? substring)
{
if (!Coerce.String(corpus, out var ctx) ||
!Coerce.String(pattern, out var ptx))
if (!Coerce.String(value, out var ctx) ||
!Coerce.String(substring, out var ptx))
return null;

return ScalarBoolean(ctx.EndsWith(ptx, sc));
Expand Down Expand Up @@ -432,17 +432,17 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
}

// Ideally this will be compiled as a short-circuiting intrinsic
public static LogEventPropertyValue? Coalesce(LogEventPropertyValue? v1, LogEventPropertyValue? v2)
public static LogEventPropertyValue? Coalesce(LogEventPropertyValue? value0, LogEventPropertyValue? value1)
{
if (v1 is null or ScalarValue {Value: null})
return v2;
if (value0 is null or ScalarValue {Value: null})
return value1;

return v1;
return value0;
}

public static LogEventPropertyValue? Substring(LogEventPropertyValue? sval, LogEventPropertyValue? startIndex, LogEventPropertyValue? length)
public static LogEventPropertyValue? Substring(LogEventPropertyValue? @string, LogEventPropertyValue? startIndex, LogEventPropertyValue? length = null)
{
if (!Coerce.String(sval, out var str) ||
if (!Coerce.String(@string, out var str) ||
!Coerce.Numeric(startIndex, out var si))
return null;

Expand All @@ -461,9 +461,9 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
return new ScalarValue(str.Substring((int)si, (int)len));
}

public static LogEventPropertyValue? Concat(LogEventPropertyValue? first, LogEventPropertyValue? second)
public static LogEventPropertyValue? Concat(LogEventPropertyValue? string0, LogEventPropertyValue? string1)
{
if (Coerce.String(first, out var f) && Coerce.String(second, out var s))
if (Coerce.String(string0, out var f) && Coerce.String(string1, out var s))
{
return new ScalarValue(f + s);
}
Expand Down Expand Up @@ -492,7 +492,7 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
return Coerce.IsTrue(condition) ? consequent : alternative;
}

public static LogEventPropertyValue? ToString(IFormatProvider? formatProvider, LogEventPropertyValue? value, LogEventPropertyValue? format)
public static LogEventPropertyValue? ToString(IFormatProvider? formatProvider, LogEventPropertyValue? value, LogEventPropertyValue? format = null)
{
if (value is not ScalarValue sv ||
sv.Value == null ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Reflection;
using Serilog.Events;
using Serilog.Expressions;
using Serilog.Expressions.Runtime;
using Serilog.Parsing;
using Serilog.Templates.Ast;

Expand Down Expand Up @@ -74,11 +75,12 @@ public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)]

// By convention, built-in functions accept and return nullable values.
// ReSharper disable once ReturnTypeCanBeNotNullable
public static LogEventPropertyValue? Implementation(UnreferencedPropertiesFunction self, LogEvent logEvent)
public static LogEventPropertyValue? Implementation(UnreferencedPropertiesFunction self, LogEvent logEvent, LogEventPropertyValue? deep = null)
{
var checkMessageTemplate = Coerce.IsTrue(deep);
return new StructureValue(logEvent.Properties
.Where(kvp => !(self._referencedInTemplate.Contains(kvp.Key) ||
TemplateContainsPropertyName(logEvent.MessageTemplate, kvp.Key)))
.Where(kvp => !self._referencedInTemplate.Contains(kvp.Key) &&
(!checkMessageTemplate || !TemplateContainsPropertyName(logEvent.MessageTemplate, kvp.Key)))
.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ A{#if false}B{#else if true}C{#end} ⇶ AC
{#if true}A{#each a in [1]}B{a}{#end}C{#end}D ⇶ AB1CD
{#each a in []}{a}!{#else}none{#end} ⇶ none
Culture-specific {42.34} ⇶ Culture-specific 42,34
{rest()} ⇶ {"Name":"nblumhardt"}
{Name} {rest()} ⇶ nblumhardt {}
{rest(true)} ⇶ {}
16 changes: 15 additions & 1 deletion test/Serilog.Expressions.Tests/ExpressionCompilerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Serilog.Events;
using System;
using Serilog.Events;
using System.Linq;
using Serilog.Expressions.Tests.Support;
using Xunit;
Expand Down Expand Up @@ -134,6 +135,19 @@ public void InExaminesSequenceValues()
Some.InformationEvent());
}

[Theory]
[InlineData("now(1)", "The function `now` accepts no arguments.")]
[InlineData("length()", "The function `length` accepts one argument, `value`.")]
[InlineData("length(1, 2)", "The function `length` accepts one argument, `value`.")]
[InlineData("round()", "The function `round` accepts two arguments, `number` and `places`.")]
[InlineData("substring()", "The function `substring` accepts arguments `string`, `startIndex`, and `length` (optional).")]
public void ReportsArityMismatches(string call, string expectedError)
{
// These will eventually be reported gracefully by `TryCompile()`...
var ex = Assert.Throws<ArgumentException>(() => SerilogExpression.Compile(call));
Assert.Equal(expectedError, ex.Message);
}

static void AssertEvaluation(string expression, LogEvent match, params LogEvent[] noMatches)
{
var sink = new CollectingSink();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Serilog.Events;
using Serilog.Parsing;
using Serilog.Templates.Ast;
Expand All @@ -20,7 +21,7 @@ public void UnreferencedPropertiesFunctionIsNamedRest()
[Fact]
public void UnreferencedPropertiesExcludeThoseInMessageAndTemplate()
{
Assert.True(new TemplateParser().TryParse("{A + 1}{#if true}{B}{#end}", out var template, out _));
Assert.True(new TemplateParser().TryParse("{@m}{A + 1}{#if true}{B}{#end}", out var template, out _));

var function = new UnreferencedPropertiesFunction(template!);

Expand All @@ -37,11 +38,16 @@ public void UnreferencedPropertiesExcludeThoseInMessageAndTemplate()
new LogEventProperty("D", new ScalarValue(null)),
});

var result = UnreferencedPropertiesFunction.Implementation(function, evt);
var deep = UnreferencedPropertiesFunction.Implementation(function, evt, new ScalarValue(true));

var sv = Assert.IsType<StructureValue>(result);
var sv = Assert.IsType<StructureValue>(deep);
var included = Assert.Single(sv.Properties);
Assert.Equal("D", included!.Name);

var shallow = UnreferencedPropertiesFunction.Implementation(function, evt);
sv = Assert.IsType<StructureValue>(shallow);
Assert.Contains(sv.Properties, p => p.Name == "C");
Assert.Contains(sv.Properties, p => p.Name == "D");
}
}
}