Skip to content

Commit 545342a

Browse files
committed
Basic format support
1 parent 0b91241 commit 545342a

File tree

6 files changed

+234
-6
lines changed

6 files changed

+234
-6
lines changed

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,60 @@
1-
# serilog-expressions
1+
# _Serilog.Expressions_
2+
3+
A mini-language for filtering, enriching, and formatting Serilog
4+
events, ideal for embedding in JSON or XML configuration.
5+
6+
## Getting started
7+
8+
Install the package from NuGet:
9+
10+
```shell
11+
dotnet add package Serilog.Expressions
12+
```
13+
14+
The package adds extension methods to Serilog's `Filter` and
15+
`Enrich` configuration objects, along with an `OutputTemplate`
16+
type that's compatible with Serilog sinks accepting an
17+
`ITextFormatter`.
18+
19+
### Filtering
20+
21+
_Serilog.Expressions_ adds `ByExcluding()` and `ByIncludingOnly()`
22+
overloads to the `Filter` configuration object that accept filter
23+
expressions:
24+
25+
```csharp
26+
Log.Logger = new LoggerConfiguration()
27+
.Filter.ByExcluding("RequestPath like '/health%'")
28+
.CreateLogger();
29+
```
30+
31+
Events with a `RequestPath` property that matches the expression
32+
will be excluded by the filter.
33+
34+
In [`appSettings.json`
35+
configuration](https://github.com/serilog/serilog-settings-configuration)
36+
this is written as:
37+
38+
```json
39+
{
40+
"Serilog": {
41+
"Using": ["Serilog.Expressions"],
42+
"Filter": [
43+
{
44+
"Name": "ByExcluding",
45+
"Args": {
46+
"expression": "RequestPath like '/health%'"
47+
}
48+
}
49+
]
50+
}
51+
}
52+
```
53+
54+
### Enriching
55+
56+
### Formatting
57+
58+
## Language reference
59+
60+

example/Sample/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ public static void Main()
1717
using var log = new LoggerConfiguration()
1818
.Enrich.WithProperty("AppId", 10)
1919
.Enrich.WithComputed("FirstItem", "Items[0]")
20-
.Enrich.WithComputed("SourceContext", "coalesce(substring(SourceContext, lastindexof(SourceContext, '.') + 1), SourceContext, '<no source>')")
20+
.Enrich.WithComputed("SourceContext", "coalesce(Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), SourceContext, '<no source>')")
2121
.Filter.ByIncludingOnly(expr)
2222
.WriteTo.Console(outputTemplate:
2323
"[{Timestamp:HH:mm:ss} {Level:u3} ({SourceContext})] {Message:lj} (first item is {FirstItem}){NewLine}{Exception}")
2424
.WriteTo.Console(new OutputTemplate(
25-
"[{@t} {@l} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
25+
"[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Items[0]})\n{@x}"))
2626
.CreateLogger();
2727

2828
log.ForContext<Program>().Information("Cart contains {@Items}", new[] { "Tea", "Coffee" });

src/Serilog.Expressions/Templates/Compilation/CompiledFormattedExpression.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Serilog.Events;
44
using Serilog.Expressions;
55
using Serilog.Formatting.Json;
6+
using Serilog.Templates.Rendering;
67

78
namespace Serilog.Templates.Compilation
89
{
@@ -30,7 +31,10 @@ public override void Evaluate(LogEvent logEvent, TextWriter output, IFormatProvi
3031
if (scalar.Value is null)
3132
return; // Null is empty
3233

33-
if (scalar.Value is IFormattable fmt)
34+
if (scalar.Value is LogEventLevel level)
35+
// This would be better implemented using CompiledLevelToken : CompiledTemplate.
36+
output.Write(LevelRenderer.GetLevelMoniker(level, _format));
37+
else if (scalar.Value is IFormattable fmt)
3438
output.Write(fmt.ToString(_format, formatProvider));
3539
else
3640
output.Write(scalar.Value.ToString());

src/Serilog.Expressions/Templates/Parsing/TemplateParser.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,44 @@ public static Template Parse(string template)
3636
}
3737
else
3838
{
39+
// A lot of TODOs, here; exceptions thrown instead of error returns; positions of errors not
40+
// reported
41+
3942
// Not reporting line/column
4043
var tokens = Tokenizer.GreedyTokenize(new TextSpan(template, new Position(i, 0, 0), template.Length - i));
4144
// Dropping error info; may return a zero-length parse
4245
var expr = ExpressionTokenParsers.TryPartialParse(tokens);
43-
// Throw on error; no format parsing
44-
elements.Add(new FormattedExpression(expr.Value, null));
46+
if (!expr.HasValue)
47+
throw new ArgumentException($"Invalid expression, {expr.FormatErrorMessageFragment()}.");
48+
4549
if (expr.Remainder.Position == tokens.Count())
4650
i = tokens.Last().Position.Absolute + tokens.Last().Span.Length;
4751
else
4852
i = tokens.ElementAt(i).Position.Absolute;
4953

54+
if (i == template.Length)
55+
throw new ArgumentException("Un-closed hole, `}` expected.");
56+
57+
string format = null;
58+
if (template[i] == ':')
59+
{
60+
i++;
61+
62+
var formatBuilder = new StringBuilder();
63+
while (i < template.Length && template[i] != '}')
64+
{
65+
formatBuilder.Append(template[i]);
66+
i++;
67+
}
68+
69+
format = formatBuilder.ToString();
70+
}
71+
5072
if (i == template.Length || template[i] != '}')
5173
throw new ArgumentException("Un-closed hole, `}` expected.");
5274
i++;
75+
76+
elements.Add(new FormattedExpression(expr.Value, format));
5377
}
5478
}
5579
else if (ch == '}')
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
namespace Serilog.Templates.Rendering
16+
{
17+
static class Casing
18+
{
19+
/// <summary>
20+
/// Apply upper or lower casing to <paramref name="value"/> when <paramref name="format"/> is provided.
21+
/// Returns <paramref name="value"/> when no or invalid format provided.
22+
/// </summary>
23+
/// <param name="value">Provided string for formatting.</param>
24+
/// <param name="format">Format string.</param>
25+
/// <returns>The provided <paramref name="value"/> with formatting applied.</returns>
26+
public static string Format(string value, string format = null)
27+
{
28+
switch (format)
29+
{
30+
case "u":
31+
return value.ToUpperInvariant();
32+
case "w":
33+
return value.ToLowerInvariant();
34+
default:
35+
return value;
36+
}
37+
}
38+
}
39+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2017 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 Serilog.Events;
16+
17+
namespace Serilog.Templates.Rendering
18+
{
19+
/// <summary>
20+
/// Implements the {Level} element.
21+
/// can now have a fixed width applied to it, as well as casing rules.
22+
/// Width is set through formats like "u3" (uppercase three chars),
23+
/// "w1" (one lowercase char), or "t4" (title case four chars).
24+
/// </summary>
25+
static class LevelRenderer
26+
{
27+
static readonly string[][] TitleCaseLevelMap =
28+
{
29+
new[] { "V", "Vb", "Vrb", "Verb" },
30+
new[] { "D", "De", "Dbg", "Dbug" },
31+
new[] { "I", "In", "Inf", "Info" },
32+
new[] { "W", "Wn", "Wrn", "Warn" },
33+
new[] { "E", "Er", "Err", "Eror" },
34+
new[] { "F", "Fa", "Ftl", "Fatl" },
35+
};
36+
37+
static readonly string[][] LowercaseLevelMap =
38+
{
39+
new[] { "v", "vb", "vrb", "verb" },
40+
new[] { "d", "de", "dbg", "dbug" },
41+
new[] { "i", "in", "inf", "info" },
42+
new[] { "w", "wn", "wrn", "warn" },
43+
new[] { "e", "er", "err", "eror" },
44+
new[] { "f", "fa", "ftl", "fatl" },
45+
};
46+
47+
static readonly string[][] UppercaseLevelMap =
48+
{
49+
new[] { "V", "VB", "VRB", "VERB" },
50+
new[] { "D", "DE", "DBG", "DBUG" },
51+
new[] { "I", "IN", "INF", "INFO" },
52+
new[] { "W", "WN", "WRN", "WARN" },
53+
new[] { "E", "ER", "ERR", "EROR" },
54+
new[] { "F", "FA", "FTL", "FATL" },
55+
};
56+
57+
public static string GetLevelMoniker(LogEventLevel value, string format)
58+
{
59+
if (format == null)
60+
return value.ToString();
61+
62+
if (format.Length != 2 && format.Length != 3)
63+
return Casing.Format(value.ToString(), format);
64+
65+
// Using int.Parse() here requires allocating a string to exclude the first character prefix.
66+
// Junk like "wxy" will be accepted but produce benign results.
67+
var width = format[1] - '0';
68+
if (format.Length == 3)
69+
{
70+
width *= 10;
71+
width += format[2] - '0';
72+
}
73+
74+
if (width < 1)
75+
return string.Empty;
76+
77+
if (width > 4)
78+
{
79+
var stringValue = value.ToString();
80+
if (stringValue.Length > width)
81+
stringValue = stringValue.Substring(0, width);
82+
return Casing.Format(stringValue);
83+
}
84+
85+
var index = (int)value;
86+
if (index >= 0 && index <= (int)LogEventLevel.Fatal)
87+
{
88+
switch (format[0])
89+
{
90+
case 'w':
91+
return LowercaseLevelMap[index][width - 1];
92+
case 'u':
93+
return UppercaseLevelMap[index][width - 1];
94+
case 't':
95+
return TitleCaseLevelMap[index][width - 1];
96+
}
97+
}
98+
99+
return Casing.Format(value.ToString(), format);
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)