Skip to content

Commit 8f5b975

Browse files
committed
NoNamespaceXmlWriter (inconclusive) experiment
1 parent 7df550c commit 8f5b975

7 files changed

+115
-45
lines changed

src/IndentationSettings.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,20 @@ public IndentationSettings(Indentation indentation, byte size)
2121
{
2222
throw new ArgumentOutOfRangeException(nameof(size), size, $"The value of argument '{nameof(size)}' must be greater than 0.");
2323
}
24-
_indentationString = indentation switch
24+
(Character, _indentationString) = indentation switch
2525
{
26-
Indentation.Space => new string(c: ' ', size),
27-
Indentation.Tab => new string(c: '\t', size),
26+
Indentation.Space => (' ', new string(c: ' ', size)),
27+
Indentation.Tab => ('\t', new string(c: '\t', size)),
2828
_ => throw new ArgumentOutOfRangeException(nameof(indentation), indentation, $"The value of argument '{nameof(indentation)}' ({indentation}) is invalid for enum type '{nameof(Indentation)}'.")
2929
};
30+
Size = size;
3031
}
3132

3233
/// <summary>
3334
/// Returns a string representation of the indentation settings.
3435
/// </summary>
3536
public override string ToString() => _indentationString;
37+
38+
internal char Character { get; }
39+
internal byte Size { get; }
3640
}

src/Log4NetTextFormatter.cs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -104,30 +104,10 @@ public void Format(LogEvent logEvent, TextWriter output)
104104
{
105105
throw new ArgumentNullException(nameof(output));
106106
}
107-
var xmlWriterOutput = _usesLog4JCompatibility ? new StringWriter() : output;
108-
using var writer = XmlWriter.Create(xmlWriterOutput, _options.XmlWriterSettings);
107+
using var writer = _options.CreateXmlWriter(output, _usesLog4JCompatibility);
109108
WriteEvent(logEvent, writer);
110109
writer.Flush();
111-
if (_usesLog4JCompatibility)
112-
{
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
129-
}
130-
output.Write(_options.XmlWriterSettings.NewLineChars);
110+
output.Write(_options.NewLineChars);
131111
}
132112

133113
/// <summary>

src/Log4NetTextFormatterOptions.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23
using System.Xml;
34

45
namespace Serilog.Formatting.Log4Net;
@@ -8,12 +9,13 @@ namespace Serilog.Formatting.Log4Net;
89
/// </summary>
910
internal sealed class Log4NetTextFormatterOptions
1011
{
11-
internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode cDataMode, XmlQualifiedName? xmlNamespace, XmlWriterSettings xmlWriterSettings, PropertyFilter filterProperty, ExceptionFormatter formatException)
12+
internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode cDataMode, XmlQualifiedName? xmlNamespace, LineEnding lineEnding, IndentationSettings? indentationSettings, PropertyFilter filterProperty, ExceptionFormatter formatException)
1213
{
1314
FormatProvider = formatProvider;
1415
CDataMode = cDataMode;
1516
XmlNamespace = xmlNamespace;
16-
XmlWriterSettings = xmlWriterSettings;
17+
NewLineChars = lineEnding.ToCharacters();
18+
IndentationSettings = indentationSettings;
1719
FilterProperty = filterProperty;
1820
FormatException = formatException;
1921
}
@@ -27,12 +29,42 @@ internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode
2729
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseNoXmlNamespace"/></summary>
2830
internal XmlQualifiedName? XmlNamespace { get; }
2931

30-
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.CreateXmlWriterSettings"/></summary>
31-
internal XmlWriterSettings XmlWriterSettings { get; }
32+
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseLineEnding"/></summary>
33+
internal string NewLineChars { get; }
34+
35+
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseIndentationSettings"/></summary>
36+
private IndentationSettings? IndentationSettings { get; }
3237

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

