Skip to content

Commit

Permalink
Merge pull request #32 from serilog/dev
Browse files Browse the repository at this point in the history
2.0.0 Release
  • Loading branch information
nblumhardt authored Mar 27, 2021
2 parents 0d76d22 + 3ce8fc6 commit e63a0d5
Show file tree
Hide file tree
Showing 52 changed files with 1,199 additions and 304 deletions.
75 changes: 68 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# _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)
# _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)

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

## Formatting
## Formatting with `ExpressionTemplate`

_Serilog.Expressions_ includes the `ExpressionTemplate` class for text formatting. `ExpressionTemplate` implements `ITextFormatter`, so
it works with any text-based Serilog sink:
Expand All @@ -89,11 +89,14 @@ it works with any text-based Serilog sink:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Cart[0]})\n{@x}"))
.CreateLogger();

// Produces log events like:
// [21:21:40 INF (Sample.Program)] Cart contains ["Tea","Coffee"] (first item is Tea)
```

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

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

The following properties are available in expressions:

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

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

Expand Down Expand Up @@ -184,7 +187,7 @@ calling a function will be undefined if:
| `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`. |
| `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. |
| `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`. |
Expand All @@ -195,6 +198,64 @@ Functions that compare text accept an optional postfix `ci` modifier to select c
StartsWith(User.Name, 'n') ci
```

### Template directives

#### Conditional blocks

Within an `ExpressionTemplate`, a portion of the template can be conditionally evaluated using `#if`.

```csharp
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"[{@t:HH:mm:ss} {@l:u3}{#if SourceContext is not null} ({SourceContext}){#end}] {@m}\n{@x}"))
.CreateLogger();

// Produces log events like:
// [21:21:45 INF] Starting up
// [21:21:46 INF (Sample.Program)] Firing engines
```

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.

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".

The syntax supports `{#if <expr>}`, chained `{#else if <expr>}`, `{#else}`, and `{#end}`, with arbitrary nesting.

#### Repetition

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):

```
{@l:w4}: {SourceContext}
{#each s in Scope}=> {s}{#delimit} {#end}
{@m}
{@x}
```

This example uses the optional `#delimit` to add a space between each element, producing output like:

```
info: Sample.Program
=> Main => TextFormattingExample
Hello, world!
```

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.

To get to the _values_ of the properties, use a second binding:

```
{#each k, v in @p}{k} = {v}{#delimit},{#end}
```

This example, if an event has three properties, will produce output like:

```
Account = "nblumhardt", Cart = ["Tea", "Coffee"], Powerup = 42
```

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.

## Recipes

**Trim down `SourceContext` to a type name only:**
Expand Down
38 changes: 28 additions & 10 deletions example/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,22 @@ public static void Main()
{
SelfLog.Enable(Console.Error);

TextFormattingExample();
TextFormattingExample1();
JsonFormattingExample();
PipelineComponentExample();
TextFormattingExample2();
}

static void TextFormattingExample()
static void TextFormattingExample1()
{
using var log = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"[{@t:HH:mm:ss} " +
"{@l:u3} " +
"({coalesce(Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), '<no source>')})] " +
"{@m} " +
"(first item is {coalesce(Items[0], '<empty>')})" +
"\n" +
"{@x}"))
"[{@t:HH:mm:ss} {@l:u3}" +
"{#if SourceContext is not null} ({Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}){#end}] " +
"{@m} (first item is {coalesce(Items[0], '<empty>')})\n{@x}"))
.CreateLogger();

log.Information("Running {Example}", nameof(TextFormattingExample));
log.Information("Running {Example}", nameof(TextFormattingExample1));

log.ForContext<Program>()
.Information("Cart contains {@Items}", new[] { "Tea", "Coffee" });
Expand Down Expand Up @@ -75,5 +72,26 @@ static void PipelineComponentExample()
log.ForContext<Program>()
.Information("Cart contains {@Items}", new[] { "Apricots" });
}

static void TextFormattingExample2()
{
using var log = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"{@l:w4}: {SourceContext}\n" +
"{#if Scope is not null}" +
" {#each s in Scope}=> {s}{#delimit} {#end}\n" +
"{#end}" +
" {@m}\n" +
"{@x}"))
.CreateLogger();

var program = log.ForContext<Program>();
program.Information("Starting up");

// Emulate data produced by the Serilog.AspNetCore integration
var scoped = program.ForContext("Scope", new[] {"Main", "TextFormattingExample2()"});

scoped.Information("Hello, world!");
}
}
}
1 change: 1 addition & 0 deletions serilog-expressions.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Acerola/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Comparand/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Enricher/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Evaluatable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Existentials/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=formattable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=nblumhardt/@EntryIndexedValue">True</s:Boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

