Skip to content

Commit 4ebb106

Browse files
committed
Implement log4j compatibility mode
1 parent 41ab75e commit 4ebb106

7 files changed

+111
-8
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ By default, Log4NetTextFormatter serializes all Serilog properties. You can filt
107107
new Log4NetTextFormatter(c => c.UsePropertyFilter((_, name) => name != "MySecretProperty"))
108108
```
109109

110+
#### Log4j compatibility mode
111+
112+
The formatter also supports a log4j compatibility mode. Log4Net and Log4j XML formats are very similar but have a few key differences.
113+
114+
* Events are in different XML namespaces
115+
* The `timestamp` is a number of milliseconds (log4j) vs an ISO 8061 formatted date (log4net)
116+
* Exception elements are named `throwable` vs `exception`
117+
118+
In order to enable the compatibility mode, call `UseLog4JCompatibility()`:
119+
120+
```c#
121+
new Log4NetTextFormatter(c => c.UseLog4JCompatibility())
122+
```
123+
124+
Note that unlike other fluent configuration methods, this one can not be chained because you should not change options after enabling the log4j compatibility mode.
125+
110126
### Combining options
111127

112128
You can also combine options, for example, both removing namespaces and using Ben.Demystifier for exception formatting:

src/Log4NetTextFormatter.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
4+
using System.Globalization;
45
using System.IO;
56
using System.Linq;
67
using System.Xml;
@@ -89,9 +90,11 @@ public void Format(LogEvent logEvent, TextWriter output)
8990
/// <remarks>https://github.com/apache/logging-log4net/blob/rel/2.0.8/src/Layout/XmlLayout.cs#L218-L310</remarks>
9091
private void WriteEvent(LogEvent logEvent, XmlWriter writer)
9192
{
93+
var useLog4JCompatibility = _options.Log4NetXmlNamespace?.Name == "log4j";
9294
WriteStartElement(writer, "event");
9395
WriteEventAttribute(logEvent, writer, "logger", Constants.SourceContextPropertyName);
94-
writer.WriteAttributeString("timestamp", XmlConvert.ToString(logEvent.Timestamp));
96+
var timestamp = useLog4JCompatibility ? logEvent.Timestamp.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture) : XmlConvert.ToString(logEvent.Timestamp);
97+
writer.WriteAttributeString("timestamp", timestamp);
9598
writer.WriteAttributeString("level", LogLevel(logEvent.Level));
9699
WriteEventAttribute(logEvent, writer, "thread", ThreadIdPropertyName);
97100
WriteDomainAndUserName(logEvent, writer);
@@ -102,7 +105,7 @@ private void WriteEvent(LogEvent logEvent, XmlWriter writer)
102105
WriteProperties(logEvent, writer, properties, machineNameProperty);
103106
}
104107
WriteMessage(logEvent, writer);
105-
WriteException(logEvent, writer);
108+
WriteException(logEvent, writer, useLog4JCompatibility ? "throwable" : "exception");
106109
writer.WriteEndElement();
107110
}
108111

@@ -146,16 +149,17 @@ private void WriteEventAttribute(LogEvent logEvent, XmlWriter writer, string att
146149
}
147150

148151
/// <summary>
149-
/// Convert Serilog <see cref="LogEventLevel"/> into log4net equivalent level.
152+
/// Convert Serilog <see cref="LogEventLevel"/> into log4net/log4j equivalent level.
150153
/// </summary>
151154
/// <param name="level">The serilog level.</param>
152-
/// <returns>The equivalent log4net level.</returns>
155+
/// <returns>The equivalent log4net/log4j level.</returns>
153156
/// <remarks>https://github.com/apache/logging-log4net/blob/rel/2.0.8/src/Core/Level.cs#L509-L603</remarks>
157+
/// <remarks>https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/Level.java#L48-L92</remarks>
154158
private static string LogLevel(LogEventLevel level)
155159
{
156160
return level switch
157161
{
158-
LogEventLevel.Verbose => "VERBOSE",
162+
LogEventLevel.Verbose => "TRACE",
159163
LogEventLevel.Debug => "DEBUG",
160164
LogEventLevel.Information => "INFO",
161165
LogEventLevel.Warning => "WARN",
@@ -331,9 +335,10 @@ private void WriteMessage(LogEvent logEvent, XmlWriter writer)
331335
/// </summary>
332336
/// <param name="logEvent">The log event.</param>
333337
/// <param name="writer">The XML writer.</param>
338+
/// <param name="elementName">The element name, should be either <c>exception</c> or <c>throwable</c>.</param>
334339
/// <remarks>https://github.com/apache/logging-log4net/blob/rel/2.0.8/src/Layout/XmlLayout.cs#L288-L295</remarks>
335340
[SuppressMessage("Microsoft.Design", "CA1031", Justification = "Protecting from user-provided code which might throw anything")]
336-
private void WriteException(LogEvent logEvent, XmlWriter writer)
341+
private void WriteException(LogEvent logEvent, XmlWriter writer, string elementName)
337342
{
338343
var exception = logEvent.Exception;
339344
if (exception == null)
@@ -352,7 +357,7 @@ private void WriteException(LogEvent logEvent, XmlWriter writer)
352357
}
353358
if (formattedException != null)
354359
{
355-
WriteContent(writer, "exception", formattedException);
360+
WriteContent(writer, elementName, formattedException);
356361
}
357362
}
358363

src/Log4NetTextFormatterOptionsBuilder.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,31 @@ public Log4NetTextFormatterOptionsBuilder UseExceptionFormatter(ExceptionFormatt
151151
return this;
152152
}
153153

154+
/// <summary>
155+
/// Enables log4j compatibility mode. This tweaks the XML elements to match the log4j logging event specification.
156+
/// The DTD can be found at https://raw.githubusercontent.com/apache/log4j/v1_2_17/src/main/resources/org/apache/log4j/xml/log4j.dtd
157+
/// <para>
158+
/// Here is the list of differences between the log4net and the log4j XML layout:
159+
/// <list type="bullet">
160+
/// <item>The log element uses <c>log4j</c> instead of <c>log4net</c> XML namespace.</item>
161+
/// <item>The <c>timestamp</c> attribute uses milliseconds elapsed from 1/1/1970 instead of an ISO 8601 formatted date.</item>
162+
/// <item>The exception element is named <c>throwable</c> instead of <c>exception</c>.</item>
163+
/// </list>
164+
/// </para>
165+
/// </summary>
166+
/// <remarks>You must not change other options after calling this method.</remarks>
167+
public void UseLog4JCompatibility()
168+
{
169+
// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L135
170+
LineEnding = LineEnding.CarriageReturn | LineEnding.LineFeed;
171+
172+
// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137
173+
Log4NetXmlNamespace = new XmlQualifiedName("log4j", "http://jakarta.apache.org/log4j/");
174+
175+
// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L147
176+
CDataMode = CDataMode.Always;
177+
}
178+
154179
internal Log4NetTextFormatterOptions Build()
155180
=> new Log4NetTextFormatterOptions(FormatProvider, CDataMode, Log4NetXmlNamespace, CreateXmlWriterSettings(LineEnding, IndentationSettings), FilterProperty, FormatException);
156181

tests/Log4NetTextFormatterOptionsBuilderTest.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,22 @@ public void UseExceptionFormatter()
120120
builder.FormatException.Should().BeSameAs(formatException);
121121
}
122122

123+
[Fact]
124+
public void UseLog4JCompatibility()
125+
{
126+
// Arrange
127+
var builder = new Log4NetTextFormatterOptionsBuilder();
128+
129+
// Act
130+
builder.UseLog4JCompatibility();
131+
132+
// Assert
133+
builder.LineEnding.Should().Be(LineEnding.CarriageReturn | LineEnding.LineFeed);
134+
builder.CDataMode.Should().Be(CDataMode.Always);
135+
builder.Log4NetXmlNamespace.Should().NotBeNull();
136+
builder.Log4NetXmlNamespace!.Name.Should().Be("log4j");
137+
}
138+
123139
[Fact]
124140
public void SettingExceptionFormatterToNullThrowsArgumentNullException()
125141
{
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<log4j:event timestamp="1041689366535" level="INFO" xmlns:log4j="http://jakarta.apache.org/log4j/">
2+
<log4j:properties>
3+
<log4j:data name="π" value="3.14" />
4+
</log4j:properties>
5+
<log4j:message><![CDATA[Hello from Serilog]]></log4j:message>
6+
<log4j:throwable><![CDATA[System.Exception: An error occurred
7+
at Serilog.Formatting.Log4Net.Tests.Log4NetTextFormatterTest.BasicMessage_WithException() in Log4NetTextFormatterTest.cs:123]]></log4j:throwable>
8+
</log4j:event>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="VERBOSE" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
1+
<log4net:event timestamp="2003-01-04T15:09:26.535+01:00" level="TRACE" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2/">
22
<log4net:message><![CDATA[Hello from Serilog]]></log4net:message>
33
</log4net:event>

tests/Log4NetTextFormatterTest.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ public void InvalidLogEventLevelThrowsArgumentOutOfRangeException()
118118
.And.Message.Should().StartWith("The value of argument 'level' (-1) is invalid for enum type 'LogEventLevel'.");
119119
}
120120

121+
[Fact]
122+
public void InvalidLogEventLevelWithLog4JCompatibilityThrowsArgumentOutOfRangeException()
123+
{
124+
// Arrange
125+
var logEvent = CreateLogEvent((LogEventLevel)(-1));
126+
var formatter = new Log4NetTextFormatter(c => c.UseLog4JCompatibility());
127+
128+
// Act
129+
Action action = () => formatter.Format(logEvent, TextWriter.Null);
130+
131+
// Assert
132+
action.Should().ThrowExactly<ArgumentOutOfRangeException>()
133+
.And.Message.Should().StartWith("The value of argument 'level' (-1) is invalid for enum type 'LogEventLevel'.");
134+
}
135+
121136
[Theory]
122137
[InlineData(Log4Net.CDataMode.Always, true)]
123138
[InlineData(Log4Net.CDataMode.Always, false)]
@@ -258,6 +273,24 @@ public void DefaultFormatProvider()
258273
Approvals.VerifyWithExtension(output.ToString(), "xml");
259274
}
260275

276+
[Fact]
277+
public void Log4JCompatibility()
278+
{
279+
// Arrange
280+
using var output = new StringWriter();
281+
var logEvent = CreateLogEvent(
282+
exception: new Exception("An error occurred").SetStackTrace(@" at Serilog.Formatting.Log4Net.Tests.Log4NetTextFormatterTest.BasicMessage_WithException() in Log4NetTextFormatterTest.cs:123"),
283+
properties: new LogEventProperty("π", new ScalarValue(3.14m))
284+
);
285+
var formatter = new Log4NetTextFormatter(c => c.UseLog4JCompatibility());
286+
287+
// Act
288+
formatter.Format(logEvent, output);
289+
290+
// Assert
291+
Approvals.VerifyWithExtension(output.ToString(), "xml");
292+
}
293+
261294
[Fact]
262295
public void ExplicitFormatProvider()
263296
{

0 commit comments

Comments
 (0)