Skip to content

Commit 82f331f

Browse files
committed
Log4JXmlNamespaceResolver hack
1 parent 7df550c commit 82f331f

File tree

4 files changed

+97
-28
lines changed

4 files changed

+97
-28
lines changed

src/Log4JXmlNamespaceResolver.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections.Generic;
2+
using System.Xml;
3+
4+
namespace Serilog.Formatting.Log4Net;
5+
6+
internal sealed class Log4JXmlNamespaceResolver : IXmlNamespaceResolver
7+
{
8+
private const string Prefix = "log4j";
9+
private const string NamespaceName = "http://jakarta.apache.org/log4j/";
10+
11+
public static readonly Log4JXmlNamespaceResolver Instance = new();
12+
13+
/// <summary>
14+
/// The XML namespace used for Log4j events.
15+
/// </summary>
16+
/// <remarks>https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137</remarks>
17+
public static readonly XmlQualifiedName Log4JXmlNamespace = new(Prefix, NamespaceName);
18+
19+
private static readonly Dictionary<string, string> Namespaces = new() { [Prefix] = NamespaceName };
20+
21+
IDictionary<string, string> IXmlNamespaceResolver.GetNamespacesInScope(XmlNamespaceScope scope) => Namespaces;
22+
23+
string? IXmlNamespaceResolver.LookupNamespace(string prefix) => prefix == Prefix ? NamespaceName : null;
24+
25+
string? IXmlNamespaceResolver.LookupPrefix(string namespaceName) => namespaceName == NamespaceName ? Prefix : null;
26+
}

src/Log4NetTextFormatter.cs

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics.CodeAnalysis;
44
using System.IO;
55
using System.Linq;
6+
using System.Reflection;
67
using System.Text.RegularExpressions;
78
using System.Xml;
89
using Serilog.Core;
@@ -84,7 +85,7 @@ public Log4NetTextFormatter(Action<Log4NetTextFormatterOptionsBuilder>? configur
8485
var optionsBuilder = new Log4NetTextFormatterOptionsBuilder();
8586
configureOptions?.Invoke(optionsBuilder);
8687
_options = optionsBuilder.Build();
87-
_usesLog4JCompatibility = ReferenceEquals(Log4NetTextFormatterOptionsBuilder.Log4JXmlNamespace, _options.XmlNamespace);
88+
_usesLog4JCompatibility = ReferenceEquals(Log4JXmlNamespaceResolver.Log4JXmlNamespace, _options.XmlNamespace);
8889
}
8990

9091
/// <summary>
@@ -104,29 +105,17 @@ public void Format(LogEvent logEvent, TextWriter output)
104105
{
105106
throw new ArgumentNullException(nameof(output));
106107
}
107-
var xmlWriterOutput = _usesLog4JCompatibility ? new StringWriter() : output;
108-
using var writer = XmlWriter.Create(xmlWriterOutput, _options.XmlWriterSettings);
109-
WriteEvent(logEvent, writer);
110-
writer.Flush();
108+
using var writer = XmlWriter.Create(output, _options.XmlWriterSettings);
111109
if (_usesLog4JCompatibility)
112110
{
113-
// log4j writes the XML "manually", see https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137-L145
114-
// The resulting XML is impossible to write with a standard compliant XML writer such as XmlWriter.
115-
// That's why we write the event in a StringWriter then massage the output to remove the xmlns:log4j attribute to match log4j output.
116-
// The XML fragment becomes valid when surrounded by an external entity, see https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L31-L49
117-
const string log4JNamespaceAttribute = """
118-
xmlns:log4j="http://jakarta.apache.org/log4j/"
119-
""";
120-
var xmlString = ((StringWriter)xmlWriterOutput).ToString();
121-
var i = xmlString.IndexOf(log4JNamespaceAttribute, StringComparison.Ordinal);
122-
#if NETSTANDARD2_0
123-
output.Write(xmlString.Substring(0, i));
124-
output.Write(xmlString.Substring(i + log4JNamespaceAttribute.Length));
125-
#else
126-
output.Write(xmlString.AsSpan(0, i));
127-
output.Write(xmlString.AsSpan(i + log4JNamespaceAttribute.Length));
128-
#endif
111+
var predefinedNamespaces = writer.GetType().GetField("_predefinedNamespaces", BindingFlags.Instance | BindingFlags.NonPublic);
112+
if (predefinedNamespaces != null && typeof(IXmlNamespaceResolver).IsAssignableFrom(predefinedNamespaces.FieldType))
113+
{
114+
predefinedNamespaces.SetValue(writer, Log4JXmlNamespaceResolver.Instance);
115+
}
129116
}
117+
WriteEvent(logEvent, writer);
118+
writer.Flush();
130119
output.Write(_options.XmlWriterSettings.NewLineChars);
131120
}
132121

