Skip to content

Commit e63a0d5

Browse files
authored
Merge pull request #32 from serilog/dev
2.0.0 Release
2 parents 0d76d22 + 3ce8fc6 commit e63a0d5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1199
-304
lines changed

README.md

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# _Serilog Expressions_ [![Build status](https://ci.appveyor.com/api/projects/status/w7igkk3w51h481r6/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-expressions/branch/dev) [![NuGet Package](https://img.shields.io/nuget/vpre/serilog.expressions)](https://nuget.org/packages/serilog.expressions)
1+
# _Serilog Expressions_ [![Build status](https://ci.appveyor.com/api/projects/status/vmcskdk2wjn1rpps/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-expressions/branch/dev) [![NuGet Package](https://img.shields.io/nuget/vpre/serilog.expressions)](https://nuget.org/packages/serilog.expressions)
22

33
An embeddable mini-language for filtering, enriching, and formatting Serilog
44
events, ideal for use with JSON or XML configuration.
@@ -79,7 +79,7 @@ _Serilog.Expressions_ adds a number of expression-based overloads and helper met
7979
* `Enrich.When()` - conditionally enable an enricher when events match an expression
8080
* `Enrich.WithComputed()` - add or modify event properties using an expression
8181

82-
## Formatting
82+
## Formatting with `ExpressionTemplate`
8383

8484
_Serilog.Expressions_ includes the `ExpressionTemplate` class for text formatting. `ExpressionTemplate` implements `ITextFormatter`, so
8585
it works with any text-based Serilog sink:
@@ -89,11 +89,14 @@ it works with any text-based Serilog sink:
8989
9090
Log.Logger = new LoggerConfiguration()
9191
.WriteTo.Console(new ExpressionTemplate(
92-
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
92+
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Cart[0]})\n{@x}"))
9393
.CreateLogger();
94+
95+
// Produces log events like:
96+
// [21:21:40 INF (Sample.Program)] Cart contains ["Tea","Coffee"] (first item is Tea)
9497
```
9598

96-
Note the use of `{Items[0]}`: "holes" in expression templates can include any valid expression.
99+
Note the use of `{Cart[0]}`: "holes" in expression templates can include any valid expression over properties from the event.
97100

98101
Newline-delimited JSON (for example, replicating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
99102
using object literals:
@@ -109,7 +112,7 @@ using object literals:
109112

110113
The following properties are available in expressions:
111114

112-
* **All first-class properties of the event** — no special syntax: `SourceContext` and `Items` are used in the formatting example above
115+
* **All first-class properties of the event** — no special syntax: `SourceContext` and `Cart` are used in the formatting examples above
113116
* `@t` - the event's timestamp, as a `DateTimeOffset`
114117
* `@m` - the rendered message
115118
* `@mt` - the raw message template
@@ -149,7 +152,7 @@ A typical set of operators is supported:
149152
* Accessors `a.b`
150153
* Indexers `a['b']` and `a[0]`
151154
* Wildcard indexing - `a[?]` any, and `a[*]` all
152-
* Conditional `if a then b else c` (all branches required)
155+
* Conditional `if a then b else c` (all branches required; see also the section below on _conditional blocks_)
153156

154157
Comparision operators that act on text all accept an optional postfix `ci` modifier to select case-insensitive comparisons:
155158

@@ -184,7 +187,7 @@ calling a function will be undefined if:
184187
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
185188
| `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. |
186189
| `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`. |
190+
| `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. |
188191
| `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'`. |
189192
| `Undefined()` | Explicitly mark an undefined value. |
190193
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
@@ -195,6 +198,64 @@ Functions that compare text accept an optional postfix `ci` modifier to select c
195198
StartsWith(User.Name, 'n') ci
196199
```
197200

201+
### Template directives
202+
203+
#### Conditional blocks
204+
205+
Within an `ExpressionTemplate`, a portion of the template can be conditionally evaluated using `#if`.
206+
207+
```csharp
208+
Log.Logger = new LoggerConfiguration()
209+
.WriteTo.Console(new ExpressionTemplate(
210+
"[{@t:HH:mm:ss} {@l:u3}{#if SourceContext is not null} ({SourceContext}){#end}] {@m}\n{@x}"))
211+
.CreateLogger();
212+
213+
// Produces log events like:
214+
// [21:21:45 INF] Starting up
215+
// [21:21:46 INF (Sample.Program)] Firing engines
216+
```
217+
218+
The block between the `{#if <expr>}` and `{#end}` directives will only appear in the output if `<expr>` is `true` - in the example, events with a `SourceContext` include this in parentheses, while those without, don't.
219+
220+
It's important to notice that the directive requires a Boolean `true` before the conditional block will be evaluated. It wouldn't be sufficient in this case to write `{#if SourceContext}`, since no values other than `true` are considered "truthy".
221+
222+
The syntax supports `{#if <expr>}`, chained `{#else if <expr>}`, `{#else}`, and `{#end}`, with arbitrary nesting.
223+
224+
#### Repetition
225+
226+
If a log event includes structured data in arrays or objects, a template block can be repeated for each element or member using `#each`/`in` (newlines, double quotes and construction of the `ExpressionTemplate` omitted for clarity):
227+
228+
```
229+
{@l:w4}: {SourceContext}
230+
{#each s in Scope}=> {s}{#delimit} {#end}
231+
{@m}
232+
{@x}
233+
```
234+
235+
This example uses the optional `#delimit` to add a space between each element, producing output like:
236+
237+
```
238+
info: Sample.Program
239+
=> Main => TextFormattingExample
240+
Hello, world!
241+
```
242+
243+
When using `{#each <name> in <expr>}` over an object, such as the built-in `@p` (properties) object, `<name>` will be bound to the _names_ of the properties of the object.
244+
245+
To get to the _values_ of the properties, use a second binding:
246+
247+
```
248+
{#each k, v in @p}{k} = {v}{#delimit},{#end}
249+
```
250+
251+
This example, if an event has three properties, will produce output like:
252+
253+
```
254+
Account = "nblumhardt", Cart = ["Tea", "Coffee"], Powerup = 42
255+
```
256+
257+
The syntax supports `{#each <name>[, <name>] in <expr>}`, an optional `{#delimit}` block, and finally an optional `{#else}` block, which will be evaluated if the array or object is empty.
258+
198259
## Recipes
199260

