Skip to content

Commit 8a7a56c

Browse files
authored
Merge pull request #20 from serilog/renderings-property
Test templates by comparing output with existing formatters
2 parents 9c0420a + 7abed59 commit 8a7a56c

File tree

12 files changed

+377
-35
lines changed

12 files changed

+377
-35
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ Log.Logger = new LoggerConfiguration()
9595

9696
Note the use of `{Items[0]}`: "holes" in expression templates can include any valid expression.
9797

98-
Newline-delimited JSON (for example, emulating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
98+
Newline-delimited JSON (for example, replicating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
9999
using object literals:
100100

101101
```csharp
102102
.WriteTo.Console(new ExpressionTemplate(
103-
"{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
103+
"{ {@t, @mt, @r, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
104104
```
105105

106106
## Language reference
@@ -116,6 +116,10 @@ The following properties are available in expressions:
116116
* `@l` - the event's level, as a `LogEventLevel`
117117
* `@x` - the exception associated with the event, if any, as an `Exception`
118118
* `@p` - a dictionary containing all first-class properties; this supports properties with non-identifier names, for example `@p['snake-case-name']`
119+
* `@i` - event id; a 32-bit numeric hash of the event's message template
120+
* `@r` - renderings; if any tokens in the message template include .NET-specific formatting, an array of rendered values for each such token
121+
122+
The built-in properties mirror those available in the CLEF format.
119123

120124
### Literals
121125

@@ -175,12 +179,15 @@ calling a function will be undefined if:
175179
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
176180
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
177181
| `Length(x)` | Returns the length of a string or array. |
182+
| `Now()` | Returns `DateTimeOffset.Now`. |
178183
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
179184
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
180185
| `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. |
181186
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
187+
| `ToString(x, f)` | Applies the format string `f` to the formattable value `x`. |
182188
| `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'`. |
183189
| `Undefined()` | Explicitly mark an undefined value. |
190+
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
184191

185192
Functions that compare text accept an optional postfix `ci` modifier to select case-insensitive comparisons:
186193

src/Serilog.Expressions/Expressions/BuiltInProperty.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
// Copyright © Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
115
namespace Serilog.Expressions
216
{
17+
// See https://github.com/serilog/serilog-formatting-compact#reified-properties
318
static class BuiltInProperty
419
{
520
public const string Exception = "x";
@@ -8,5 +23,7 @@ static class BuiltInProperty
823
public const string Message = "m";
924
public const string MessageTemplate = "mt";
1025
public const string Properties = "p";
26+
public const string Renderings = "r";
27+
public const string EventId = "i";
1128
}
1229
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright © Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
// ReSharper disable ForCanBeConvertedToForeach
18+
19+
namespace Serilog.Expressions.Compilation.Linq
20+
{
21+
/// <summary>
22+
/// Hash functions for message templates. See <see cref="Compute"/>.
23+
/// </summary>
24+
public static class EventIdHash
25+
{
26+
/// <summary>
27+
/// Compute a 32-bit hash of the provided <paramref name="messageTemplate"/>. The
28+
/// resulting hash value can be uses as an event id in lieu of transmitting the
29+
/// full template string.
30+
/// </summary>
31+
/// <param name="messageTemplate">A message template.</param>
32+
/// <returns>A 32-bit hash of the template.</returns>
33+
[CLSCompliant(false)]
34+
public static uint Compute(string messageTemplate)
35+
{
36+
if (messageTemplate == null) throw new ArgumentNullException(nameof(messageTemplate));
37+
38+
// Jenkins one-at-a-time https://en.wikipedia.org/wiki/Jenkins_hash_function
39+
unchecked
40+
{
41+
uint hash = 0;
42+
for (var i = 0; i < messageTemplate.Length; ++i)
43+
{
44+
hash += messageTemplate[i];
45+
hash += hash << 10;
46+
hash ^= hash >> 6;
47+
}
48+
hash += hash << 3;
49+
hash ^= hash >> 11;
50+
hash += hash << 15;
51+
return hash;
52+
}
53+
}
54+
}
55+
}

src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text.RegularExpressions;
77
using Serilog.Events;
88
using Serilog.Formatting.Display;
9+
using Serilog.Parsing;
910

1011
// ReSharper disable ParameterTypeCanBeEnumerable.Global
1112

@@ -15,6 +16,8 @@ static class Intrinsics
1516
{
1617
static readonly LogEventPropertyValue NegativeOne = new ScalarValue(-1);
1718
static readonly LogEventPropertyValue Tombstone = new ScalarValue("😬 (if you see this you have found a bug.)");
19+
20+
// TODO #19: formatting is culture-specific.
1821
static readonly MessageTemplateTextFormatter MessageFormatter = new MessageTemplateTextFormatter("{Message:lj}");
1922

2023
public static List<LogEventPropertyValue?> CollectSequenceElements(LogEventPropertyValue?[] elements)
@@ -159,5 +162,25 @@ public static string RenderMessage(LogEvent logEvent)
159162
MessageFormatter.Format(logEvent, sw);
160163
return sw.ToString();
161164
}
165+
166+
public static LogEventPropertyValue? GetRenderings(LogEvent logEvent)
167+
{
168+
List<LogEventPropertyValue>? elements = null;
169+
foreach (var token in logEvent.MessageTemplate.Tokens)
170+
{
171+
if (token is PropertyToken pt && pt.Format != null)
172+
{
173+
elements ??= new List<LogEventPropertyValue>();
174+
175+
var space = new StringWriter();
176+
177+
// TODO #19: formatting is culture-specific.
178+
pt.Render(logEvent.Properties, space);
179+
elements.Add(new ScalarValue(space.ToString()));
180+
}
181+
}
182+
183+
return elements == null ? null : new SequenceValue(elements);
184+
}
162185
}
163186
}

src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
using System;
1+
// Copyright © Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
216
using System.Collections.Generic;
317
using System.Linq;
418
using System.Linq.Expressions;
@@ -124,25 +138,22 @@ protected override ExpressionBody Transform(AmbientPropertyExpression px)
124138
{
125139
if (px.IsBuiltIn)
126140
{
127-
if (px.PropertyName == BuiltInProperty.Level)
128-
return Splice(context => new ScalarValue(context.Level));
129-
130-
if (px.PropertyName == BuiltInProperty.Message)
131-
return Splice(context => new ScalarValue(Intrinsics.RenderMessage(context)));
132-
133-
if (px.PropertyName == BuiltInProperty.Exception)
134-
return Splice(context => context.Exception == null ? null : new ScalarValue(context.Exception));
135-
136-
if (px.PropertyName == BuiltInProperty.Timestamp)
137-
return Splice(context => new ScalarValue(context.Timestamp));
138-
139-
if (px.PropertyName == BuiltInProperty.MessageTemplate)
140-
return Splice(context => new ScalarValue(context.MessageTemplate.Text));
141-
142-
if (px.PropertyName == BuiltInProperty.Properties)
143-
return Splice(context => new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)), null));
144-
145-
return LX.Constant(null, typeof(LogEventPropertyValue));
141+
return px.PropertyName switch
142+
{
143+
BuiltInProperty.Level => Splice(context => new ScalarValue(context.Level)),
144+
BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context))),
145+
BuiltInProperty.Exception => Splice(context =>
146+
context.Exception == null ? null : new ScalarValue(context.Exception)),
147+
BuiltInProperty.Timestamp => Splice(context => new ScalarValue(context.Timestamp)),
148+
BuiltInProperty.MessageTemplate => Splice(context => new ScalarValue(context.MessageTemplate.Text)),
149+
BuiltInProperty.Properties => Splice(context =>
150+
new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
151+
null)),
152+
BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context)),
153+
BuiltInProperty.EventId => Splice(context =>
154+
new ScalarValue(EventIdHash.Compute(context.MessageTemplate.Text))),
155+
_ => LX.Constant(null, typeof(LogEventPropertyValue))
156+
};
146157
}
147158