namespace Serilog.Expressions.Ast
{
class AmbientPropertyExpression : Expression
class AmbientNameExpression : Expression
{
readonly bool _requiresEscape;

public AmbientPropertyExpression(string propertyName, bool isBuiltIn)
public AmbientNameExpression(string Name, bool isBuiltIn)
{
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
PropertyName = Name ?? throw new ArgumentNullException(nameof(Name));
IsBuiltIn = isBuiltIn;
_requiresEscape = !SerilogExpression.IsValidIdentifier(propertyName);
_requiresEscape = !SerilogExpression.IsValidIdentifier(Name);
}

public string PropertyName { get; }
Expand All @@ -25,4 +25,4 @@ public override string ToString()
return (IsBuiltIn ? "@" : "") + PropertyName;
}
}
}
}
21 changes: 21 additions & 0 deletions src/Serilog.Expressions/Expressions/Ast/LocalNameExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace Serilog.Expressions.Ast
{
class LocalNameExpression : Expression
{
public LocalNameExpression(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}

public string Name { get; }

public override string ToString()
{
// No unambiguous syntax for this right now, `$` will do to make these stand out when debugging,
// but the result won't round-trip parse.
return $"${Name}";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static Expression Translate(Expression expression)
return actual;
}

public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
public static Evaluatable Compile(Expression expression, NameResolver nameResolver)
{
var actual = Translate(expression);
return LinqExpressionCompiler.Compile(actual, nameResolver);
Expand Down
13 changes: 11 additions & 2 deletions src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using Serilog.Events;
using Serilog.Expressions.Runtime;
using Serilog.Formatting.Display;
using Serilog.Parsing;

Expand Down Expand Up @@ -131,9 +132,17 @@ public static bool CoerceToScalarBoolean(LogEventPropertyValue value)
return null;
}

public static LogEventPropertyValue? GetPropertyValue(LogEvent context, string propertyName)
public static LogEventPropertyValue? GetPropertyValue(EvaluationContext ctx, string propertyName)
{
if (!context.Properties.TryGetValue(propertyName, out var value))
if (!ctx.LogEvent.Properties.TryGetValue(propertyName, out var value))
return null;

return value;
}

public static LogEventPropertyValue? GetLocalValue(EvaluationContext ctx, string localName)
{
if (!Locals.TryGetValue(ctx.Locals, localName, out var value))
return null;

return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,22 @@ class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
static readonly MethodInfo TryGetStructurePropertyValueMethod = typeof(Intrinsics)
.GetMethod(nameof(Intrinsics.TryGetStructurePropertyValue), BindingFlags.Static | BindingFlags.Public)!;

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

LinqExpressionCompiler(NameResolver nameResolver)
{
_nameResolver = nameResolver;
}

public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
public static Evaluatable Compile(Expression expression, NameResolver nameResolver)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
var compiler = new LinqExpressionCompiler(nameResolver);
var body = compiler.Transform(expression);
return LX.Lambda<CompiledExpression>(body, compiler.Context).Compile();
return LX.Lambda<Evaluatable>(body, compiler.Context).Compile();
}

ExpressionBody Splice(Expression<CompiledExpression> lambda)
ExpressionBody Splice(Expression<Evaluatable> lambda)
{
return ParameterReplacementVisitor.ReplaceParameters(lambda, Context);
}
Expand Down Expand Up @@ -134,32 +134,40 @@ protected override ExpressionBody Transform(ConstantExpression cx)
return LX.Constant(cx.Constant);
}

protected override ExpressionBody Transform(AmbientPropertyExpression px)
protected override ExpressionBody Transform(AmbientNameExpression px)
{
if (px.IsBuiltIn)
{
return px.PropertyName switch
{
BuiltInProperty.Level => Splice(context => new ScalarValue(context.Level)),
BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context))),
BuiltInProperty.Level => Splice(context => new ScalarValue(context.LogEvent.Level)),
BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context.LogEvent))),
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)),
context.LogEvent.Exception == null ? null : new ScalarValue(context.LogEvent.Exception)),
BuiltInProperty.Timestamp => Splice(context => new ScalarValue(context.LogEvent.Timestamp)),
BuiltInProperty.MessageTemplate => Splice(context => new ScalarValue(context.LogEvent.MessageTemplate.Text)),
BuiltInProperty.Properties => Splice(context =>
new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
new StructureValue(context.LogEvent.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
null)),
BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context)),
BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context.LogEvent)),
BuiltInProperty.EventId => Splice(context =>
new ScalarValue(EventIdHash.Compute(context.MessageTemplate.Text))),
new ScalarValue(EventIdHash.Compute(context.LogEvent.MessageTemplate.Text))),
_ => LX.Constant(null, typeof(LogEventPropertyValue))
};
}

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

protected override ExpressionBody Transform(LocalNameExpression nlx)
{
// Don't close over the AST node.
var name = nlx.Name;
return Splice(context => Intrinsics.GetLocalValue(context, name));
}

protected override ExpressionBody Transform(Ast.LambdaExpression lmx)
{
var parameters = lmx.Parameters.Select(px => Tuple.Create(px, LX.Parameter(typeof(LogEventPropertyValue), px.ParameterName))).ToList();
Expand Down
2 changes: 1 addition & 1 deletion src/Serilog.Expressions/Expressions/Compilation/Pattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ static class Pattern
{
public static bool IsAmbientProperty(Expression expression, string name, bool isBuiltIn)
{
return expression is AmbientPropertyExpression px &&
return expression is AmbientNameExpression px &&
px.PropertyName == name &&
px.IsBuiltIn == isBuiltIn;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ protected override Expression Transform(AccessorExpression ax)
if (!Pattern.IsAmbientProperty(ax.Receiver, BuiltInProperty.Properties, true))
return base.Transform(ax);

return new AmbientPropertyExpression(ax.MemberName, false);
return new AmbientNameExpression(ax.MemberName, false);
}

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

return new AmbientPropertyExpression(name, false);
return new AmbientNameExpression(name, false);
}
}
}
Loading

0 comments on commit e63a0d5

Please sign in to comment.