diff --git a/Build.ps1 b/Build.ps1
index 8283851..1ea9c5e 100644
--- a/Build.ps1
+++ b/Build.ps1
@@ -11,7 +11,7 @@ if(Test-Path .\artifacts) {
$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL];
$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
-$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]
+$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"]
$commitHash = $(git rev-parse --short HEAD)
$buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]
@@ -57,4 +57,4 @@ foreach ($test in ls test/*.PerformanceTests) {
Pop-Location
}
-Pop-Location
\ No newline at end of file
+Pop-Location
diff --git a/README.md b/README.md
index 683e821..2d497f7 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ events, ideal for use with JSON or XML configuration.
Install the package from NuGet:
```shell
-dotnet add package Serilog.Expressions -v 1.0.0-*
+dotnet add package Serilog.Expressions
```
The package adds extension methods to Serilog's `Filter`, `WriteTo`, and
@@ -85,6 +85,8 @@ _Serilog.Expressions_ includes the `ExpressionTemplate` class for text formattin
it works with any text-based Serilog sink:
```csharp
+// using Serilog.Templates;
+
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
@@ -93,27 +95,31 @@ Log.Logger = new LoggerConfiguration()
Note the use of `{Items[0]}`: "holes" in expression templates can include any valid expression.
-Newline-delimited JSON (for example, emulating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
+Newline-delimited JSON (for example, replicating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
using object literals:
```csharp
.WriteTo.Console(new ExpressionTemplate(
- "{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
+ "{ {@t, @mt, @r, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
```
## Language reference
-### Built-in properties
+### Properties
The following properties are available in expressions:
- * All first-class properties of the event; no special syntax: `SourceContext` and `Items` are used in the formatting example above
+ * **All first-class properties of the event** — no special syntax: `SourceContext` and `Items` are used in the formatting example above
* `@t` - the event's timestamp, as a `DateTimeOffset`
* `@m` - the rendered message
* `@mt` - the raw message template
* `@l` - the event's level, as a `LogEventLevel`
* `@x` - the exception associated with the event, if any, as an `Exception`
* `@p` - a dictionary containing all first-class properties; this supports properties with non-identifier names, for example `@p['snake-case-name']`
+ * `@i` - event id; a 32-bit numeric hash of the event's message template
+ * `@r` - renderings; if any tokens in the message template include .NET-specific formatting, an array of rendered values for each such token
+
+The built-in properties mirror those available in the CLEF format.
### Literals
@@ -140,7 +146,8 @@ A typical set of operators is supported:
* Existence `is null` and `is not null`
* SQL-style `like` and `not like`, with `%` and `_` wildcards (double wildcards to escape them)
* Array membership with `in` and `not in`
- * Indexers `a[b]` and accessors `a.b`
+ * Accessors `a.b`
+ * Indexers `a['b']` and `a[0]`
* Wildcard indexing - `a[?]` any, and `a[*]` all
* Conditional `if a then b else c` (all branches required)
@@ -172,12 +179,15 @@ calling a function will be undefined if:
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
| `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`. |
| `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. |
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
+| `ToString(x, f)` | Applies the format string `f` to the formattable value `x`. |
| `TypeOf(x)` | Returns a string describing the type of expression `x`: a .NET type name if `x` is scalar and non-null, or, `'array'`, `'object'`, `'dictionary'`, `'null'`, or `'undefined'`. |
| `Undefined()` | Explicitly mark an undefined value. |
+| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
Functions that compare text accept an optional postfix `ci` modifier to select case-insensitive comparisons:
diff --git a/src/Serilog.Expressions/Expressions/Ast/Expression.cs b/src/Serilog.Expressions/Expressions/Ast/Expression.cs
index b67abcd..045d1dd 100644
--- a/src/Serilog.Expressions/Expressions/Ast/Expression.cs
+++ b/src/Serilog.Expressions/Expressions/Ast/Expression.cs
@@ -2,5 +2,7 @@
{
abstract class Expression
{
+ // Used only as an enabler for testing and debugging.
+ public abstract override string ToString();
}
}
diff --git a/src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs b/src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs
index 2de6262..574ae0a 100644
--- a/src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs
+++ b/src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs
@@ -13,5 +13,10 @@ public IndexOfMatchExpression(Expression corpus, Regex regex)
Corpus = corpus ?? throw new ArgumentNullException(nameof(corpus));
Regex = regex ?? throw new ArgumentNullException(nameof(regex));
}
+
+ public override string ToString()
+ {
+ return $"_Internal_IndexOfMatch({Corpus}, '{Regex.ToString().Replace("'", "''")}')";
+ }
}
}
diff --git a/src/Serilog.Expressions/Expressions/BuiltInProperty.cs b/src/Serilog.Expressions/Expressions/BuiltInProperty.cs
index e499dda..cf1a563 100644
--- a/src/Serilog.Expressions/Expressions/BuiltInProperty.cs
+++ b/src/Serilog.Expressions/Expressions/BuiltInProperty.cs
@@ -1,5 +1,20 @@
+// Copyright © 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.
+
namespace Serilog.Expressions
{
+ // See https://github.com/serilog/serilog-formatting-compact#reified-properties
static class BuiltInProperty
{
public const string Exception = "x";
@@ -8,5 +23,7 @@ static class BuiltInProperty
public const string Message = "m";
public const string MessageTemplate = "mt";
public const string Properties = "p";
+ public const string Renderings = "r";
+ public const string EventId = "i";
}
}
\ No newline at end of file
diff --git a/src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs b/src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs
index 7a1a4b5..01639e4 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs
@@ -10,7 +10,7 @@ namespace Serilog.Expressions.Compilation
{
static class ExpressionCompiler
{
- public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
+ public static Expression Translate(Expression expression)
{
var actual = expression;
actual = VariadicCallRewriter.Rewrite(actual);
@@ -19,7 +19,12 @@ public static CompiledExpression Compile(Expression expression, NameResolver nam
actual = PropertiesObjectAccessorTransformer.Rewrite(actual);
actual = ConstantArrayEvaluator.Evaluate(actual);
actual = WildcardComprehensionTransformer.Expand(actual);
-
+ return actual;
+ }
+
+ public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
+ {
+ var actual = Translate(expression);
return LinqExpressionCompiler.Compile(actual, nameResolver);
}
}
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs b/src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs
new file mode 100644
index 0000000..087a86f
--- /dev/null
+++ b/src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs
@@ -0,0 +1,55 @@
+// Copyright © 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;
+
+// ReSharper disable ForCanBeConvertedToForeach
+
+namespace Serilog.Expressions.Compilation.Linq
+{
+ ///
+ /// Hash functions for message templates. See .
+ ///
+ public static class EventIdHash
+ {
+ ///
+ /// Compute a 32-bit hash of the provided . The
+ /// resulting hash value can be uses as an event id in lieu of transmitting the
+ /// full template string.
+ ///
+ /// A message template.
+ /// A 32-bit hash of the template.
+ [CLSCompliant(false)]
+ public static uint Compute(string messageTemplate)
+ {
+ if (messageTemplate == null) throw new ArgumentNullException(nameof(messageTemplate));
+
+ // Jenkins one-at-a-time https://en.wikipedia.org/wiki/Jenkins_hash_function
+ unchecked
+ {
+ uint hash = 0;
+ for (var i = 0; i < messageTemplate.Length; ++i)
+ {
+ hash += messageTemplate[i];
+ hash += hash << 10;
+ hash ^= hash >> 6;
+ }
+ hash += hash << 3;
+ hash ^= hash >> 11;
+ hash += hash << 15;
+ return hash;
+ }
+ }
+ }
+}
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs b/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
index 5892758..ff4b3a5 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
@@ -6,6 +6,7 @@
using System.Text.RegularExpressions;
using Serilog.Events;
using Serilog.Formatting.Display;
+using Serilog.Parsing;
// ReSharper disable ParameterTypeCanBeEnumerable.Global
@@ -15,6 +16,8 @@ static class Intrinsics
{
static readonly LogEventPropertyValue NegativeOne = new ScalarValue(-1);
static readonly LogEventPropertyValue Tombstone = new ScalarValue("😬 (if you see this you have found a bug.)");
+
+ // TODO #19: formatting is culture-specific.
static readonly MessageTemplateTextFormatter MessageFormatter = new MessageTemplateTextFormatter("{Message:lj}");
public static List CollectSequenceElements(LogEventPropertyValue?[] elements)
@@ -159,5 +162,25 @@ public static string RenderMessage(LogEvent logEvent)
MessageFormatter.Format(logEvent, sw);
return sw.ToString();
}
+
+ public static LogEventPropertyValue? GetRenderings(LogEvent logEvent)
+ {
+ List? elements = null;
+ foreach (var token in logEvent.MessageTemplate.Tokens)
+ {
+ if (token is PropertyToken pt && pt.Format != null)
+ {
+ elements ??= new List();
+
+ var space = new StringWriter();
+
+ // TODO #19: formatting is culture-specific.
+ pt.Render(logEvent.Properties, space);
+ elements.Add(new ScalarValue(space.ToString()));
+ }
+ }
+
+ return elements == null ? null : new SequenceValue(elements);
+ }
}
}
\ No newline at end of file
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs b/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs
index bfcfd85..d56fe2e 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs
@@ -1,4 +1,18 @@
-using System;
+// Copyright © 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 System.Linq.Expressions;
@@ -124,25 +138,22 @@ protected override ExpressionBody Transform(AmbientPropertyExpression px)
{
if (px.IsBuiltIn)
{
- if (px.PropertyName == BuiltInProperty.Level)
- return Splice(context => new ScalarValue(context.Level));
-
- if (px.PropertyName == BuiltInProperty.Message)
- return Splice(context => new ScalarValue(Intrinsics.RenderMessage(context)));
-
- if (px.PropertyName == BuiltInProperty.Exception)
- return Splice(context => context.Exception == null ? null : new ScalarValue(context.Exception));
-
- if (px.PropertyName == BuiltInProperty.Timestamp)
- return Splice(context => new ScalarValue(context.Timestamp));
-
- if (px.PropertyName == BuiltInProperty.MessageTemplate)
- return Splice(context => new ScalarValue(context.MessageTemplate.Text));
-
- if (px.PropertyName == BuiltInProperty.Properties)
- return Splice(context => new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)), null));
-
- return LX.Constant(null, typeof(LogEventPropertyValue));
+ return px.PropertyName switch
+ {
+ BuiltInProperty.Level => Splice(context => new ScalarValue(context.Level)),
+ BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context))),
+ BuiltInProperty.Exception => Splice(context =>
+ context.Exception == null ? null : new ScalarValue(context.Exception)),
+ BuiltInProperty.Timestamp => Splice(context => new ScalarValue(context.Timestamp)),
+ BuiltInProperty.MessageTemplate => Splice(context => new ScalarValue(context.MessageTemplate.Text)),
+ BuiltInProperty.Properties => Splice(context =>
+ new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
+ null)),
+ BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context)),
+ BuiltInProperty.EventId => Splice(context =>
+ new ScalarValue(EventIdHash.Compute(context.MessageTemplate.Text))),
+ _ => LX.Constant(null, typeof(LogEventPropertyValue))
+ };
}
var propertyName = px.PropertyName;
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Text/LikeSyntaxTransformer.cs b/src/Serilog.Expressions/Expressions/Compilation/Text/LikeSyntaxTransformer.cs
index 863b6fb..e4a4463 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Text/LikeSyntaxTransformer.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Text/LikeSyntaxTransformer.cs
@@ -54,7 +54,10 @@ cx.Constant is ScalarValue scalar &&
static string LikeToRegex(string like)
{
+ var begin = "^";
var regex = "";
+ var end = "$";
+
for (var i = 0; i < like.Length; ++i)
{
var ch = like[i];
@@ -68,7 +71,17 @@ static string LikeToRegex(string like)
}
else
{
- regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
+ if (i == 0)
+ begin = "";
+
+ if (i == like.Length - 1)
+ end = "";
+
+ if (i == 0 && i == like.Length - 1)
+ regex += ".*";
+
+ if (i != 0 && i != like.Length - 1)
+ regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
}
}
else if (ch == '_')
@@ -87,7 +100,7 @@ static string LikeToRegex(string like)
regex += Regex.Escape(ch.ToString());
}
- return regex;
+ return begin + regex + end;
}
}
}
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs b/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs
index 1bcaf0d..51a4ecd 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs
@@ -1,4 +1,5 @@
-using Serilog.Expressions.Ast;
+using System.Linq;
+using Serilog.Expressions.Ast;
using Serilog.Expressions.Compilation.Transformations;
namespace Serilog.Expressions.Compilation.Wildcards
@@ -13,27 +14,45 @@ public static Expression Expand(Expression root)
return wc.Transform(root);
}
+ // This matches expression fragments such as `A[?] = 'test'` and
+ // transforms them into `any(A, |p| p = 'test)`.
+ //
+ // As the comparand in such expressions can be complex, e.g.
+ // `Substring(A[?], 0, 4) = 'test')`, the search for `?` and `*` wildcards
+ // is deep, but, it terminates upon reaching any other wildcard-compatible
+ // comparison. Thus `(A[?] = 'test') = true` will result in `any(A, |p| p = 'test') = true` and
+ // not `any(A, |p| (p = 'test') = true)`, which is important because short-circuiting when the first
+ // argument to `any()` is undefined will change the semantics of the resulting expression, otherwise.
protected override Expression Transform(CallExpression lx)
{
- if (!Operators.WildcardComparators.Contains(lx.OperatorName) || lx.Operands.Length != 2)
+ if (!Operators.WildcardComparators.Contains(lx.OperatorName))
return base.Transform(lx);
- var lhsIs = WildcardSearch.FindElementAtWildcard(lx.Operands[0]);
- var rhsIs = WildcardSearch.FindElementAtWildcard(lx.Operands[1]);
- if (lhsIs != null && rhsIs != null || lhsIs == null && rhsIs == null)
+ IndexerExpression? indexer = null;
+ Expression? wildcardPath = null;
+ var indexerOperand = -1;
+ for (var i = 0; i < lx.Operands.Length; ++i)
+ {
+ indexer = WildcardSearch.FindWildcardIndexer(lx.Operands[i]);
+ if (indexer != null)
+ {
+ indexerOperand = i;
+ wildcardPath = lx.Operands[i];
+ break;
+ }
+ }
+
+ if (indexer == null || wildcardPath == null)
return base.Transform(lx); // N/A, or invalid
- var wildcardPath = lhsIs != null ? lx.Operands[0] : lx.Operands[1];
- var comparand = lhsIs != null ? lx.Operands[1] : lx.Operands[0];
- var indexer = lhsIs ?? rhsIs!;
-
var px = new ParameterExpression("p" + _nextParameter++);
var nestedComparand = NodeReplacer.Replace(wildcardPath, indexer, px);
var coll = indexer.Receiver;
var wc = ((IndexerWildcardExpression)indexer.Index).Wildcard;
- var comparisonArgs = lhsIs != null ? new[] { nestedComparand, comparand } : new[] { comparand, nestedComparand };
+ var comparisonArgs = lx.Operands.ToArray();
+ comparisonArgs[indexerOperand] = nestedComparand;
var body = new CallExpression(lx.IgnoreCase, lx.OperatorName, comparisonArgs);
var lambda = new LambdaExpression(new[] { px }, body);
@@ -42,5 +61,19 @@ protected override Expression Transform(CallExpression lx)
var call = new CallExpression(false, op, coll, lambda);
return Transform(call);
}
+
+ // Detects and transforms standalone `A[?]` fragments that are not part of a comparision; these
+ // are effectively Boolean tests.
+ protected override Expression Transform(IndexerExpression ix)
+ {
+ if (!(ix.Index is IndexerWildcardExpression wx))
+ return base.Transform(ix);
+
+ var px = new ParameterExpression("p" + _nextParameter++);
+ var coll = Transform(ix.Receiver);
+ var lambda = new LambdaExpression(new[] { px }, px);
+ var op = Operators.ToRuntimeWildcardOperator(wx.Wildcard);
+ return new CallExpression(false, op, coll, lambda);
+ }
}
}
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardSearch.cs b/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardSearch.cs
index 5e3b2ec..9b07536 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardSearch.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardSearch.cs
@@ -8,7 +8,7 @@ class WildcardSearch : SerilogExpressionTransformer
{
static readonly WildcardSearch Instance = new WildcardSearch();
- public static IndexerExpression? FindElementAtWildcard(Expression fx)
+ public static IndexerExpression? FindWildcardIndexer(Expression fx)
{
return Instance.Transform(fx);
}
@@ -59,6 +59,11 @@ class WildcardSearch : SerilogExpressionTransformer
protected override IndexerExpression? Transform(CallExpression lx)
{
+ // If we hit a wildcard-compatible operation, then any wildcards within its operands "belong" to
+ // it and can't be the result of this search.
+ if (Operators.WildcardComparators.Contains(lx.OperatorName))
+ return null;
+
return lx.Operands.Select(Transform).FirstOrDefault(e => e != null);
}
diff --git a/src/Serilog.Expressions/Expressions/Helpers.cs b/src/Serilog.Expressions/Expressions/Helpers.cs
new file mode 100644
index 0000000..368f816
--- /dev/null
+++ b/src/Serilog.Expressions/Expressions/Helpers.cs
@@ -0,0 +1,44 @@
+// Copyright 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.
+
+#if NETSTANDARD2_0
+
+using System;
+
+namespace Serilog.Expressions
+{
+ ///
+ /// Helper methods.
+ ///
+ internal static class Helpers
+ {
+ ///
+ /// Backport .NET Standard 2.1 additions to maintain .NET Standard 2.0 compatibility.
+ /// Returns a value indicating whether a specified string occurs within this string, using the specified comparison rules.
+ ///
+ /// From;
+ /// https://github.com/dotnet/runtime/issues/22198
+ /// https://stackoverflow.com/questions/444798/case-insensitive-containsstring/444818#444818
+ ///
+ /// input string
+ /// The string to seek.
+ /// Specifies the rule to use in the comparison.
+ ///
+ public static bool Contains(this string source, string value, StringComparison comparisonType)
+ {
+ return source?.IndexOf(value, comparisonType) >= 0;
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Serilog.Expressions/Expressions/Operators.cs b/src/Serilog.Expressions/Expressions/Operators.cs
index cd22499..b8b7d30 100644
--- a/src/Serilog.Expressions/Expressions/Operators.cs
+++ b/src/Serilog.Expressions/Expressions/Operators.cs
@@ -24,12 +24,15 @@ static class Operators
public const string OpIsDefined = "IsDefined";
public const string OpLastIndexOf = "LastIndexOf";
public const string OpLength = "Length";
+ public const string OpNow = "Now";
public const string OpRound = "Round";
public const string OpStartsWith = "StartsWith";
public const string OpSubstring = "Substring";
public const string OpTagOf = "TagOf";
+ public const string OpToString = "ToString";
public const string OpTypeOf = "TypeOf";
public const string OpUndefined = "Undefined";
+ public const string OpUtcDateTime = "UtcDateTime";
public const string IntermediateOpLike = "_Internal_Like";
public const string IntermediateOpNotLike = "_Internal_NotLike";
diff --git a/src/Serilog.Expressions/Expressions/Parsing/ExpressionParser.cs b/src/Serilog.Expressions/Expressions/Parsing/ExpressionParser.cs
index 6c66f9f..eeca008 100644
--- a/src/Serilog.Expressions/Expressions/Parsing/ExpressionParser.cs
+++ b/src/Serilog.Expressions/Expressions/Parsing/ExpressionParser.cs
@@ -8,9 +8,9 @@ static class ExpressionParser
{
static ExpressionTokenizer Tokenizer { get; } = new ExpressionTokenizer();
- public static Expression Parse(string filterExpression)
+ public static Expression Parse(string expression)
{
- if (!TryParse(filterExpression, out var root, out var error))
+ if (!TryParse(expression, out var root, out var error))
throw new ArgumentException(error);
return root;
diff --git a/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs b/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
index 7cdc21d..580fa2b 100644
--- a/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
+++ b/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
@@ -467,5 +467,38 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
{
return Coerce.IsTrue(condition) ? consequent : alternative;
}
+
+ public static LogEventPropertyValue? ToString(LogEventPropertyValue? value, LogEventPropertyValue? format)
+ {
+ if (!(value is ScalarValue sv && sv.Value is IFormattable formattable) ||
+ !Coerce.String(format, out var fmt))
+ {
+ return null;
+ }
+
+ // TODO #19: formatting is culture-specific.
+ return new ScalarValue(formattable.ToString(fmt, null));
+ }
+
+ public static LogEventPropertyValue? UtcDateTime(LogEventPropertyValue? dateTime)
+ {
+ if (dateTime is ScalarValue sv)
+ {
+ if (sv.Value is DateTimeOffset dto)
+ return new ScalarValue(dto.UtcDateTime);
+
+ if (sv.Value is DateTime dt)
+ return new ScalarValue(dt.ToUniversalTime());
+ }
+
+ return null;
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public static LogEventPropertyValue? Now()
+ {
+ // DateTimeOffset.Now is the generator for LogEvent.Timestamp.
+ return new ScalarValue(DateTimeOffset.Now);
+ }
}
}
diff --git a/src/Serilog.Expressions/Expressions/SerilogExpression.cs b/src/Serilog.Expressions/Expressions/SerilogExpression.cs
index ae420d4..b8e2a25 100644
--- a/src/Serilog.Expressions/Expressions/SerilogExpression.cs
+++ b/src/Serilog.Expressions/Expressions/SerilogExpression.cs
@@ -13,7 +13,6 @@
// limitations under the License.
using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Serilog.Expressions.Compilation;
diff --git a/src/Serilog.Expressions/Serilog.Expressions.csproj b/src/Serilog.Expressions/Serilog.Expressions.csproj
index 3445ec0..5dfdc9c 100644
--- a/src/Serilog.Expressions/Serilog.Expressions.csproj
+++ b/src/Serilog.Expressions/Serilog.Expressions.csproj
@@ -3,9 +3,9 @@
An embeddable mini-language for filtering, enriching, and formatting Serilog
events, ideal for use with JSON or XML configuration.
- 1.0.0
+ 1.1.0
Serilog Contributors
- netstandard2.1
+ netstandard2.0;netstandard2.1
true
true
Serilog
diff --git a/src/Serilog.Expressions/System.Diagnostics.CodeAnalysis/NotNullAttributes.cs b/src/Serilog.Expressions/System.Diagnostics.CodeAnalysis/NotNullAttributes.cs
new file mode 100644
index 0000000..f2b79ba
--- /dev/null
+++ b/src/Serilog.Expressions/System.Diagnostics.CodeAnalysis/NotNullAttributes.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NETSTANDARD2_0
+//From: https://medium.com/@SergioPedri/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb
+namespace System.Diagnostics.CodeAnalysis
+{
+ /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
+ internal sealed class NotNullAttribute : Attribute { }
+
+ /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it.
+ [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
+ internal sealed class MaybeNullWhenAttribute : Attribute
+ {
+ /// Initializes the attribute with the specified return value condition.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter may be null.
+ ///
+ public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv b/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv
index a783b3e..cafaa5d 100644
--- a/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv
+++ b/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv
@@ -217,7 +217,13 @@ if undefined() then 1 else 2 ⇶ 2
if 'string' then 1 else 2 ⇶ 2
if true then if false then 1 else 2 else 3 ⇶ 2
-// Typeof
+// ToString
+tostring(16, '000') ⇶ '016'
+tostring('test', '000') ⇶ undefined()
+tostring(16, undefined()) ⇶ undefined()
+tostring(16, null) ⇶ undefined()
+
+// TypeOf
typeof(undefined()) ⇶ 'undefined'
typeof('test') ⇶ 'System.String'
typeof(10) ⇶ 'System.Decimal'
@@ -226,6 +232,9 @@ typeof(null) ⇶ 'null'
typeof([]) ⇶ 'array'
typeof({}) ⇶ 'object'
+// UtcDateTime
+tostring(utcdatetime(now()), 'o') like '20%' ⇶ true
+
// Case comparison
'test' = 'TEST' ⇶ false
'tschüß' = 'TSCHÜSS' ⇶ false
@@ -258,3 +267,7 @@ undefined() = undefined() ci ⇶ undefined()
'test' like '%s_' ⇶ true
'test' like '%' ⇶ true
'test' like 't%s%' ⇶ true
+'test' like 'es' ⇶ false
+'test' like '%' ⇶ true
+'test' like '' ⇶ false
+'' like '' ⇶ true
diff --git a/test/Serilog.Expressions.Tests/Cases/translation-cases.asv b/test/Serilog.Expressions.Tests/Cases/translation-cases.asv
new file mode 100644
index 0000000..7163e7d
--- /dev/null
+++ b/test/Serilog.Expressions.Tests/Cases/translation-cases.asv
@@ -0,0 +1,20 @@
+// Like
+A like 'a' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a$'), -1)
+A like 'a%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a'), -1)
+A like '%a%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a'), -1)
+A like '%a' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a$'), -1)
+A like '%a_b%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a.b'), -1)
+A like 'a%b%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a(?:.|\r|\n)*b'), -1)
+A like '%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '.*'), -1)
+
+// Root properties
+@p['Test'] ⇶ Test
+@p[Test] ⇶ @p[Test]
+
+// Variadics
+coalesce(A, B, C, D) ⇶ coalesce(A, coalesce(B, coalesce(C, D)))
+
+// Wildcards!
+A[?] ⇶ _Internal_Any(A, |$$p0| {$$p0})
+A or B[*] ⇶ _Internal_Or(A, _Internal_All(B, |$$p0| {$$p0}))
+not (A is not null) or not (A[?] = 'a') ⇶ _Internal_Or(_Internal_Not(_Internal_IsNotNull(A)), _Internal_Not(_Internal_Any(A, |$$p0| {_Internal_Equal($$p0, 'a')})))
diff --git a/test/Serilog.Expressions.Tests/ExpressionEvaluationTests.cs b/test/Serilog.Expressions.Tests/ExpressionEvaluationTests.cs
index 5c33a79..0532a9b 100644
--- a/test/Serilog.Expressions.Tests/ExpressionEvaluationTests.cs
+++ b/test/Serilog.Expressions.Tests/ExpressionEvaluationTests.cs
@@ -11,20 +11,8 @@ namespace Serilog.Expressions.Tests
{
public class ExpressionEvaluationTests
{
- static readonly string CasesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "Cases");
-
- static IEnumerable