148159
var propertyName = px.PropertyName;

src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Serilog.Expressions.Ast;
1+
using System.Linq;
2+
using Serilog.Expressions.Ast;
23
using Serilog.Expressions.Compilation.Transformations;
34

45
namespace Serilog.Expressions.Compilation.Wildcards
@@ -15,25 +16,34 @@ public static Expression Expand(Expression root)
1516

1617
protected override Expression Transform(CallExpression lx)
1718
{
18-
if (!Operators.WildcardComparators.Contains(lx.OperatorName) || lx.Operands.Length != 2)
19+
if (!Operators.WildcardComparators.Contains(lx.OperatorName))
1920
return base.Transform(lx);
2021

21-
var lhsIs = WildcardSearch.FindElementAtWildcard(lx.Operands[0]);
22-
var rhsIs = WildcardSearch.FindElementAtWildcard(lx.Operands[1]);
23-
if (lhsIs != null && rhsIs != null || lhsIs == null && rhsIs == null)
22+
IndexerExpression? indexer = null;
23+
Expression? wildcardPath = null;
24+
var indexerOperand = -1;
25+
for (var i = 0; i < lx.Operands.Length; ++i)
26+
{
27+
indexer = WildcardSearch.FindElementAtWildcard(lx.Operands[i]);
28+
if (indexer != null)
29+
{
30+
indexerOperand = i;
31+
wildcardPath = lx.Operands[i];
32+
break;
33+
}
34+
}
35+
36+
if (indexer == null || wildcardPath == null)
2437
return base.Transform(lx); // N/A, or invalid
2538

26-
var wildcardPath = lhsIs != null ? lx.Operands[0] : lx.Operands[1];
27-
var comparand = lhsIs != null ? lx.Operands[1] : lx.Operands[0];
28-
var indexer = lhsIs ?? rhsIs!;
29-
3039
var px = new ParameterExpression("p" + _nextParameter++);
3140
var nestedComparand = NodeReplacer.Replace(wildcardPath, indexer, px);
3241

3342
var coll = indexer.Receiver;
3443
var wc = ((IndexerWildcardExpression)indexer.Index).Wildcard;
3544

36-
var comparisonArgs = lhsIs != null ? new[] { nestedComparand, comparand } : new[] { comparand, nestedComparand };
45+
var comparisonArgs = lx.Operands.ToArray();
46+
comparisonArgs[indexerOperand] = nestedComparand;
3747
var body = new CallExpression(lx.IgnoreCase, lx.OperatorName, comparisonArgs);
3848

3949
var lambda = new LambdaExpression(new[] { px }, body);

src/Serilog.Expressions/Expressions/Operators.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ static class Operators
2424
public const string OpIsDefined = "IsDefined";
2525
public const string OpLastIndexOf = "LastIndexOf";
2626
public const string OpLength = "Length";
27+
public const string OpNow = "Now";
2728
public const string OpRound = "Round";
2829
public const string OpStartsWith = "StartsWith";
2930
public const string OpSubstring = "Substring";
3031
public const string OpTagOf = "TagOf";
32+
public const string OpToString = "ToString";
3133
public const string OpTypeOf = "TypeOf";
3234
public const string OpUndefined = "Undefined";
35+
public const string OpUtcDateTime = "UtcDateTime";
3336

3437
public const string IntermediateOpLike = "_Internal_Like";
3538
public const string IntermediateOpNotLike = "_Internal_NotLike";

src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,5 +467,38 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
467467
{
468468
return Coerce.IsTrue(condition) ? consequent : alternative;
469469
}
470+
471+
public static LogEventPropertyValue? ToString(LogEventPropertyValue? value, LogEventPropertyValue? format)
472+
{
473+
if (!(value is ScalarValue sv && sv.Value is IFormattable formattable) ||
474+
!Coerce.String(format, out var fmt))
475+
{
476+
return null;
477+
}
478+
479+
// TODO #19: formatting is culture-specific.
480+
return new ScalarValue(formattable.ToString(fmt, null));
481+
}
482+
483+
public static LogEventPropertyValue? UtcDateTime(LogEventPropertyValue? dateTime)
484+
{
485+
if (dateTime is ScalarValue sv)
486+
{
487+
if (sv.Value is DateTimeOffset dto)
488+
return new ScalarValue(dto.UtcDateTime);
489+
490+
if (sv.Value is DateTime dt)
491+
return new ScalarValue(dt.ToUniversalTime());
492+
}
493+
494+
return null;
495+
}
496+
497+
// ReSharper disable once UnusedMember.Global
498+
public static LogEventPropertyValue? Now()
499+
{
500+
// DateTimeOffset.Now is the generator for LogEvent.Timestamp.
501+
return new ScalarValue(DateTimeOffset.Now);
502+
}
470503
}
471504
}

