Skip to content

Commit 198499c

Browse files
authored
Merge pull request #228 from liammclennan/custom-format-provider
Custom format provider
2 parents b3069e5 + c2fe74d commit 198499c

File tree

5 files changed

+270
-11
lines changed

5 files changed

+270
-11
lines changed

src/Serilog.Sinks.Seq/SeqLoggerConfigurationExtensions.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Globalization;
1617
using Serilog.Configuration;
1718
using Serilog.Core;
1819
using Serilog.Events;
@@ -34,7 +35,7 @@ public static class SeqLoggerConfigurationExtensions
3435
const int DefaultBatchPostingLimit = 1000;
3536
static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(2);
3637
const int DefaultQueueSizeLimit = 100000;
37-
static ITextFormatter CreateDefaultFormatter() => new SeqCompactJsonFormatter();
38+
static ITextFormatter CreateDefaultFormatter(IFormatProvider? formatProvider) => new SeqCompactJsonFormatter(formatProvider);
3839

3940
/// <summary>
4041
/// Write log events to a <a href="https://datalust.co/seq">Seq</a> server.
@@ -67,6 +68,8 @@ public static class SeqLoggerConfigurationExtensions
6768
/// durable log shipping.</param>
6869
/// <param name="payloadFormatter">An <see cref="ITextFormatter"/> that will be used to format (newline-delimited CLEF/JSON)
6970
/// payloads. Experimental.</param>
71+
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens. Does not apply if <paramref name="payloadFormatter"/> is provided.
72+
/// If <paramref name="formatProvider"/> is provided then event messages will be rendered and included in the payload.</param>
7073
/// <returns>Logger configuration, allowing configuration to continue.</returns>
7174
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
7275
public static LoggerConfiguration Seq(
@@ -83,7 +86,8 @@ public static LoggerConfiguration Seq(
8386
HttpMessageHandler? messageHandler = null,
8487
long? retainedInvalidPayloadsLimitBytes = null,
8588
int queueSizeLimit = DefaultQueueSizeLimit,
86-
ITextFormatter? payloadFormatter = null)
89+
ITextFormatter? payloadFormatter = null,
90+
IFormatProvider? formatProvider = null)
8791
{
8892
if (loggerSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerSinkConfiguration));
8993
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
@@ -94,7 +98,7 @@ public static LoggerConfiguration Seq(
9498

9599
var defaultedPeriod = period ?? DefaultPeriod;
96100
var controlledSwitch = new ControlledLevelSwitch(controlLevelSwitch);
97-
var formatter = payloadFormatter ?? CreateDefaultFormatter();
101+
var formatter = payloadFormatter ?? CreateDefaultFormatter(formatProvider);
98102
var ingestionApi = new SeqIngestionApiClient(serverUrl, apiKey, messageHandler);
99103

100104
if (bufferBaseFilename == null)
@@ -145,6 +149,7 @@ public static LoggerConfiguration Seq(
145149
/// <param name="messageHandler">Used to construct the HttpClient that will send the log messages to Seq.</param>
146150
/// <param name="payloadFormatter">An <see cref="ITextFormatter"/> that will be used to format (newline-delimited CLEF/JSON)
147151
/// payloads. Experimental.</param>
152+
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens.</param>
148153
/// <returns>Logger configuration, allowing configuration to continue.</returns>
149154
/// <exception cref="ArgumentNullException">A required parameter is null.</exception>
150155
public static LoggerConfiguration Seq(
@@ -153,13 +158,14 @@ public static LoggerConfiguration Seq(
153158
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
154159
string? apiKey = null,
155160
HttpMessageHandler? messageHandler = null,
156-
ITextFormatter? payloadFormatter = null)
161+
ITextFormatter? payloadFormatter = null,
162+
IFormatProvider? formatProvider = null)
157163
{
158164
if (loggerAuditSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerAuditSinkConfiguration));
159165
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
160166

161167
var ingestionApi = new SeqIngestionApiClient(serverUrl, apiKey, messageHandler);
162-
var sink = new SeqAuditSink(ingestionApi, payloadFormatter ?? CreateDefaultFormatter());
168+
var sink = new SeqAuditSink(ingestionApi, payloadFormatter ?? CreateDefaultFormatter(formatProvider));
163169
return loggerAuditSinkConfiguration.Sink(sink, restrictedToMinimumLevel);
164170
}
165171
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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;
16+
using System.Collections.Generic;
17+
using System.IO;
18+
using Serilog.Events;
19+
using Serilog.Formatting.Json;
20+
using Serilog.Parsing;
21+
22+
namespace Serilog.Sinks.Seq.Formatting;
23+
24+
/// <summary>
25+
/// Matches the `:lj` clean formatting style now employed by Serilog.Expressions, Serilog.Sinks.Console, and elsewhere.
26+
/// In this mode, strings embedded in message templates are unquoted, and structured data is rendered as JSON.
27+
/// </summary>
28+
/// <remarks>This implementation is derived from the Serilog.Expressions one, sans theming support, and avoiding the
29+
/// extra dependency. In time there should be core Serilog support for this.</remarks>
30+
static class CleanMessageTemplateFormatter
31+
{
32+
static readonly JsonValueFormatter SharedJsonValueFormatter = new("$type");
33+
34+
public static string Format(MessageTemplate messageTemplate, IReadOnlyDictionary<string, LogEventPropertyValue> properties, IFormatProvider? formatProvider)
35+
{
36+
var output = new StringWriter();
37+
38+
foreach (var token in messageTemplate.Tokens)
39+
{
40+
switch (token)
41+
{
42+
case TextToken tt:
43+
{
44+
output.Write(tt.Text);
45+
break;
46+
}
47+
case PropertyToken pt:
48+
{
49+
RenderPropertyToken(properties, pt, output, formatProvider);
50+
break;
51+
}
52+
default:
53+
{
54+
output.Write(token);
55+
break;
56+
}
57+
}
58+
}
59+
60+
return output.ToString();
61+
}
62+
63+
static void RenderPropertyToken(IReadOnlyDictionary<string, LogEventPropertyValue> properties, PropertyToken pt, TextWriter output, IFormatProvider? formatProvider)
64+
{
65+
if (!properties.TryGetValue(pt.PropertyName, out var value))
66+
{
67+
output.Write(pt.ToString());
68+
return;
69+
}
70+
71+
if (pt.Alignment is null)
72+
{
73+
RenderPropertyValueUnaligned(value, output, pt.Format, formatProvider);
74+
return;
75+
}
76+
77+
var buffer = new StringWriter();
78+
79+
RenderPropertyValueUnaligned(value, buffer, pt.Format, formatProvider);
80+
81+
var result = buffer.ToString();
82+
83+
if (result.Length >= pt.Alignment.Value.Width)
84+
output.Write(result);
85+
else
86+
Padding.Apply(output, result, pt.Alignment.Value);
87+
}
88+
89+
static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, TextWriter output, string? format, IFormatProvider? formatProvider)
90+
{
91+
if (propertyValue is not ScalarValue scalar)
92+
{
93+
SharedJsonValueFormatter.Format(propertyValue, output);
94+
return;
95+
}
96+
97+
var value = scalar.Value;
98+
99+
if (value == null)
100+
{
101+
output.Write("null");
102+
return;
103+
}
104+
105+
if (value is string str)
106+
{
107+
output.Write(str);
108+
return;
109+
}
110+
111+
if (value is ValueType)
112+
{
113+
if (value is int or uint or long or ulong or decimal or byte or sbyte or short or ushort)
114+
{
115+
output.Write(((IFormattable)value).ToString(format, formatProvider));
116+
return;
117+
}
118+
119+
if (value is double d)
120+
{
121+
output.Write(d.ToString(format, formatProvider));
122+
return;
123+
}
124+
125+
if (value is float f)
126+
{
127+
output.Write(f.ToString(format, formatProvider));
128+
return;
129+
}
130+
131+
if (value is bool b)
132+
{
133+
output.Write(b);
134+
return;
135+
}
136+
}
137+
138+
if (value is IFormattable formattable)
139+
{
140+
output.Write(formattable.ToString(format, formatProvider));
141+
return;
142+
}
143+
144+
output.Write(value);
145+
}
146+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.IO;
16+
using System.Linq;
17+
using Serilog.Parsing;
18+
19+
namespace Serilog.Sinks.Seq.Formatting
20+
{
21+
static class Padding
22+
{
23+
static readonly char[] PaddingChars = Enumerable.Repeat(' ', 80).ToArray();
24+
25+
/// <summary>
26+
/// Writes the provided value to the output, applying direction-based padding when <paramref name="alignment"/> is provided.
27+
/// </summary>
28+
public static void Apply(TextWriter output, string value, Alignment alignment)
29+
{
30+
if (value.Length >= alignment.Width)
31+
{
32+
output.Write(value);
33+
return;
34+
}
35+
36+
var pad = alignment.Width - value.Length;
37+
38+
if (alignment.Direction == AlignmentDirection.Left)
39+
output.Write(value);
40+
41+
if (pad <= PaddingChars.Length)
42+
{
43+
output.Write(PaddingChars, 0, pad);
44+
}
45+
else
46+
{
47+
output.Write(new string(' ', pad));
48+
}
49+
50+
if (alignment.Direction == AlignmentDirection.Right)
51+
output.Write(value);
52+
}
53+
}
54+
}

src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Serilog.Formatting.Json;
2323
using Serilog.Parsing;
2424
using Serilog.Sinks.Seq.Conventions;
25+
using Serilog.Sinks.Seq.Formatting;
2526

2627
// ReSharper disable MemberCanBePrivate.Global
2728
// ReSharper disable PossibleMultipleEnumeration
@@ -41,15 +42,22 @@ public class SeqCompactJsonFormatter: ITextFormatter
4142
new PreserveDottedPropertyNames();
4243

4344
readonly JsonValueFormatter _valueFormatter = new("$type");
45+
readonly IFormatProvider _formatProvider;
4446

47+
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens.</param>
48+
public SeqCompactJsonFormatter(IFormatProvider? formatProvider = null)
49+
{
50+
_formatProvider = formatProvider ?? CultureInfo.InvariantCulture;
51+
}
52+
4553
/// <summary>
4654
/// Format the log event into the output. Subsequent events will be newline-delimited.
4755
/// </summary>
4856
/// <param name="logEvent">The event to format.</param>
4957
/// <param name="output">The output.</param>
5058
public void Format(LogEvent logEvent, TextWriter output)
5159
{
52-
FormatEvent(logEvent, output, _valueFormatter);
60+
FormatEvent(logEvent, output, _valueFormatter, _formatProvider);
5361
output.WriteLine();
5462
}
5563

@@ -59,17 +67,29 @@ public void Format(LogEvent logEvent, TextWriter output)
5967
/// <param name="logEvent">The event to format.</param>
6068
/// <param name="output">The output.</param>
6169
/// <param name="valueFormatter">A value formatter for <see cref="LogEventPropertyValue"/>s on the event.</param>
62-
public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFormatter valueFormatter)
70+
/// <param name="formatProvider">An <see cref="IFormatProvider"/> that will be used to render log event tokens.</param>
71+
public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFormatter valueFormatter, IFormatProvider formatProvider)
6372
{
6473
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
6574
if (output == null) throw new ArgumentNullException(nameof(output));
6675
if (valueFormatter == null) throw new ArgumentNullException(nameof(valueFormatter));
6776

6877
output.Write("{\"@t\":\"");
6978
output.Write(logEvent.Timestamp.UtcDateTime.ToString("O"));
79+
7080
output.Write("\",\"@mt\":");
7181
JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output);
7282

83+
if (!formatProvider.Equals(CultureInfo.InvariantCulture))
84+
{
85+
// `@m` is normally created during ingestion, however, it must be sent from the client
86+
// to honour non-default IFormatProviders
87+
output.Write(",\"@m\":");
88+
JsonValueFormatter.WriteQuotedJsonString(
89+
CleanMessageTemplateFormatter.Format(logEvent.MessageTemplate, logEvent.Properties, formatProvider),
90+
output);
91+
}
92+
7393
var tokensWithFormat = logEvent.MessageTemplate.Tokens
7494
.OfType<PropertyToken>()
7595
.Where(pt => pt.Format != null);
@@ -85,7 +105,7 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo
85105
output.Write(delim);
86106
delim = ",";
87107
var space = new StringWriter();
88-
r.Render(logEvent.Properties, space, CultureInfo.InvariantCulture);
108+
r.Render(logEvent.Properties, space, formatProvider);
89109
JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output);
90110
}
91111
output.Write(']');

0 commit comments

Comments
 (0)