3641
/// <summary>See <see cref="Log4NetTextFormatterOptionsBuilder.UseExceptionFormatter"/></summary>
3742
internal ExceptionFormatter FormatException { get; }
43+
44+
internal XmlWriter CreateXmlWriter(TextWriter output, bool useLog4JCompatibility)
45+
{
46+
if (useLog4JCompatibility)
47+
{
48+
var xmlWriter = new NoNamespaceXmlWriter(output, Log4NetTextFormatterOptionsBuilder.Log4JXmlNamespace);
49+
if (IndentationSettings != null)
50+
{
51+
xmlWriter.Formatting = System.Xml.Formatting.Indented;
52+
xmlWriter.IndentChar = IndentationSettings.Character;
53+
xmlWriter.Indentation = IndentationSettings.Size;
54+
}
55+
return xmlWriter;
56+
}
57+
58+
var settings = new XmlWriterSettings
59+
{
60+
Indent = IndentationSettings is not null,
61+
NewLineChars = NewLineChars,
62+
ConformanceLevel = ConformanceLevel.Fragment,
63+
};
64+
if (IndentationSettings is not null)
65+
{
66+
settings.IndentChars = IndentationSettings.ToString();
67+
}
68+
return XmlWriter.Create(output, settings);
69+
}
3870
}

src/Log4NetTextFormatterOptionsBuilder.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -177,22 +177,7 @@ public void UseLog4JCompatibility()
177177
}
178178

179179
internal Log4NetTextFormatterOptions Build()
180-
=> new(_formatProvider, _cDataMode, _xmlNamespace, CreateXmlWriterSettings(_lineEnding, _indentationSettings), _filterProperty, _formatException);
181-
182-
private static XmlWriterSettings CreateXmlWriterSettings(LineEnding lineEnding, IndentationSettings? indentationSettings)
183-
{
184-
var xmlWriterSettings = new XmlWriterSettings
185-
{
186-
Indent = indentationSettings is not null,
187-
NewLineChars = lineEnding.ToCharacters(),
188-
ConformanceLevel = ConformanceLevel.Fragment,
189-
};
190-
if (indentationSettings is not null)
191-
{
192-
xmlWriterSettings.IndentChars = indentationSettings.ToString();
193-
}
194-
return xmlWriterSettings;
195-
}
180+
=> new(_formatProvider, _cDataMode, _xmlNamespace, _lineEnding, _indentationSettings, _filterProperty, _formatException);
196181
}
197182

198183
/// <summary>

src/NoNamespaceXmlWriter.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.IO;
2+
using System.Xml;
3+
4+
namespace Serilog.Formatting.Log4Net;
5+
6+
// 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.
7+
internal sealed class NoNamespaceXmlWriter : XmlTextWriter
8+
{
9+
private readonly XmlQualifiedName _ns;
10+
private bool _skipAttribute;
11+
12+
// 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
13+
// The resulting XML is impossible to write with a standard compliant XML writer such as XmlWriter.
14+
// That's why we write the event in a StringWriter then massage the output to remove the xmlns:log4j attribute to match log4j output.
15+
// 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
16+
public NoNamespaceXmlWriter(TextWriter output, XmlQualifiedName ns) : base(output)
17+
{
18+
_ns = ns;
19+
}
20+
21+
public override void WriteEndAttribute()
22+
{
23+
if (_skipAttribute)
24+
{
25+
_skipAttribute = false;
26+
}
27+
else
28+
{
29+
base.WriteEndAttribute();
30+
}
31+
}
32+
33+
public override void WriteStartAttribute(string? prefix, string localName, string? ns)
34+
{
35+
// Actually that's not how writing XML namespaces work...
36+
if (prefix == "xmlns" && localName == _ns.Name)
37+
{
38+
_skipAttribute = true;
39+
}
40+
else
41+
{
42+
base.WriteStartAttribute(prefix, localName, ns);
43+
}
44+
}
45+
46+
public override void WriteString(string? text)
47+
{
48+
if (!_skipAttribute)
49+
base.WriteString(text);
50+
}
51+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<log4j:event timestamp="1041689366535" level="INFO">
2+
<log4j:message><![CDATA[Hello from Serilog]]></log4j:message>
3+
</log4j:event>

tests/Log4NetTextFormatterTest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,21 @@ public Task Log4JCompatibility(bool useStaticInstance)
324324
return Verify(output).DisableRequireUniquePrefix();
325325
}
326326

327+
[Fact]
328+
public Task BasicLog4J()
329+
{
330+
// Arrange
331+
using var output = new StringWriter();
332+
var logEvent = CreateLogEvent();
333+
var formatter = new Log4NetTextFormatter(c => c.UseLog4JCompatibility());
334+
335+
// Act
336+
formatter.Format(logEvent, output);
337+
338+
// Assert
339+
return Verify(output).DisableRequireUniquePrefix();
340+
}
341+
327342
[Fact]
328343
public Task ExplicitFormatProvider()
329344
{

0 commit comments

Comments
 (0)