src/Serilog.Expressions/Serilog.Expressions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<Description>An embeddable mini-language for filtering, enriching, and formatting Serilog
55
events, ideal for use with JSON or XML configuration.</Description>
6-
<VersionPrefix>1.0.1</VersionPrefix>
6+
<VersionPrefix>1.1.0</VersionPrefix>
77
<Authors>Serilog Contributors</Authors>
88
<TargetFramework>netstandard2.1</TargetFramework>
99
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,13 @@ if undefined() then 1 else 2 ⇶ 2
217217
if 'string' then 1 else 2 ⇶ 2
218218
if true then if false then 1 else 2 else 3 ⇶ 2
219219

220-
// Typeof
220+
// ToString
221+
tostring(16, '000') ⇶ '016'
222+
tostring('test', '000') ⇶ undefined()
223+
tostring(16, undefined()) ⇶ undefined()
224+
tostring(16, null) ⇶ undefined()
225+
226+
// TypeOf
221227
typeof(undefined()) ⇶ 'undefined'
222228
typeof('test') ⇶ 'System.String'
223229
typeof(10) ⇶ 'System.Decimal'
@@ -226,6 +232,9 @@ typeof(null) ⇶ 'null'
226232
typeof([]) ⇶ 'array'
227233
typeof({}) ⇶ 'object'
228234

235+
// UtcDateTime
236+
tostring(utcdatetime(now()), 'o') like '20%' ⇶ true
237+
229238
// Case comparison
230239
'test' = 'TEST' ⇶ false
231240
'tschüß' = 'TSCHÜSS' ⇶ false

0 commit comments

Comments
 (0)