Skip to content

Test templates by comparing output with existing formatters #20

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
Dec 22, 2020
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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ 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
Expand All @@ -116,6 +116,10 @@ The following properties are available in expressions:
* `@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

Expand Down Expand Up @@ -175,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:

Expand Down
17 changes: 17 additions & 0 deletions src/Serilog.Expressions/Expressions/BuiltInProperty.cs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Hash functions for message templates. See <see cref="Compute"/>.
/// </summary>
public static class EventIdHash
{
/// <summary>
/// Compute a 32-bit hash of the provided <paramref name="messageTemplate"/>. The
/// resulting hash value can be uses as an event id in lieu of transmitting the
/// full template string.
/// </summary>
/// <param name="messageTemplate">A message template.</param>
/// <returns>A 32-bit hash of the template.</returns>
[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;
}
}
}
}
23 changes: 23 additions & 0 deletions src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.RegularExpressions;
using Serilog.Events;
using Serilog.Formatting.Display;
using Serilog.Parsing;

// ReSharper disable ParameterTypeCanBeEnumerable.Global

Expand All @@ -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<LogEventPropertyValue?> CollectSequenceElements(LogEventPropertyValue?[] elements)
Expand Down Expand Up @@ -159,5 +162,25 @@ public static string RenderMessage(LogEvent logEvent)
MessageFormatter.Format(logEvent, sw);
return sw.ToString();
}

public static LogEventPropertyValue? GetRenderings(LogEvent logEvent)
{
List<LogEventPropertyValue>? elements = null;
foreach (var token in logEvent.MessageTemplate.Tokens)
{
if (token is PropertyToken pt && pt.Format != null)
{
elements ??= new List<LogEventPropertyValue>();

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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,25 +16,34 @@ public static Expression Expand(Expression root)

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.FindElementAtWildcard(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);
Expand Down
3 changes: 3 additions & 0 deletions src/Serilog.Expressions/Expressions/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
33 changes: 33 additions & 0 deletions src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion src/Serilog.Expressions/Serilog.Expressions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Description>An embeddable mini-language for filtering, enriching, and formatting Serilog
events, ideal for use with JSON or XML configuration.</Description>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionPrefix>1.1.0</VersionPrefix>
<Authors>Serilog Contributors</Authors>
<TargetFramework>netstandard2.1</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
Loading