Skip to content

Commit dc89a23

Browse files
committed
Output encoding feature
1 parent 1460b49 commit dc89a23

15 files changed

+302
-19
lines changed

src/Serilog.Expressions/Serilog.Expressions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@
2525
<None Include="..\..\assets\icon.png" Pack="true" Visible="false" PackagePath="" />
2626
</ItemGroup>
2727

28+
2829
</Project>

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Serilog.Expressions.Ast;
1717
using Serilog.Expressions.Compilation;
1818
using Serilog.Templates.Ast;
19+
using Serilog.Templates.Encoding;
1920
using Serilog.Templates.Themes;
2021

2122
namespace Serilog.Templates.Compilation;
@@ -24,39 +25,40 @@ static class TemplateCompiler
2425
{
2526
public static CompiledTemplate Compile(Template template,
2627
IFormatProvider? formatProvider, NameResolver nameResolver,
27-
TemplateTheme theme)
28+
TemplateTheme theme,
29+
EncodedTemplateFactory encoder)
2830
{
2931
return template switch
3032
{
3133
LiteralText text => new CompiledLiteralText(text.Text, theme),
32-
FormattedExpression { Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Level} } level => new CompiledLevelToken(
33-
level.Format, level.Alignment, theme),
34+
FormattedExpression { Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Level} } level =>
35+
encoder.Wrap(new CompiledLevelToken(level.Format, level.Alignment, theme)),
3436
FormattedExpression
3537
{
3638
Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Exception },
3739
Alignment: null,
3840
Format: null
39-
} => new CompiledExceptionToken(theme),
41+
} => encoder.Wrap(new CompiledExceptionToken(theme)),
4042
FormattedExpression
4143
{
4244
Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Message },
4345
Format: null
44-
} message => new CompiledMessageToken(formatProvider, message.Alignment, theme),
45-
FormattedExpression expression => new CompiledFormattedExpression(
46+
} message => encoder.Wrap(new CompiledMessageToken(formatProvider, message.Alignment, theme)),
47+
FormattedExpression expression => encoder.MakeCompiledFormattedExpression(
4648
ExpressionCompiler.Compile(expression.Expression, formatProvider, nameResolver), expression.Format, expression.Alignment, formatProvider, theme),
47-
TemplateBlock block => new CompiledTemplateBlock(block.Elements.Select(e => Compile(e, formatProvider, nameResolver, theme)).ToArray()),
49+
TemplateBlock block => new CompiledTemplateBlock(block.Elements.Select(e => Compile(e, formatProvider, nameResolver, theme, encoder)).ToArray()),
4850
Conditional conditional => new CompiledConditional(
4951
ExpressionCompiler.Compile(conditional.Condition, formatProvider, nameResolver),
50-
Compile(conditional.Consequent, formatProvider, nameResolver, theme),
51-
conditional.Alternative == null ? null : Compile(conditional.Alternative, formatProvider, nameResolver, theme)),
52+
Compile(conditional.Consequent, formatProvider, nameResolver, theme, encoder),
53+
conditional.Alternative == null ? null : Compile(conditional.Alternative, formatProvider, nameResolver, theme, encoder)),
5254
Repetition repetition => new CompiledRepetition(
5355
ExpressionCompiler.Compile(repetition.Enumerable, formatProvider, nameResolver),
5456
repetition.BindingNames.Length > 0 ? repetition.BindingNames[0] : null,
5557
repetition.BindingNames.Length > 1 ? repetition.BindingNames[1] : null,
56-
Compile(repetition.Body, formatProvider, nameResolver, theme),
57-
repetition.Delimiter == null ? null : Compile(repetition.Delimiter, formatProvider, nameResolver, theme),
58-
repetition.Alternative == null ? null : Compile(repetition.Alternative, formatProvider, nameResolver, theme)),
58+
Compile(repetition.Body, formatProvider, nameResolver, theme, encoder),
59+
repetition.Delimiter == null ? null : Compile(repetition.Delimiter, formatProvider, nameResolver, theme, encoder),
60+
repetition.Alternative == null ? null : Compile(repetition.Alternative, formatProvider, nameResolver, theme, encoder)),
5961
_ => throw new NotSupportedException()
6062
};
6163
}
62-
}
64+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Serilog.Expressions.Runtime;
1818
using Serilog.Templates.Ast;
1919
using Serilog.Templates.Compilation.UnreferencedProperties;
20+
using Serilog.Templates.Compilation.Unsafe;
2021

