Skip to content

Commit cd80817

Browse files
committed
Merge branch 'MessageFormatter'
2 parents 860d43e + 9846ef6 commit cd80817

12 files changed

+203
-6
lines changed

src/Log4NetTextFormatter.cs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public class Log4NetTextFormatter : ITextFormatter
5353
/// <remarks>https://github.com/pmetz-steelcase/Serilog.Enrichers.WithCaller/blob/1.2.0/Serilog.Enrichers.WithCaller/CallerEnricher.cs#L66</remarks>
5454
private const string CallerPropertyName = "Caller";
5555

56+
/// <summary>
57+
/// The name of the event id property, set by <a href="https://www.nuget.org/packages/Serilog.Extensions.Logging/">Serilog.Extensions.Logging</a>
58+
/// </summary>
59+
/// <remarks>https://github.com/serilog/serilog-extensions-logging/blob/v9.0.1/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs#L158</remarks>
60+
private const string EventIdPropertyName = "EventId";
61+
5662
/// <summary>
5763
/// The regular exception matching "class", "method", and optionally "file" and "line" for the caller property.
5864
/// </summary>
@@ -130,6 +136,43 @@ public void Format(LogEvent logEvent, TextWriter output)
130136
output.Write(_options.XmlWriterSettings.NewLineChars);
131137
}
132138

139+
/// <summary>
140+
/// The default message formatter.
141+
/// <list type="bullet">
142+
/// <item>If the log event comes from <c>Microsoft.Extensions.Logging</c> (detected by the presence of the <c>EventId</c> property) then the message is formatted by switching off quoting of strings.</item>
143+
/// <item>If the log event comes from Serilog, then the message is formatted by calling <see cref="LogEvent.RenderMessage(System.IO.TextWriter,System.IFormatProvider)"/>.</item>
144+
/// </list>
145+
/// </summary>
146+
/// <param name="logEvent">The log event to format.</param>
147+
/// <param name="formatProvider">The <see cref="IFormatProvider"/> that supplies culture-specific formatting information, or <see langword="null"/>.</param>
148+
/// <returns>The formatted message.</returns>
149+
internal static string DefaultMessageFormatter(LogEvent logEvent, IFormatProvider? formatProvider)
150+
{
151+
// https://github.com/serilog/serilog-extensions-logging/blob/v9.0.1/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs#L58-L73
152+
var isMicrosoftExtensionsLoggingEvent = logEvent.Properties.TryGetValue(EventIdPropertyName, out var property)
153+
&& property is StructureValue structure
154+
&& structure.Properties.Count == 2
155+
&& structure.Properties[0].Name == "Id"
156+
&& structure.Properties[1].Name == "Name";
157+
158+
if (isMicrosoftExtensionsLoggingEvent)
159+
{
160+
var formatter = new MessageTemplateTextFormatter($"{{{OutputProperties.MessagePropertyName}:l}}", formatProvider);
161+
using var output = new StringWriter();
162+
formatter.Format(logEvent, output);
163+
return output.ToString();
164+
}
165+
166+
return logEvent.RenderMessage(formatProvider);
167+
}
168+
169+
/// <summary>
170+
/// The default exception formatter. Calls the <see cref="Exception.ToString"/> method on the exception.
171+
/// </summary>
172+
/// <param name="exception">The exception to format.</param>
173+
/// <returns>The formatted exception.</returns>
174+
internal static string DefaultExceptionFormatter(Exception exception) => exception.ToString();
175+
133176
/// <summary>
134177
/// Write the log event into the XML writer.
135178
/// </summary>
@@ -379,9 +422,20 @@ private void WritePropertyElement(XmlWriter writer, string name, LogEventPropert
379422
/// <param name="logEvent">The log event.</param>
380423
/// <param name="writer">The XML writer.</param>
381424
/// <remarks>https://github.com/apache/logging-log4net/blob/rel/2.0.8/src/Layout/XmlLayout.cs#L245-L257</remarks>
425+
[SuppressMessage("Microsoft.Design", "CA1031", Justification = "Protecting from user-provided code which might throw anything")]
382426
private void WriteMessage(LogEvent logEvent, XmlWriter writer)
383427
{
384-
var message = logEvent.RenderMessage(_options.FormatProvider);
428+
string message;
429+
try
430+
{
431+
message = _options.FormatMessage(logEvent, _options.FormatProvider);
432+
}
433+
catch (Exception exception)
434+
{
435+
Debugging.SelfLog.WriteLine($"[{GetType().FullName}] An exception was thrown while formatting a message. Using the default message formatter.\n{exception}");
436+
message = DefaultMessageFormatter(logEvent, _options.FormatProvider);
437+
}
438+
385439
WriteContent(writer, "message", message);
386440
}
387441

@@ -426,7 +480,7 @@ private void WriteException(LogEvent logEvent, XmlWriter writer)
426480
catch (Exception formattingException)
427481
{
428482
Debugging.SelfLog.WriteLine($"[{GetType().FullName}] An exception was thrown while formatting an exception. Using the default exception formatter.\n{formattingException}");
429-
return exception.ToString();
483+
return DefaultExceptionFormatter(exception);
430484
}
431485
}
432486