src/Log4NetTextFormatterOptionsBuilder.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ public class Log4NetTextFormatterOptionsBuilder
1515
/// <remarks>https://github.com/apache/logging-log4net/blob/rel/2.0.8/src/Layout/XmlLayout.cs#L49</remarks>
1616
private static readonly XmlQualifiedName Log4NetXmlNamespace = new("log4net", "http://logging.apache.org/log4net/schemas/log4net-events-1.2/");
1717

18-
/// <summary>
19-
/// The XML namespace used for Log4j events.
20-
/// </summary>
21-
/// <remarks>https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137</remarks>
22-
internal static readonly XmlQualifiedName Log4JXmlNamespace = new("log4j", "http://jakarta.apache.org/log4j/");
23-
2418
/// <summary>
2519
/// Initialize a new instance of the <see cref="Log4NetTextFormatterOptionsBuilder"/> class.
2620
/// </summary>
@@ -170,7 +164,7 @@ public void UseLog4JCompatibility()
170164
_lineEnding = LineEnding.CarriageReturn | LineEnding.LineFeed;
171165

172166
// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137
173-
_xmlNamespace = Log4JXmlNamespace;
167+
_xmlNamespace = Log4JXmlNamespaceResolver.Log4JXmlNamespace;
174168

175169
// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L147
176170
_cDataMode = CDataMode.Always;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using System.Xml;
5+
using FluentAssertions;
6+
using Xunit;
7+
8+
namespace Serilog.Formatting.Log4Net.Tests;
9+
10+
public class Log4JXmlNamespaceResolverTest
11+
{
12+
private readonly IXmlNamespaceResolver _resolver;
13+
14+
public Log4JXmlNamespaceResolverTest()
15+
{
16+
var log4JXmlNamespaceResolver = typeof(Log4NetTextFormatter).Assembly.GetType("Serilog.Formatting.Log4Net.Log4JXmlNamespaceResolver")
17+
?? throw new MissingMemberException("Serilog.Formatting.Log4Net.Log4JXmlNamespaceResolver");
18+
var instance = log4JXmlNamespaceResolver.GetField("Instance", BindingFlags.Public | BindingFlags.Static)
19+
?? throw new MissingFieldException("Serilog.Formatting.Log4Net.Log4JXmlNamespaceResolver", "Instance");
20+
_resolver = (IXmlNamespaceResolver)instance.GetValue(log4JXmlNamespaceResolver)!;
21+
}
22+
23+
[Theory]
24+
[InlineData(XmlNamespaceScope.All)]
25+
[InlineData(XmlNamespaceScope.ExcludeXml)]
26+
[InlineData(XmlNamespaceScope.Local)]
27+
public void GetNamespacesInScope(XmlNamespaceScope scope)
28+
{
29+
var expected = new Dictionary<string, string> { ["log4j"] = "http://jakarta.apache.org/log4j/" };
30+
_resolver.GetNamespacesInScope(scope).Should().BeEquivalentTo(expected);
31+
}
32+
33+
[Fact]
34+
public void LookupLog4JNamespace()
35+
{
36+
_resolver.LookupNamespace("log4j").Should().Be("http://jakarta.apache.org/log4j/");
37+
}
38+
39+
[Fact]
40+
public void LookupLog4JPrefix()
41+
{
42+
_resolver.LookupPrefix("http://jakarta.apache.org/log4j/").Should().Be("log4j");
43+
}
44+
45+
[Theory]
46+
[InlineData("xmlns")]
47+
[InlineData("xml")]
48+
public void LookupStandardXmlNamespace(string prefix)
49+
{
50+
_resolver.LookupNamespace(prefix).Should().BeNull();
51+
}
52+
53+
[Theory]
54+
[InlineData("http://www.w3.org/2000/xmlns/")]
55+
[InlineData("http://www.w3.org/XML/1998/namespace")]
56+
public void LookupStandardXmlPrefix(string namespaceName)
57+
{
58+
_resolver.LookupPrefix(namespaceName).Should().BeNull();
59+
}
60+
}

0 commit comments

Comments
 (0)