diff --git a/src/IndentationSettings.cs b/src/IndentationSettings.cs index b050ba1..8140945 100644 --- a/src/IndentationSettings.cs +++ b/src/IndentationSettings.cs @@ -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; } /// /// Returns a string representation of the indentation settings. /// public override string ToString() => _indentationString; + + internal char Character { get; } + internal byte Size { get; } } \ No newline at end of file diff --git a/src/Log4NetTextFormatter.cs b/src/Log4NetTextFormatter.cs index a5099fe..1abeb5d 100644 --- a/src/Log4NetTextFormatter.cs +++ b/src/Log4NetTextFormatter.cs @@ -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); } /// diff --git a/src/Log4NetTextFormatterOptions.cs b/src/Log4NetTextFormatterOptions.cs index 80f7ad5..0d7119b 100644 --- a/src/Log4NetTextFormatterOptions.cs +++ b/src/Log4NetTextFormatterOptions.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Xml; namespace Serilog.Formatting.Log4Net; @@ -8,12 +9,13 @@ namespace Serilog.Formatting.Log4Net; /// 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; } @@ -27,12 +29,42 @@ internal Log4NetTextFormatterOptions(IFormatProvider? formatProvider, CDataMode /// See internal XmlQualifiedName? XmlNamespace { get; } - /// See - internal XmlWriterSettings XmlWriterSettings { get; } + /// See + internal string NewLineChars { get; } + + /// See + private IndentationSettings? IndentationSettings { get; } /// See internal PropertyFilter FilterProperty { get; } /// See 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); + } } \ No newline at end of file diff --git a/src/Log4NetTextFormatterOptionsBuilder.cs b/src/Log4NetTextFormatterOptionsBuilder.cs index 55d0a06..63cc846 100644 --- a/src/Log4NetTextFormatterOptionsBuilder.cs +++ b/src/Log4NetTextFormatterOptionsBuilder.cs @@ -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); } /// diff --git a/src/NoNamespaceXmlWriter.cs b/src/NoNamespaceXmlWriter.cs new file mode 100644 index 0000000..da82855 --- /dev/null +++ b/src/NoNamespaceXmlWriter.cs @@ -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); + } +} \ No newline at end of file diff --git a/tests/Log4NetTextFormatterTest.BasicLog4J.verified.xml b/tests/Log4NetTextFormatterTest.BasicLog4J.verified.xml new file mode 100644 index 0000000..81df489 --- /dev/null +++ b/tests/Log4NetTextFormatterTest.BasicLog4J.verified.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/Log4NetTextFormatterTest.cs b/tests/Log4NetTextFormatterTest.cs index 97ff4d5..63480fa 100644 --- a/tests/Log4NetTextFormatterTest.cs +++ b/tests/Log4NetTextFormatterTest.cs @@ -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() {