Skip to content

Commit 6975368

Browse files
authored
Merge pull request #27 from serilog/dev
1.1.0 Release
2 parents 8347a7b + 81bcc69 commit 6975368

26 files changed

+596
-74
lines changed

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ events, ideal for use with JSON or XML configuration.
88
Install the package from NuGet:
99

1010
```shell
11-
dotnet add package Serilog.Expressions -v 1.0.0-*
11+
dotnet add package Serilog.Expressions
1212
```
1313

1414
The package adds extension methods to Serilog's `Filter`, `WriteTo`, and
@@ -85,6 +85,8 @@ _Serilog.Expressions_ includes the `ExpressionTemplate` class for text formattin
8585
it works with any text-based Serilog sink:
8686

8787
```csharp
88+
// using Serilog.Templates;
89+
8890
Log.Logger = new LoggerConfiguration()
8991
.WriteTo.Console(new ExpressionTemplate(
9092
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
@@ -93,27 +95,31 @@ Log.Logger = new LoggerConfiguration()
9395

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

96-
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
9799
using object literals:
98100

99101
```csharp
100102
.WriteTo.Console(new ExpressionTemplate(
101-
"{ {@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"))
102104
```
103105

104106
## Language reference
105107

106-
### Built-in properties
108+
### Properties
107109

108110
The following properties are available in expressions:
109111

110-
* All first-class properties of the event; no special syntax: `SourceContext` and `Items` are used in the formatting example above
112+
* **All first-class properties of the event** — no special syntax: `SourceContext` and `Items` are used in the formatting example above
111113
* `@t` - the event's timestamp, as a `DateTimeOffset`
112114
* `@m` - the rendered message
113115
* `@mt` - the raw message template
114116
* `@l` - the event's level, as a `LogEventLevel`
115117
* `@x` - the exception associated with the event, if any, as an `Exception`
116118
* `@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.
117123

118124
### Literals
119125

@@ -140,7 +146,8 @@ A typical set of operators is supported:
140146
* Existence `is null` and `is not null`
141147
* SQL-style `like` and `not like`, with `%` and `_` wildcards (double wildcards to escape them)
142148
* Array membership with `in` and `not in`
143-
* Indexers `a[b]` and accessors `a.b`
149+
* Accessors `a.b`
150+
* Indexers `a['b']` and `a[0]`
144151
* Wildcard indexing - `a[?]` any, and `a[*]` all
145152
* Conditional `if a then b else c` (all branches required)
146153

@@ -172,12 +179,15 @@ calling a function will be undefined if:
172179
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
173180
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
174181
| `Length(x)` | Returns the length of a string or array. |
182+
| `Now()` | Returns `DateTimeOffset.Now`. |
175183
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
176184
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
177185
| `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. |
178186
| `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`. |
179188
| `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'`. |
180189
| `Undefined()` | Explicitly mark an undefined value. |
190+
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
181191

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

src/Serilog.Expressions/Expressions/Ast/Expression.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
{
33
abstract class Expression
44
{
5+
// Used only as an enabler for testing and debugging.
6+
public abstract override string ToString();
57
}
68
}

src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@ public IndexOfMatchExpression(Expression corpus, Regex regex)
1313
Corpus = corpus ?? throw new ArgumentNullException(nameof(corpus));
1414
Regex = regex ?? throw new ArgumentNullException(nameof(regex));
1515
}
16+
17+
public override string ToString()
18+
{
19+
return $"_Internal_IndexOfMatch({Corpus}, '{Regex.ToString().Replace("'", "''")}')";
20+
}
1621
}
1722
}

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
}

src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Serilog.Expressions.Compilation
1010
{
1111
static class ExpressionCompiler
1212
{
13-
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
13+
public static Expression Translate(Expression expression)
1414
{
1515
var actual = expression;
1616
actual = VariadicCallRewriter.Rewrite(actual);
@@ -19,7 +19,12 @@ public static CompiledExpression Compile(Expression expression, NameResolver nam
1919
actual = PropertiesObjectAccessorTransformer.Rewrite(actual);
2020
actual = ConstantArrayEvaluator.Evaluate(actual);
2121
actual = WildcardComprehensionTransformer.Expand(actual);
22-
22+
return actual;
23+
}
24+
25+
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
26+
{
27+
var actual = Translate(expression);
2328
return LinqExpressionCompiler.Compile(actual, nameResolver);
2429
}
2530
}
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/Text/LikeSyntaxTransformer.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ cx.Constant is ScalarValue scalar &&
5454

5555
static string LikeToRegex(string like)
5656
{
57+
var begin = "^";
5758
var regex = "";
59+
var end = "$";
60+
5861
for (var i = 0; i < like.Length; ++i)
5962
{
6063
var ch = like[i];
@@ -68,7 +71,17 @@ static string LikeToRegex(string like)
6871
}
6972
else
7073
{
71-
regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
74+
if (i == 0)
75+
begin = "";
76+
77+
if (i == like.Length - 1)
78+
end = "";
79+
80+
if (i == 0 && i == like.Length - 1)
81+
regex += ".*";
82+
83+
if (i != 0 && i != like.Length - 1)
84+
regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
7285
}
7386
}
7487
else if (ch == '_')
@@ -87,7 +100,7 @@ static string LikeToRegex(string like)
87100
regex += Regex.Escape(ch.ToString());
88101
}
89102

90-
return regex;
103+
return begin + regex + end;
91104
}
92105
}
93106
}

0 commit comments

Comments
 (0)