2122
namespace Serilog.Templates.Compilation;
2223

@@ -27,7 +28,8 @@ public static NameResolver Build(NameResolver? additionalNameResolver, Template
2728
var resolvers = new List<NameResolver>
2829
{
2930
new StaticMemberNameResolver(typeof(RuntimeOperators)),
30-
new UnreferencedPropertiesFunction(template)
31+
new UnreferencedPropertiesFunction(template),
32+
new UnsafeOutputFunction()
3133
};
3234

3335
if (additionalNameResolver != null)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.Diagnostics.CodeAnalysis;
16+
using System.Reflection;
17+
using Serilog.Events;
18+
using Serilog.Expressions;
19+
using Serilog.Templates.Encoding;
20+
21+
namespace Serilog.Templates.Compilation.Unsafe;
22+
23+
/// <summary>
24+
/// Marks an expression in a template as bypassing the output encoding mechanism.
25+
/// </summary>
26+
class UnsafeOutputFunction : NameResolver
27+
{
28+
const string FunctionName = "unsafe";
29+
30+
public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation)
31+
{
32+
if (name.Equals(FunctionName, StringComparison.OrdinalIgnoreCase))
33+
{
34+
implementation = typeof(UnsafeOutputFunction).GetMethod(nameof(Implementation),
35+
BindingFlags.Static | BindingFlags.Public)!;
36+
return true;
37+
}
38+
39+
implementation = null;
40+
return false;
41+
}
42+
43+
// By convention, built-in functions accept and return nullable values.
44+
// ReSharper disable once ReturnTypeCanBeNotNullable
45+
public static LogEventPropertyValue? Implementation(LogEventPropertyValue? inner)
46+
{
47+
return new ScalarValue(new PreEncodedValue(inner));
48+
}
49+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Serilog.Expressions;
2+
using Serilog.Templates.Compilation;
3+
4+
namespace Serilog.Templates.Encoding
5+
{
6+
class EncodedCompiledTemplate : CompiledTemplate
7+
{
8+
readonly CompiledTemplate _inner;
9+
readonly TemplateOutputEncoder _encoder;
10+
11+
public EncodedCompiledTemplate(CompiledTemplate inner, TemplateOutputEncoder encoder)
12+
{
13+
_inner = inner;
14+
_encoder = encoder;
15+
}
16+
17+
public override void Evaluate(EvaluationContext ctx, TextWriter output)
18+
{
19+
var buffer = new StringWriter(output.FormatProvider);
20+
_inner.Evaluate(ctx, buffer);
21+
var encoded = _encoder.Encode(buffer.ToString());
22+
output.Write(encoded);
23+
}
24+
}
25+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Serilog.Expressions;
2+
using Serilog.Parsing;
3+
using Serilog.Templates.Compilation;
4+
using Serilog.Templates.Themes;
5+
6+
namespace Serilog.Templates.Encoding
7+
{
8+
class EncodedTemplateFactory
9+
{
10+
readonly TemplateOutputEncoder? _encoder;
11+
12+
public EncodedTemplateFactory(TemplateOutputEncoder? encoder)
13+
{
14+
_encoder = encoder;
15+
}
16+
17+
public CompiledTemplate Wrap(CompiledTemplate inner)
18+
{
19+
if (_encoder == null)
20+
return inner;
21+
22+
return new EncodedCompiledTemplate(inner, _encoder);
23+
}
24+
25+
public CompiledTemplate MakeCompiledFormattedExpression(Evaluatable expression, string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme)
26+
{
27+
if (_encoder == null)
28+
return new CompiledFormattedExpression(expression, format, alignment, formatProvider, theme);
29+
30+
return new EscapableEncodedCompiledFormattedExpression(expression, format, alignment, formatProvider, theme, _encoder);
31+
}
32+
}
33+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Serilog.Events;
2+
using Serilog.Expressions;
3+
using Serilog.Expressions.Runtime;
4+
using Serilog.Parsing;
5+
using Serilog.Templates.Compilation;
6+
using Serilog.Templates.Themes;
7+
8+
namespace Serilog.Templates.Encoding
9+
{
10+
class EscapableEncodedCompiledFormattedExpression : CompiledTemplate
11+
{
12+
static int _nextSubstituteLocalNameSuffix;
13+
readonly string _substituteLocalName = $"%sub{Interlocked.Increment(ref _nextSubstituteLocalNameSuffix)}";
14+
readonly Evaluatable _expression;
15+
readonly TemplateOutputEncoder _encoder;
16+
readonly CompiledFormattedExpression _inner;
17+
18+
public EscapableEncodedCompiledFormattedExpression(Evaluatable expression, string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme, TemplateOutputEncoder encoder)
19+
{
20+
_expression = expression;
21+
_encoder = encoder;
22+
_inner = new CompiledFormattedExpression(GetSubstituteLocalValue, format, alignment, formatProvider, theme);
23+
}
24+
25+
LogEventPropertyValue? GetSubstituteLocalValue(EvaluationContext context)
26+
{
27+
return Locals.TryGetValue(context.Locals, _substituteLocalName, out var computed)
28+
? computed
29+
: null;
30+
}
31+
32+
public override void Evaluate(EvaluationContext ctx, TextWriter output)
33+
{
34+
var value = _expression(ctx);
35+
36+
if (value is ScalarValue { Value: PreEncodedValue pv })
37+
{
38+
var rawContext = pv.Inner == null ?
39+
new EvaluationContext(ctx.LogEvent) :
40+
new EvaluationContext(ctx.LogEvent, Locals.Set(ctx.Locals, _substituteLocalName, pv.Inner));
41+
_inner.Evaluate(rawContext, output);
42+
return;
43+
}
44+
45+
var buffer = new StringWriter(output.FormatProvider);
46+
47+
var bufferedContext = value == null
48+
? ctx
49+
: new EvaluationContext(ctx.LogEvent, Locals.Set(ctx.Locals, _substituteLocalName, value));
50+
51+
_inner.Evaluate(bufferedContext, buffer);
52+
var encoded = _encoder.Encode(buffer.ToString());
53+
output.Write(encoded);
54+
}
55+
}
56+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Serilog.Events;
2+
3+
namespace Serilog.Templates.Encoding
4+
{
5+
class PreEncodedValue
6+
{
7+
public LogEventPropertyValue? Inner { get; }
8+
9+
public PreEncodedValue(LogEventPropertyValue? inner)
10+
{
11+
Inner = inner;
12+
}
13+
14+
public override string ToString()
15+
{
16+
// This code path indicates that the template expects encoding to be performed, but no encoder is
17+
// registered (probably a bad situation to be in).
18+
throw new InvalidOperationException("No output encoder is registered.");
19+
}
20+
}
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Serilog.Templates.Encoding
2+
{
3+
/// <summary>
4+
/// An encoder applied to the output substituted into template holes.
5+
/// </summary>
6+
public abstract class TemplateOutputEncoder
7+
{
8+
/// <summary>
9+
/// Encode <paramref name="value" />.
10+
/// </summary>
11+
/// <param name="value">The raw template output to encode.</param>
12+
/// <returns>The encoded output.</returns>
13+
public abstract string Encode(string value);
14+
}
15+
}

src/Serilog.Expressions/Templates/ExpressionTemplate.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Serilog.Formatting;
1919
using Serilog.Templates.Compilation;
2020
using Serilog.Templates.Compilation.NameResolution;
21+
using Serilog.Templates.Encoding;
2122
using Serilog.Templates.Parsing;
2223
using Serilog.Templates.Themes;
2324

@@ -43,7 +44,7 @@ public static bool TryParse(
4344
[MaybeNullWhen(true)] out string error)
4445
{
4546
if (template == null) throw new ArgumentNullException(nameof(template));
46-
return TryParse(template, null, null, null, false, out result, out error);
47+
return TryParse(template, null, null, null, false, null, out result, out error);
4748
}
4849

4950
/// <summary>
@@ -59,13 +60,15 @@ public static bool TryParse(
5960
/// with which to resolve function names that appear in the template.</param>
6061
/// <param name="applyThemeWhenOutputIsRedirected">Apply <paramref name="theme"/> even when
6162
/// <see cref="System.Console.IsOutputRedirected"/> or <see cref="Console.IsErrorRedirected"/> returns <c>true</c>.</param>
63+
/// <param name="encoder">Optionally, an encoder to apply to output substituted into template holes.</param>
6264
/// <returns><c langword="true">true</c> if the template was well-formed.</returns>
6365
public static bool TryParse(
6466
string template,
6567
IFormatProvider? formatProvider,
6668
NameResolver? nameResolver,
6769
TemplateTheme? theme,
6870
bool applyThemeWhenOutputIsRedirected,
71+
TemplateOutputEncoder? encoder,
6972
[MaybeNullWhen(false)] out ExpressionTemplate result,
7073
[MaybeNullWhen(true)] out string error)
7174
{
@@ -85,7 +88,8 @@ public static bool TryParse(
8588
planned,
8689
formatProvider,
8790
TemplateFunctionNameResolver.Build(nameResolver, planned),
88-
SelectTheme(theme, applyThemeWhenOutputIsRedirected)));
91+
SelectTheme(theme, applyThemeWhenOutputIsRedirected),
92+
new EncodedTemplateFactory(encoder)));
8993

9094
return true;
9195
}
@@ -106,12 +110,14 @@ public static bool TryParse(
106110
/// <param name="theme">Optionally, an ANSI theme to apply to the template output.</param>
107111
/// <param name="applyThemeWhenOutputIsRedirected">Apply <paramref name="theme"/> even when
108112
/// <see cref="Console.IsOutputRedirected"/> or <see cref="Console.IsErrorRedirected"/> returns <c>true</c>.</param>
113+
/// <param name="encoder">Optionally, an encoder to apply to output substituted into template holes.</param>
109114
public ExpressionTemplate(
110115
string template,
111116
IFormatProvider? formatProvider = null,
112117
NameResolver? nameResolver = null,
113118
TemplateTheme? theme = null,
114-
bool applyThemeWhenOutputIsRedirected = false)
119+
bool applyThemeWhenOutputIsRedirected = false,
120+
TemplateOutputEncoder? encoder = null)
115121
{
116122
if (template == null) throw new ArgumentNullException(nameof(template));
117123

@@ -125,7 +131,8 @@ public ExpressionTemplate(
125131
planned,
126132
formatProvider,
127133
TemplateFunctionNameResolver.Build(nameResolver, planned),
128-
SelectTheme(theme, applyThemeWhenOutputIsRedirected));
134+
SelectTheme(theme, applyThemeWhenOutputIsRedirected),
135+
new EncodedTemplateFactory(encoder));
129136
}
130137

131138
static TemplateTheme SelectTheme(TemplateTheme? supplied, bool applyThemeWhenOutputIsRedirected)

0 commit comments

Comments
 (0)