src/Log4NetTextFormatterOptions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ namespace Serilog.Formatting.Log4Net;
88
/// </summary>
99
internal sealed class Log4NetTextFormatterOptions
1010
{
11-
internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode cDataMode, XmlQualifiedName? xmlNamespace, XmlWriterSettings xmlWriterSettings, PropertyFilter filterProperty, ExceptionFormatter formatException)
11+
internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode cDataMode, XmlQualifiedName? xmlNamespace, XmlWriterSettings xmlWriterSettings, PropertyFilter filterProperty, MessageFormatter formatMessage, ExceptionFormatter formatException)
1212
{
1313
FormatProvider = formatProvider;
1414
CDataMode = cDataMode;
1515
XmlNamespace = xmlNamespace;
1616
XmlWriterSettings = xmlWriterSettings;
1717
FilterProperty = filterProperty;
18+
FormatMessage = formatMessage;
1819
FormatException = formatException;
1920
}
2021

@@ -33,6 +34,9 @@ internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode
3334
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UsePropertyFilter"/></summary>
3435
internal PropertyFilter FilterProperty { get; }
3536

37+
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseMessageFormatter"/></summary>
38+
internal MessageFormatter FormatMessage { get; }
39+
3640
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseExceptionFormatter"/></summary>
3741
internal ExceptionFormatter FormatException { get; }
3842
}

src/Log4NetTextFormatterOptionsBuilder.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ internal Log4NetTextFormatterOptionsBuilder()
4646
/// <summary>See <see cref="UsePropertyFilter"/></summary>
4747
private PropertyFilter _filterProperty = (_, _) => true;
4848

49+
/// <summary>See <see cref="UseMessageFormatter"/></summary>
50+
private MessageFormatter _formatMessage = Log4NetTextFormatter.DefaultMessageFormatter;
51+
4952
/// <summary>See <see cref="UseExceptionFormatter"/></summary>
50-
private ExceptionFormatter _formatException = exception => exception.ToString();
53+
private ExceptionFormatter _formatException = Log4NetTextFormatter.DefaultExceptionFormatter;
5154

5255
/// <summary>
5356
/// Sets the <see cref="IFormatProvider"/> used when formatting message and properties of log4net events.
@@ -136,6 +139,23 @@ public Log4NetTextFormatterOptionsBuilder UsePropertyFilter(PropertyFilter filte
136139
return this;
137140
}
138141

