Skip to content

Commit

Permalink
NoNamespaceXmlWriter (inconclusive) experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed Jul 11, 2024
1 parent 7df550c commit 8f5b975
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 45 deletions.
10 changes: 7 additions & 3 deletions src/IndentationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ public IndentationSettings(Indentation indentation, byte size)
{
throw new ArgumentOutOfRangeException(nameof(size), size, $"The value of argument '{nameof(size)}' must be greater than 0.");
}
_indentationString = indentation switch
(Character, _indentationString) = indentation switch
{
Indentation.Space => new string(c: ' ', size),
Indentation.Tab => new string(c: '\t', size),
Indentation.Space => (' ', new string(c: ' ', size)),
Indentation.Tab => ('\t', new string(c: '\t', size)),
_ => throw new ArgumentOutOfRangeException(nameof(indentation), indentation, $"The value of argument '{nameof(indentation)}' ({indentation}) is invalid for enum type '{nameof(Indentation)}'.")
};
Size = size;
}

/// <summary>
/// Returns a string representation of the indentation settings.
/// </summary>
public override string ToString() => _indentationString;

internal char Character { get; }
internal byte Size { get; }
}
24 changes: 2 additions & 22 deletions src/Log4NetTextFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,30 +104,10 @@ 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);
using var writer = _options.CreateXmlWriter(output, _usesLog4JCompatibility);
WriteEvent(logEvent, writer);
writer.Flush();
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
}
output.Write(_options.XmlWriterSettings.NewLineChars);
output.Write(_options.NewLineChars);
}

/// <summary>
Expand Down
40 changes: 36 additions & 4 deletions src/Log4NetTextFormatterOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Xml;

namespace Serilog.Formatting.Log4Net;
Expand All @@ -8,12 +9,13 @@ namespace Serilog.Formatting.Log4Net;
/// </summary>
internal sealed class Log4NetTextFormatterOptions
{
internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode cDataMode, XmlQualifiedName? xmlNamespace, XmlWriterSettings xmlWriterSettings, PropertyFilter filterProperty, ExceptionFormatter formatException)
internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode cDataMode, XmlQualifiedName? xmlNamespace, LineEnding lineEnding, IndentationSettings? indentationSettings, PropertyFilter filterProperty, ExceptionFormatter formatException)
{
FormatProvider = formatProvider;
CDataMode = cDataMode;
XmlNamespace = xmlNamespace;
XmlWriterSettings = xmlWriterSettings;
NewLineChars = lineEnding.ToCharacters();
IndentationSettings = indentationSettings;
FilterProperty = filterProperty;
FormatException = formatException;
}
Expand All @@ -27,12 +29,42 @@ internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseNoXmlNamespace"/></summary>
internal XmlQualifiedName? XmlNamespace { get; }

/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.CreateXmlWriterSettings"/></summary>
internal XmlWriterSettings XmlWriterSettings { get; }
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseLineEnding"/></summary>
internal string NewLineChars { get; }

/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseIndentationSettings"/></summary>
private IndentationSettings? IndentationSettings { get; }

/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UsePropertyFilter"/></summary>
internal PropertyFilter FilterProperty { get; }

/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseExceptionFormatter"/></summary>
internal ExceptionFormatter FormatException { get; }

internal XmlWriter CreateXmlWriter(TextWriter output, bool useLog4JCompatibility)
{
if (useLog4JCompatibility)
{
var xmlWriter = new NoNamespaceXmlWriter(output, Log4NetTextFormatterOptionsBuilder.Log4JXmlNamespace);
if (IndentationSettings != null)
{
xmlWriter.Formatting = System.Xml.Formatting.Indented;
xmlWriter.IndentChar = IndentationSettings.Character;
xmlWriter.Indentation = IndentationSettings.Size;
}
return xmlWriter;
}

var settings = new XmlWriterSettings
{
Indent = IndentationSettings is not null,
NewLineChars = NewLineChars,
ConformanceLevel = ConformanceLevel.Fragment,
};
if (IndentationSettings is not null)
{
settings.IndentChars = IndentationSettings.ToString();
}
return XmlWriter.Create(output, settings);
}
}
17 changes: 1 addition & 16 deletions src/Log4NetTextFormatterOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,22 +177,7 @@ public void UseLog4JCompatibility()
}

internal Log4NetTextFormatterOptions Build()
=> new(_formatProvider, _cDataMode, _xmlNamespace, CreateXmlWriterSettings(_lineEnding, _indentationSettings), _filterProperty, _formatException);

private static XmlWriterSettings CreateXmlWriterSettings(LineEnding lineEnding, IndentationSettings? indentationSettings)
{
var xmlWriterSettings = new XmlWriterSettings
{
Indent = indentationSettings is not null,
NewLineChars = lineEnding.ToCharacters(),
ConformanceLevel = ConformanceLevel.Fragment,
};
if (indentationSettings is not null)
{
xmlWriterSettings.IndentChars = indentationSettings.ToString();
}
return xmlWriterSettings;
}
=> new(_formatProvider, _cDataMode, _xmlNamespace, _lineEnding, _indentationSettings, _filterProperty, _formatException);
}

/// <summary>
Expand Down
51 changes: 51 additions & 0 deletions src/NoNamespaceXmlWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.IO;
using System.Xml;

namespace Serilog.Formatting.Log4Net;

// Inspired by https://www.hanselman.com/blog/xmlfragmentwriter-omiting-the-xml-declaration-and-the-xsd-and-xsi-namespaces but does not actually work since the xmlns stack can't be manipulated that easily.
internal sealed class NoNamespaceXmlWriter : XmlTextWriter
{
private readonly XmlQualifiedName _ns;
private bool _skipAttribute;

// 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
public NoNamespaceXmlWriter(TextWriter output, XmlQualifiedName ns) : base(output)
{
_ns = ns;
}

public override void WriteEndAttribute()
{
if (_skipAttribute)
{
_skipAttribute = false;
}
else
{
base.WriteEndAttribute();
}
}

public override void WriteStartAttribute(string? prefix, string localName, string? ns)
{
// Actually that's not how writing XML namespaces work...
if (prefix == "xmlns" && localName == _ns.Name)
{
_skipAttribute = true;
}
else
{
base.WriteStartAttribute(prefix, localName, ns);
}
}

public override void WriteString(string? text)
{
if (!_skipAttribute)
base.WriteString(text);
}
}
3 changes: 3 additions & 0 deletions tests/Log4NetTextFormatterTest.BasicLog4J.verified.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<log4j:event timestamp="1041689366535" level="INFO">
<log4j:message><![CDATA[Hello from Serilog]]></log4j:message>
</log4j:event>
15 changes: 15 additions & 0 deletions tests/Log4NetTextFormatterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,21 @@ public Task Log4JCompatibility(bool useStaticInstance)
return Verify(output).DisableRequireUniquePrefix();
}

[Fact]
public Task BasicLog4J()
{
// Arrange
using var output = new StringWriter();
var logEvent = CreateLogEvent();
var formatter = new Log4NetTextFormatter(c => c.UseLog4JCompatibility());

// Act
formatter.Format(logEvent, output);

// Assert
return Verify(output).DisableRequireUniquePrefix();
}

[Fact]
public Task ExplicitFormatProvider()
{
Expand Down

0 comments on commit 8f5b975

Please sign in to comment.