200261
**Trim down `SourceContext` to a type name only:**

example/Sample/Program.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,22 @@ public static void Main()
1212
{
1313
SelfLog.Enable(Console.Error);
1414

15-
TextFormattingExample();
15+
TextFormattingExample1();
1616
JsonFormattingExample();
1717
PipelineComponentExample();
18+
TextFormattingExample2();
1819
}
1920

20-
static void TextFormattingExample()
21+
static void TextFormattingExample1()
2122
{
2223
using var log = new LoggerConfiguration()
2324
.WriteTo.Console(new ExpressionTemplate(
24-
"[{@t:HH:mm:ss} " +
25-
"{@l:u3} " +
26-
"({coalesce(Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), '<no source>')})] " +
27-
"{@m} " +
28-
"(first item is {coalesce(Items[0], '<empty>')})" +
29-
"\n" +
30-
"{@x}"))
25+
"[{@t:HH:mm:ss} {@l:u3}" +
26+
"{#if SourceContext is not null} ({Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}){#end}] " +
27+
"{@m} (first item is {coalesce(Items[0], '<empty>')})\n{@x}"))
3128
.CreateLogger();
3229

33-
log.Information("Running {Example}", nameof(TextFormattingExample));
30+
log.Information("Running {Example}", nameof(TextFormattingExample1));
3431

3532
log.ForContext<Program>()
3633
.Information("Cart contains {@Items}", new[] { "Tea", "Coffee" });
@@ -75,5 +72,26 @@ static void PipelineComponentExample()
7572
log.ForContext<Program>()
7673
.Information("Cart contains {@Items}", new[] { "Apricots" });
7774
}
75+
76+
static void TextFormattingExample2()
77+
{
78+
using var log = new LoggerConfiguration()
79+
.WriteTo.Console(new ExpressionTemplate(
80+
"{@l:w4}: {SourceContext}\n" +
81+
"{#if Scope is not null}" +
82+
" {#each s in Scope}=> {s}{#delimit} {#end}\n" +
83+
"{#end}" +
84+
" {@m}\n" +
85+
"{@x}"))
86+
.CreateLogger();
87+
88+
var program = log.ForContext<Program>();
89+
program.Information("Starting up");
90+
91+
// Emulate data produced by the Serilog.AspNetCore integration
92+
var scoped = program.ForContext("Scope", new[] {"Main", "TextFormattingExample2()"});
93+
94+
scoped.Information("Hello, world!");
95+
}
7896
}
7997
}

