Skip to content

1.1.0 Release #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if(Test-Path .\artifacts) {

$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL];
$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]
$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"]
$commitHash = $(git rev-parse --short HEAD)
$buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]

Expand Down Expand Up @@ -57,4 +57,4 @@ foreach ($test in ls test/*.PerformanceTests) {
Pop-Location
}

Pop-Location
Pop-Location
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ events, ideal for use with JSON or XML configuration.
Install the package from NuGet:

```shell
dotnet add package Serilog.Expressions -v 1.0.0-*
dotnet add package Serilog.Expressions
```

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

```csharp
// using Serilog.Templates;

Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
Expand All @@ -93,27 +95,31 @@ 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

### Built-in properties
### Properties

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 `Items` are used in the formatting example above
* `@t` - the event's timestamp, as a `DateTimeOffset`
* `@m` - the rendered message
* `@mt` - the raw message template
* `@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 All @@ -140,7 +146,8 @@ A typical set of operators is supported:
* Existence `is null` and `is not null`
* SQL-style `like` and `not like`, with `%` and `_` wildcards (double wildcards to escape them)
* Array membership with `in` and `not in`
* Indexers `a[b]` and accessors `a.b`
* 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)

Expand Down Expand Up @@ -172,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
2 changes: 2 additions & 0 deletions src/Serilog.Expressions/Expressions/Ast/Expression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
{
abstract class Expression
{
// Used only as an enabler for testing and debugging.
public abstract override string ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ public IndexOfMatchExpression(Expression corpus, Regex regex)
Corpus = corpus ?? throw new ArgumentNullException(nameof(corpus));
Regex = regex ?? throw new ArgumentNullException(nameof(regex));
}

public override string ToString()
{
return $"_Internal_IndexOfMatch({Corpus}, '{Regex.ToString().Replace("'", "''")}')";
}
}
}
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
Expand Up @@ -10,7 +10,7 @@ namespace Serilog.Expressions.Compilation
{
static class ExpressionCompiler
{
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
public static Expression Translate(Expression expression)
{
var actual = expression;
actual = VariadicCallRewriter.Rewrite(actual);
Expand All @@ -19,7 +19,12 @@ public static CompiledExpression Compile(Expression expression, NameResolver nam
actual = PropertiesObjectAccessorTransformer.Rewrite(actual);
actual = ConstantArrayEvaluator.Evaluate(actual);
actual = WildcardComprehensionTransformer.Expand(actual);

return actual;
}

public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
{
var actual = Translate(expression);
return LinqExpressionCompiler.Compile(actual, nameResolver);
}
}
Expand Down
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
Expand Up @@ -54,7 +54,10 @@ cx.Constant is ScalarValue scalar &&

static string LikeToRegex(string like)
{
var begin = "^";
var regex = "";
var end = "$";

for (var i = 0; i < like.Length; ++i)
{
var ch = like[i];
Expand All @@ -68,7 +71,17 @@ static string LikeToRegex(string like)
}
else
{
regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
if (i == 0)
begin = "";

if (i == like.Length - 1)
end = "";

if (i == 0 && i == like.Length - 1)
regex += ".*";

if (i != 0 && i != like.Length - 1)
regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline
}
}
else if (ch == '_')
Expand All @@ -87,7 +100,7 @@ static string LikeToRegex(string like)
regex += Regex.Escape(ch.ToString());
}

return regex;
return begin + regex + end;
}
}
}
Loading