Skip to content

Commit

Permalink
Merge pull request #20 from serilog/renderings-property
Browse files Browse the repository at this point in the history
Test templates by comparing output with existing formatters
  • Loading branch information
nblumhardt authored Dec 22, 2020
2 parents 9c0420a + 7abed59 commit 8a7a56c
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 35 deletions.
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

0 comments on commit 8a7a56c

Please sign in to comment.