serilog-expressions.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<s:Boolean x:Key="/Default/UserDictionary/Words/=Acerola/@EntryIndexedValue">True</s:Boolean>
33
<s:Boolean x:Key="/Default/UserDictionary/Words/=Comparand/@EntryIndexedValue">True</s:Boolean>
44
<s:Boolean x:Key="/Default/UserDictionary/Words/=Enricher/@EntryIndexedValue">True</s:Boolean>
5+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Evaluatable/@EntryIndexedValue">True</s:Boolean>
56
<s:Boolean x:Key="/Default/UserDictionary/Words/=Existentials/@EntryIndexedValue">True</s:Boolean>
67
<s:Boolean x:Key="/Default/UserDictionary/Words/=formattable/@EntryIndexedValue">True</s:Boolean>
78
<s:Boolean x:Key="/Default/UserDictionary/Words/=nblumhardt/@EntryIndexedValue">True</s:Boolean>

src/Serilog.Expressions/Expressions/Ast/AmbientPropertyExpression.cs renamed to src/Serilog.Expressions/Expressions/Ast/AmbientNameExpression.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
namespace Serilog.Expressions.Ast
44
{
5-
class AmbientPropertyExpression : Expression
5+
class AmbientNameExpression : Expression
66
{
77
readonly bool _requiresEscape;
88

9-
public AmbientPropertyExpression(string propertyName, bool isBuiltIn)
9+
public AmbientNameExpression(string Name, bool isBuiltIn)
1010
{
11-
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
11+
PropertyName = Name ?? throw new ArgumentNullException(nameof(Name));
1212
IsBuiltIn = isBuiltIn;
13-
_requiresEscape = !SerilogExpression.IsValidIdentifier(propertyName);
13+
_requiresEscape = !SerilogExpression.IsValidIdentifier(Name);
1414
}
1515

1616
public string PropertyName { get; }
@@ -25,4 +25,4 @@ public override string ToString()
2525
return (IsBuiltIn ? "@" : "") + PropertyName;
2626
}
2727
}
28-
}
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
3+
namespace Serilog.Expressions.Ast
4+
{
5+
class LocalNameExpression : Expression
6+
{
7+
public LocalNameExpression(string name)
8+
{
9+
Name = name ?? throw new ArgumentNullException(nameof(name));
10+
}
11+
12+
public string Name { get; }
13+
14+
public override string ToString()
15+
{
16+
// No unambiguous syntax for this right now, `$` will do to make these stand out when debugging,
17+
// but the result won't round-trip parse.
18+
return $"${Name}";
19+
}
20+
}
21+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static Expression Translate(Expression expression)
2222
return actual;
2323
}
2424

25-
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
25+
public static Evaluatable Compile(Expression expression, NameResolver nameResolver)
2626
{
2727
var actual = Translate(expression);
2828
return LinqExpressionCompiler.Compile(actual, nameResolver);

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Text.RegularExpressions;
77
using Serilog.Events;
8+
using Serilog.Expressions.Runtime;
89
using Serilog.Formatting.Display;
910
using Serilog.Parsing;
1011

@@ -131,9 +132,17 @@ public static bool CoerceToScalarBoolean(LogEventPropertyValue value)
131132
return null;
132133
}
133134

134-
public static LogEventPropertyValue? GetPropertyValue(LogEvent context, string propertyName)
135+
public static LogEventPropertyValue? GetPropertyValue(EvaluationContext ctx, string propertyName)
135136
{
136-
if (!context.Properties.TryGetValue(propertyName, out var value))
137+
if (!ctx.LogEvent.Properties.TryGetValue(propertyName, out var value))
138+
return null;
139+
140+
return value;
141+
}
142+
143+
public static LogEventPropertyValue? GetLocalValue(EvaluationContext ctx, string localName)
144+
{
145+
if (!Locals.TryGetValue(ctx.Locals, localName, out var value))
137146
return null;
138147

139148
return value;

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,22 @@ class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
6868
static readonly MethodInfo TryGetStructurePropertyValueMethod = typeof(Intrinsics)
6969
.GetMethod(nameof(Intrinsics.TryGetStructurePropertyValue), BindingFlags.Static | BindingFlags.Public)!;
7070

71-
ParameterExpression Context { get; } = LX.Variable(typeof(LogEvent), "evt");
71+
ParameterExpression Context { get; } = LX.Variable(typeof(EvaluationContext), "ctx");
7272

7373
LinqExpressionCompiler(NameResolver nameResolver)
7474
{
7575
_nameResolver = nameResolver;
7676
}
7777

78-
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
78+
public static Evaluatable Compile(Expression expression, NameResolver nameResolver)
7979
{
8080
if (expression == null) throw new ArgumentNullException(nameof(expression));
8181
var compiler = new LinqExpressionCompiler(nameResolver);
8282
var body = compiler.Transform(expression);
83-
return LX.Lambda<CompiledExpression>(body, compiler.Context).Compile();
83+
return LX.Lambda<Evaluatable>(body, compiler.Context).Compile();
8484
}
8585

86-
ExpressionBody Splice(Expression<CompiledExpression> lambda)
86+
ExpressionBody Splice(Expression<Evaluatable> lambda)
8787
{
8888
return ParameterReplacementVisitor.ReplaceParameters(lambda, Context);
8989
}
@@ -134,32 +134,40 @@ protected override ExpressionBody Transform(ConstantExpression cx)
134134
return LX.Constant(cx.Constant);
135135
}
136136

