Skip to content

Commit

Permalink
Log4JXmlNamespaceResolver hack
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed Jul 12, 2024
1 parent 7df550c commit 82f331f
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 28 deletions.
26 changes: 26 additions & 0 deletions src/Log4JXmlNamespaceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Xml;

namespace Serilog.Formatting.Log4Net;

internal sealed class Log4JXmlNamespaceResolver : IXmlNamespaceResolver
{
private const string Prefix = "log4j";
private const string NamespaceName = "http://jakarta.apache.org/log4j/";

public static readonly Log4JXmlNamespaceResolver Instance = new();

/// <summary>
/// The XML namespace used for Log4j events.
/// </summary>
/// <remarks>https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137</remarks>
public static readonly XmlQualifiedName Log4JXmlNamespace = new(Prefix, NamespaceName);

private static readonly Dictionary<string, string> Namespaces = new() { [Prefix] = NamespaceName };

IDictionary<string, string> IXmlNamespaceResolver.GetNamespacesInScope(XmlNamespaceScope scope) => Namespaces;

string? IXmlNamespaceResolver.LookupNamespace(string prefix) => prefix == Prefix ? NamespaceName : null;

string? IXmlNamespaceResolver.LookupPrefix(string namespaceName) => namespaceName == NamespaceName ? Prefix : null;
}
31 changes: 10 additions & 21 deletions src/Log4NetTextFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml;
using Serilog.Core;
Expand Down Expand Up @@ -84,7 +85,7 @@ public Log4NetTextFormatter(Action<Log4NetTextFormatterOptionsBuilder>? configur
var optionsBuilder = new Log4NetTextFormatterOptionsBuilder();
configureOptions?.Invoke(optionsBuilder);
_options = optionsBuilder.Build();
_usesLog4JCompatibility = ReferenceEquals(Log4NetTextFormatterOptionsBuilder.Log4JXmlNamespace, _options.XmlNamespace);
_usesLog4JCompatibility = ReferenceEquals(Log4JXmlNamespaceResolver.Log4JXmlNamespace, _options.XmlNamespace);
}

/// <summary>
Expand All @@ -104,29 +105,17 @@ public void Format(LogEvent logEvent, TextWriter output)
{
throw new ArgumentNullException(nameof(output));
}
var xmlWriterOutput = _usesLog4JCompatibility ? new StringWriter() : output;
using var writer = XmlWriter.Create(xmlWriterOutput, _options.XmlWriterSettings);
WriteEvent(logEvent, writer);
writer.Flush();
using var writer = XmlWriter.Create(output, _options.XmlWriterSettings);
if (_usesLog4JCompatibility)
{
// 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
// The resulting XML is impossible to write with a standard compliant XML writer such as XmlWriter.
// That's why we write the event in a StringWriter then massage the output to remove the xmlns:log4j attribute to match log4j output.
// 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
const string log4JNamespaceAttribute = """
xmlns:log4j="http://jakarta.apache.org/log4j/"
""";
var xmlString = ((StringWriter)xmlWriterOutput).ToString();
var i = xmlString.IndexOf(log4JNamespaceAttribute, StringComparison.Ordinal);
#if NETSTANDARD2_0
output.Write(xmlString.Substring(0, i));
output.Write(xmlString.Substring(i + log4JNamespaceAttribute.Length));
#else
output.Write(xmlString.AsSpan(0, i));
output.Write(xmlString.AsSpan(i + log4JNamespaceAttribute.Length));
#endif
var predefinedNamespaces = writer.GetType().GetField("_predefinedNamespaces", BindingFlags.Instance | BindingFlags.NonPublic);
if (predefinedNamespaces != null && typeof(IXmlNamespaceResolver).IsAssignableFrom(predefinedNamespaces.FieldType))
{
predefinedNamespaces.SetValue(writer, Log4JXmlNamespaceResolver.Instance);
}
}
WriteEvent(logEvent, writer);
writer.Flush();
output.Write(_options.XmlWriterSettings.NewLineChars);
}

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

/// <summary>
/// The XML namespace used for Log4j events.
/// </summary>
/// <remarks>https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137</remarks>
internal static readonly XmlQualifiedName Log4JXmlNamespace = new("log4j", "http://jakarta.apache.org/log4j/");

/// <summary>
/// Initialize a new instance of the <see cref="Log4NetTextFormatterOptionsBuilder"/> class.
/// </summary>
Expand Down Expand Up @@ -170,7 +164,7 @@ public void UseLog4JCompatibility()
_lineEnding = LineEnding.CarriageReturn | LineEnding.LineFeed;

// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L137
_xmlNamespace = Log4JXmlNamespace;
_xmlNamespace = Log4JXmlNamespaceResolver.Log4JXmlNamespace;

// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L147
_cDataMode = CDataMode.Always;
Expand Down
60 changes: 60 additions & 0 deletions tests/Log4JXmlNamespaceResolverTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml;
using FluentAssertions;
using Xunit;

namespace Serilog.Formatting.Log4Net.Tests;

public class Log4JXmlNamespaceResolverTest
{
private readonly IXmlNamespaceResolver _resolver;

public Log4JXmlNamespaceResolverTest()
{
var log4JXmlNamespaceResolver = typeof(Log4NetTextFormatter).Assembly.GetType("Serilog.Formatting.Log4Net.Log4JXmlNamespaceResolver")
?? throw new MissingMemberException("Serilog.Formatting.Log4Net.Log4JXmlNamespaceResolver");
var instance = log4JXmlNamespaceResolver.GetField("Instance", BindingFlags.Public | BindingFlags.Static)
?? throw new MissingFieldException("Serilog.Formatting.Log4Net.Log4JXmlNamespaceResolver", "Instance");
_resolver = (IXmlNamespaceResolver)instance.GetValue(log4JXmlNamespaceResolver)!;
}

[Theory]
[InlineData(XmlNamespaceScope.All)]
[InlineData(XmlNamespaceScope.ExcludeXml)]
[InlineData(XmlNamespaceScope.Local)]
public void GetNamespacesInScope(XmlNamespaceScope scope)
{
var expected = new Dictionary<string, string> { ["log4j"] = "http://jakarta.apache.org/log4j/" };
_resolver.GetNamespacesInScope(scope).Should().BeEquivalentTo(expected);
}

[Fact]
public void LookupLog4JNamespace()
{
_resolver.LookupNamespace("log4j").Should().Be("http://jakarta.apache.org/log4j/");
}

[Fact]
public void LookupLog4JPrefix()
{
_resolver.LookupPrefix("http://jakarta.apache.org/log4j/").Should().Be("log4j");
}

[Theory]
[InlineData("xmlns")]
[InlineData("xml")]
public void LookupStandardXmlNamespace(string prefix)
{
_resolver.LookupNamespace(prefix).Should().BeNull();
}

[Theory]
[InlineData("http://www.w3.org/2000/xmlns/")]
[InlineData("http://www.w3.org/XML/1998/namespace")]
public void LookupStandardXmlPrefix(string namespaceName)
{
_resolver.LookupPrefix(namespaceName).Should().BeNull();
}
}

0 comments on commit 82f331f

Please sign in to comment.