142+
/// <summary>
143+
/// Sets the <see cref="MessageFormatter"/> controlling how all messages are formatted.
144+
/// <para/>
145+
/// The default message formatter has two formatting modes.
146+
/// <list type="bullet">
147+
/// <item>If the log event comes from <c>Microsoft.Extensions.Logging</c> (detected by the presence of the <c>EventId</c> property) then the message is formatted by switching off quoting of strings.</item>
148+
/// <item>If the log event comes from Serilog, then the message is formatted by calling <see cref="LogEvent.RenderMessage(System.IO.TextWriter,System.IFormatProvider)"/>.</item>
149+
/// </list>
150+
/// </summary>
151+
/// <remarks>If an exception is thrown while executing the formatter, the default formatter will be used.</remarks>
152+
/// <returns>The builder in order to fluently chain all options.</returns>
153+
public Log4NetTextFormatterOptionsBuilder UseMessageFormatter(MessageFormatter formatMessage)
154+
{
155+
_formatMessage = formatMessage ?? throw new ArgumentNullException(nameof(formatMessage), "The message formatter can not be null.");
156+
return this;
157+
}
158+
139159
/// <summary>
140160
/// Sets the <see cref="ExceptionFormatter"/> controlling how all exceptions are formatted.
141161
/// <para/>
@@ -177,7 +197,7 @@ public void UseLog4JCompatibility()
177197
}
178198

179199
internal Log4NetTextFormatterOptions Build()
180-
=> new(_formatProvider, _cDataMode, _xmlNamespace, CreateXmlWriterSettings(_lineEnding, _indentationSettings), _filterProperty, _formatException);
200+
=> new(_formatProvider, _cDataMode, _xmlNamespace, CreateXmlWriterSettings(_lineEnding, _indentationSettings), _filterProperty, _formatMessage, _formatException);
181201