137-
protected override ExpressionBody Transform(AmbientPropertyExpression px)
137+
protected override ExpressionBody Transform(AmbientNameExpression px)
138138
{
139139
if (px.IsBuiltIn)
140140
{
141141
return px.PropertyName switch
142142
{
143-
BuiltInProperty.Level => Splice(context => new ScalarValue(context.Level)),
144-
BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context))),
143+
BuiltInProperty.Level => Splice(context => new ScalarValue(context.LogEvent.Level)),
144+
BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context.LogEvent))),
145145
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)),
146+
context.LogEvent.Exception == null ? null : new ScalarValue(context.LogEvent.Exception)),
147+
BuiltInProperty.Timestamp => Splice(context => new ScalarValue(context.LogEvent.Timestamp)),
148+
BuiltInProperty.MessageTemplate => Splice(context => new ScalarValue(context.LogEvent.MessageTemplate.Text)),
149149
BuiltInProperty.Properties => Splice(context =>
150-
new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
150+
new StructureValue(context.LogEvent.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
151151
null)),
152-
BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context)),
152+
BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context.LogEvent)),
153153
BuiltInProperty.EventId => Splice(context =>
154-
new ScalarValue(EventIdHash.Compute(context.MessageTemplate.Text))),
154+
new ScalarValue(EventIdHash.Compute(context.LogEvent.MessageTemplate.Text))),
155155
_ => LX.Constant(null, typeof(LogEventPropertyValue))
156156
};
157157
}
158158

159+
// Don't close over the AST node.
159160
var propertyName = px.PropertyName;
160161
return Splice(context => Intrinsics.GetPropertyValue(context, propertyName));
161162
}
162163

164+
protected override ExpressionBody Transform(LocalNameExpression nlx)
165+
{
166+
// Don't close over the AST node.
167+
var name = nlx.Name;
168+
return Splice(context => Intrinsics.GetLocalValue(context, name));
169+
}
170+
163171
protected override ExpressionBody Transform(Ast.LambdaExpression lmx)
164172
{
165173
var parameters = lmx.Parameters.Select(px => Tuple.Create(px, LX.Parameter(typeof(LogEventPropertyValue), px.ParameterName))).ToList();

src/Serilog.Expressions/Expressions/Compilation/Pattern.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ static class Pattern
88
{
99
public static bool IsAmbientProperty(Expression expression, string name, bool isBuiltIn)
1010
{
11-
return expression is AmbientPropertyExpression px &&
11+
return expression is AmbientNameExpression px &&
1212
px.PropertyName == name &&
1313
px.IsBuiltIn == isBuiltIn;
1414
}

src/Serilog.Expressions/Expressions/Compilation/Properties/PropertiesObjectAccessorTransformer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ protected override Expression Transform(AccessorExpression ax)
1515
if (!Pattern.IsAmbientProperty(ax.Receiver, BuiltInProperty.Properties, true))
1616
return base.Transform(ax);
1717

18-
return new AmbientPropertyExpression(ax.MemberName, false);
18+
return new AmbientNameExpression(ax.MemberName, false);
1919
}
2020

2121
protected override Expression Transform(IndexerExpression ix)
@@ -24,7 +24,7 @@ protected override Expression Transform(IndexerExpression ix)
2424
!Pattern.IsStringConstant(ix.Index, out var name))
2525
return base.Transform(ix);
2626

27-
return new AmbientPropertyExpression(name, false);
27+
return new AmbientNameExpression(name, false);
2828
}
2929
}
3030
}

0 commit comments

Comments
 (0)