182202
private static XmlWriterSettings CreateXmlWriterSettings(LineEnding lineEnding, IndentationSettings? indentationSettings)
183203
{
@@ -203,8 +223,17 @@ private static XmlWriterSettings CreateXmlWriterSettings(LineEnding lineEnding,
203223
/// <returns><see langword="true"/> to include the Serilog property in the log4net properties or <see langword="false"/> to ignore the Serilog property.</returns>
204224
public delegate bool PropertyFilter(LogEvent logEvent, string propertyName);
205225

226+
/// <summary>
227+
/// Represents the method that formats the message for a <see cref="LogEvent"/>.
228+
/// </summary>
229+
/// <param name="logEvent">The log event to format.</param>
230+
/// <param name="formatProvider">The <see cref="IFormatProvider"/> that supplies culture-specific formatting information, or <see langword="null"/>.</param>
231+
/// <returns>The formatted message.</returns>
232+
public delegate string MessageFormatter(LogEvent logEvent, IFormatProvider? formatProvider);
233+
206234
/// <summary>
207235
/// Represents the method that formats an <see cref="Exception"/>.
208236
/// </summary>
209-
/// <param name="exception">The exception to be formatted.</param>
237+
/// <param name="exception">The exception to format.</param>
238+
/// <returns>The formatted exception.</returns>
210239
public delegate string ExceptionFormatter(Exception exception);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="INFO" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
2+
<log4net:message><![CDATA[Custom message]]></log4net:message>
3+
</log4net:event>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="INFO" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
2+
<log4net:properties>
3+
<log4net:data name="Name" value="World" />
4+
</log4net:properties>
5+
<log4net:message><![CDATA[Hello "World"!]]></log4net:message>
6+
</log4net:event>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="INFO" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
2+
<log4net:properties>
3+
<log4net:data name="Name" value="World" />
4+
<log4net:data name="EventId.Id" value="1" />
5+
<log4net:data name="EventId.Name" value="EventIdName" />
6+
</log4net:properties>
7+
<log4net:message><![CDATA[Hello World!]]></log4net:message>
8+
</log4net:event>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="INFO" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
2+
<log4net:message><![CDATA[]]></log4net:message>
3+
</log4net:event>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="INFO" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
2+
<log4net:message><![CDATA[Hello from Serilog]]></log4net:message>
3+
</log4net:event>

tests/Log4NetTextFormatterTest.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ public void SettingPropertyFilterToNullThrowsArgumentNullException()
9494
.And.ParamName.Should().Be("filterProperty");
9595
}
9696

97+
[Fact]
98+
public void SettingMessageFormatterToNullThrowsArgumentNullException()
99+
{
100+
// Act
101+
Action action = () => _ = new Log4NetTextFormatter(c => c.UseMessageFormatter(null!));
102+
103+
// Assert
104+
action.Should().ThrowExactly<ArgumentNullException>()
105+
.WithMessage("The message formatter can not be null.*")
106+
.And.ParamName.Should().Be("formatMessage");
107+
}
108+
97109
[Fact]
98110
public void SettingExceptionFormatterToNullThrowsArgumentNullException()
99111
{
@@ -438,6 +450,75 @@ public Task Exception()
438450
return Verify(output);
439451
}
440452

453+
[Theory]
454+
[InlineData(true)]
455+
[InlineData(false)]
456+
public Task DefaultMessageFormatter(bool hasEventId)
457+
{
458+
// Arrange
459+
using var output = new StringWriter();
460+
List<LogEventProperty> properties = [ new("Name", new ScalarValue("World")) ];
461+
if (hasEventId)
462+
{
463+
LogEventProperty[] eventIdProperties = [ new("Id", new ScalarValue(1)), new("Name", new ScalarValue("EventIdName")) ];
464+
properties.Add(new LogEventProperty("EventId", new StructureValue(eventIdProperties)));
465+
}
466+
var logEvent = CreateLogEvent(messageTemplate: "Hello {Name}!", properties: properties.ToArray());
467+
var formatter = new Log4NetTextFormatter();
468+
469+
// Act
470+
formatter.Format(logEvent, output);
471+
472+
// Assert
473+
return Verify(output).UseParameters(hasEventId);
474+
}
475+
476+
[Fact]
477+
public Task CustomMessageFormatter()
478+
{
479+
// Arrange
480+
using var output = new StringWriter();
481+
var logEvent = CreateLogEvent();
482+
var formatter = new Log4NetTextFormatter(options => options.UseMessageFormatter((_, _) => "Custom message"));
483+
484+
// Act
485+
formatter.Format(logEvent, output);
486+
487+
// Assert
488+
return Verify(output);
489+
}
490+
491+
[Fact]
492+
public Task MessageFormatterReturningNull()
493+
{
494+
// Arrange
495+
using var output = new StringWriter();
496+
var logEvent = CreateLogEvent();
497+
var formatter = new Log4NetTextFormatter(options => options.UseMessageFormatter((_, _) => null!));
498+
499+
// Act
500+
formatter.Format(logEvent, output);
501+
502+
// Assert
503+
return Verify(output);
504+
}
505+
506+
[Fact]
507+
public Task MessageFormatterThrowing()
508+
{
509+
// Arrange
510+
using var output = new StringWriter();
511+
var logEvent = CreateLogEvent();
512+
var formatter = new Log4NetTextFormatter(options => options.UseMessageFormatter((_, _) => throw new InvalidOperationException("💥 Boom 💥")));
513+
514+
// Act
515+
formatter.Format(logEvent, output);
516+
517+
// Assert
518+
SelfLogValue.Should().Contain("[Serilog.Formatting.Log4Net.Log4NetTextFormatter] An exception was thrown while formatting a message.");
519+
return Verify(output);
520+
}
521+
441522
[Fact]
442523
public Task ExceptionFormatter()
443524
{

tests/PublicApi.net6.0.verified.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UseFormatPr
4242
public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UseIndentationSettings(Serilog.Formatting.Log4Net.IndentationSettings indentationSettings) { }
4343
public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UseLineEnding(Serilog.Formatting.Log4Net.LineEnding lineEnding) { }
4444
public void UseLog4JCompatibility() { }
45+
public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UseMessageFormatter(Serilog.Formatting.Log4Net.MessageFormatter formatMessage) { }
4546
public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UseNoIndentation() { }
4647
public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UseNoXmlNamespace() { }
4748
public Serilog.Formatting.Log4Net.Log4NetTextFormatterOptionsBuilder UsePropertyFilter(Serilog.Formatting.Log4Net.PropertyFilter filterProperty) { }
4849
}
50+
public delegate string MessageFormatter(Serilog.Events.LogEvent logEvent, System.IFormatProvider? formatProvider);
4951
public delegate bool PropertyFilter(Serilog.Events.LogEvent logEvent, string propertyName);
5052
}

0 commit comments

Comments
 (0)