diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a2787..6995179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ This project adheres to [Semantic Versioning](http://semver.org/) and is followi ### :syringe: Fixed - [#42](https://github.com/FantasticFiasco/serilog-sinks-udp/pull/42) Revert to support IPv4 on networks without IPv6 (contribution by [brettdavis-bmw](https://github.com/brettdavis-bmw)) -- [#45](https://github.com/FantasticFiasco/serilog-sinks-udp/pull/45) Correctly XML escape exception message serialized by `Log4jTextFormatter` +- [#45](https://github.com/FantasticFiasco/serilog-sinks-udp/issues/45) Correctly XML escape exception message serialized by `Log4jTextFormatter` +- [#51](https://github.com/FantasticFiasco/serilog-sinks-udp/issues/51) Correctly XML escape all properties serialized by `Log4jTextFormatter` and `Log4netTextFormatter` ### :dizzy: Changed diff --git a/src/Serilog.Sinks.Udp/Sinks/Udp/Private/RemoteEndPoint.cs b/src/Serilog.Sinks.Udp/Sinks/Udp/Private/RemoteEndPoint.cs index 5c03bf1..9d844c4 100644 --- a/src/Serilog.Sinks.Udp/Sinks/Udp/Private/RemoteEndPoint.cs +++ b/src/Serilog.Sinks.Udp/Sinks/Udp/Private/RemoteEndPoint.cs @@ -36,6 +36,14 @@ public RemoteEndPoint(string address, int port) public int Port { get; } + /// + /// It's a very small performance optimization to parse the IP address and use it instead + /// of having the HTTP client trying to resolve the address and figure out that it isn't a + /// hostname at all but instead an ordinary IP address. + /// + /// A small optimization indeed, but one that was requested by one of the consumers of the + /// package. + /// public IPEndPoint IPEndPoint { get; } } } diff --git a/src/Serilog.Sinks.Udp/Sinks/Udp/Private/XmlSerializer.cs b/src/Serilog.Sinks.Udp/Sinks/Udp/Private/XmlSerializer.cs new file mode 100644 index 0000000..3dd65df --- /dev/null +++ b/src/Serilog.Sinks.Udp/Sinks/Udp/Private/XmlSerializer.cs @@ -0,0 +1,115 @@ +// Copyright 2015-2019 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.IO; +using System.Text; + +namespace Serilog.Sinks.Udp.Private +{ + /// + /// The methods in this class where influenced by + /// https://weblog.west-wind.com/posts/2018/Nov/30/Returning-an-XML-Encoded-String-in-NET. + /// + internal class XmlSerializer + { + private const char LtCharacter = '<'; + private const char GtCharacter = '>'; + private const char AmpCharacter = '&'; + private const char QuotCharacter = '\"'; + private const char AposCharacter = '\''; + private const char LfCharacter = '\n'; + private const char CrCharacter = '\r'; + private const char TabCharacter = '\t'; + + private const string LfString = "\n"; + private const string CrString = "\r"; + private const string TabString = "\t"; + + private const string SerializedLt = "<"; + private const string SerializedGt = ">"; + private const string SerializedAmp = "&"; + private const string SerializedQuot = """; + private const string SerializedApos = "'"; + private const string SerializedLf = " "; + private const string SerializedCr = " "; + private const string SerializedTab = " "; + + internal void SerializeXmlValue(TextWriter output, string text, bool isAttribute) + { + foreach (var character in text) + { + output.Write(SerializeXmlValue(character, isAttribute)); + } + } + + internal string SerializeXmlValue(string text, bool isAttribute) + { + var builder = new StringBuilder(); + + foreach (var character in text) + { + builder.Append(SerializeXmlValue(character, isAttribute)); + } + + return builder.ToString(); + } + + private static string SerializeXmlValue(char character, bool isAttribute) + { + if (character == LtCharacter) + { + return SerializedLt; + } + + if (character == GtCharacter) + { + return SerializedGt; + } + + if (character == AmpCharacter) + { + return SerializedAmp; + } + + // Special handling for quotes + if (isAttribute && character == QuotCharacter) + { + return SerializedQuot; + } + + if (isAttribute && character == AposCharacter) + { + return SerializedApos; + } + + // Legal sub-chr32 characters + if (character == LfCharacter) + { + return isAttribute ? SerializedLf : LfString; + } + + if (character == CrCharacter) + { + return isAttribute ? SerializedCr : CrString; + } + + if (character == TabCharacter) + { + return isAttribute ? SerializedTab : TabString; + } + + return character.ToString(); + } + } +} diff --git a/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4jTextFormatter.cs b/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4jTextFormatter.cs index 949d127..f51f663 100644 --- a/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4jTextFormatter.cs +++ b/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4jTextFormatter.cs @@ -1,11 +1,11 @@ // Copyright 2015-2019 Serilog Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,17 +16,28 @@ using Serilog.Formatting; using System.IO; using System.Xml; +using Serilog.Sinks.Udp.Private; namespace Serilog.Sinks.Udp.TextFormatters { /// - /// Text formatter serializing log events into log4j complient XML. + /// Text formatter serializing log events into log4j compliant XML. /// public class Log4jTextFormatter : ITextFormatter { private static readonly string SourceContextPropertyName = "SourceContext"; private static readonly string ThreadIdPropertyName = "ThreadId"; + private readonly XmlSerializer xmlSerializer; + + /// + /// Initializes a new instance of the class. + /// + public Log4jTextFormatter() + { + xmlSerializer = new XmlSerializer(); + } + /// /// Format the log event into the output. /// @@ -49,11 +60,12 @@ public void Format(LogEvent logEvent, TextWriter output) output.Write(""); } - private static void WriteLogger(LogEvent logEvent, TextWriter output) + private void WriteLogger(LogEvent logEvent, TextWriter output) { - if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out LogEventPropertyValue sourceContext)) + if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out var sourceContext)) { - output.Write($" logger=\"{((ScalarValue)sourceContext).Value}\""); + var sourceContextValue = ((ScalarValue)sourceContext).Value.ToString(); + output.Write($" logger=\"{xmlSerializer.SerializeXmlValue(sourceContextValue, true)}\""); } } @@ -98,22 +110,23 @@ private static void WriteLevel(LogEvent logEvent, TextWriter output) output.Write($" level=\"{level}\""); } - private static void WriteThread(LogEvent logEvent, TextWriter output) + private void WriteThread(LogEvent logEvent, TextWriter output) { - if (logEvent.Properties.TryGetValue(ThreadIdPropertyName, out LogEventPropertyValue threadId)) + if (logEvent.Properties.TryGetValue(ThreadIdPropertyName, out var threadId)) { - output.Write($" thread=\"{((ScalarValue)threadId).Value}\""); + var threadIdValue = ((ScalarValue)threadId).Value.ToString(); + output.Write($" thread=\"{xmlSerializer.SerializeXmlValue(threadIdValue, true)}\""); } } - private static void WriteMessage(LogEvent logEvent, TextWriter output) + private void WriteMessage(LogEvent logEvent, TextWriter output) { output.Write(""); - logEvent.RenderMessage(output); + xmlSerializer.SerializeXmlValue(output, logEvent.RenderMessage(), false); output.Write(""); } - private static void WriteException(LogEvent logEvent, TextWriter output) + private void WriteException(LogEvent logEvent, TextWriter output) { if (logEvent.Exception == null) { @@ -121,39 +134,8 @@ private static void WriteException(LogEvent logEvent, TextWriter output) } output.Write(""); - EscapeXmlPropertyValue(output, logEvent.Exception.ToString()); + xmlSerializer.SerializeXmlValue(output, logEvent.Exception.ToString(), false); output.Write(""); } - - /// - /// This method has been influenced by - /// https://weblog.west-wind.com/posts/2018/Nov/30/Returning-an-XML-Encoded-String-in-NET - /// and does not support XML attribute values. The method in the article does, but this - /// doesn't. - /// - private static void EscapeXmlPropertyValue(TextWriter output, string text) - { - foreach (var character in text) - { - switch (character) - { - case '<': - output.Write("<"); - break; - - case '>': - output.Write(">"); - break; - - case '&': - output.Write("&"); - break; - - default: - output.Write(character); - break; - } - } - } } } diff --git a/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4netTextFormatter.cs b/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4netTextFormatter.cs index 32d08e9..2c5d33b 100644 --- a/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4netTextFormatter.cs +++ b/src/Serilog.Sinks.Udp/Sinks/Udp/TextFormatters/Log4netTextFormatter.cs @@ -1,11 +1,11 @@ // Copyright 2015-2019 Serilog Contributors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,6 +15,7 @@ using Serilog.Events; using Serilog.Formatting; using System.IO; +using Serilog.Sinks.Udp.Private; namespace Serilog.Sinks.Udp.TextFormatters { @@ -25,10 +26,20 @@ public class Log4netTextFormatter : ITextFormatter { private static readonly string SourceContextPropertyName = "SourceContext"; private static readonly string ThreadIdPropertyName = "ThreadId"; - private static readonly string MachineNamePropertyName = "MachineName"; private static readonly string UserNamePropertyName = "EnvironmentUserName"; - private static readonly string MethodPropertyName = "Method"; private static readonly string ProcessNamePropertyName = "ProcessName"; + private static readonly string MethodPropertyName = "Method"; + private static readonly string MachineNamePropertyName = "MachineName"; + + private readonly XmlSerializer xmlSerializer; + + /// + /// Initializes a new instance of the class. + /// + public Log4netTextFormatter() + { + xmlSerializer = new XmlSerializer(); + } /// /// Format the log event into the output. @@ -62,51 +73,12 @@ public void Format(LogEvent logEvent, TextWriter output) output.Write(""); } - private static void WriteProcessName(LogEvent logEvent, TextWriter output) - { - if (logEvent.Properties.TryGetValue(ProcessNamePropertyName, out LogEventPropertyValue processName)) - { - output.Write($" domain=\"{((ScalarValue)processName).Value}\""); - } - } - - private static void WriteLocationInfoClass(LogEvent logEvent, TextWriter output) - { - if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out LogEventPropertyValue sourceContext)) - { - output.Write($" class=\"{((ScalarValue)sourceContext).Value}\""); - } - } - - private static void WriteLocationInfoMethod(LogEvent logEvent, TextWriter output) - { - if (logEvent.Properties.TryGetValue(MethodPropertyName, out LogEventPropertyValue methodName)) - { - output.Write($" method=\"{((ScalarValue)methodName).Value}\""); - } - } - - private static void WriteLogger(LogEvent logEvent, TextWriter output) - { - if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out LogEventPropertyValue sourceContext)) - { - output.Write($" logger=\"{((ScalarValue)sourceContext).Value}\""); - } - } - - private static void WriteUserName(LogEvent logEvent, TextWriter output) + private void WriteLogger(LogEvent logEvent, TextWriter output) { - if (logEvent.Properties.TryGetValue(UserNamePropertyName, out LogEventPropertyValue userName)) + if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out var sourceContext)) { - output.Write($" username=\"{((ScalarValue)userName).Value}\""); - } - } - - private static void WriteHostName(LogEvent logEvent, TextWriter output) - { - if (logEvent.Properties.TryGetValue(MachineNamePropertyName, out LogEventPropertyValue machineName)) - { - output.Write($" "); + var sourceContextValue = ((ScalarValue)sourceContext).Value.ToString(); + output.Write($" logger=\"{xmlSerializer.SerializeXmlValue(sourceContextValue, true)}\""); } } @@ -150,29 +122,77 @@ private static void WriteLevel(LogEvent logEvent, TextWriter output) output.Write($" level=\"{level}\""); } - private static void WriteThread(LogEvent logEvent, TextWriter output) + private void WriteThread(LogEvent logEvent, TextWriter output) + { + if (logEvent.Properties.TryGetValue(ThreadIdPropertyName, out var threadId)) + { + var threadIdValue = ((ScalarValue)threadId).Value.ToString(); + output.Write($" thread=\"{xmlSerializer.SerializeXmlValue(threadIdValue, true)}\""); + } + } + + private void WriteUserName(LogEvent logEvent, TextWriter output) + { + if (logEvent.Properties.TryGetValue(UserNamePropertyName, out var userName)) + { + var userNameValue = ((ScalarValue)userName).Value.ToString(); + output.Write($" username=\"{xmlSerializer.SerializeXmlValue(userNameValue, true)}\""); + } + } + + private void WriteProcessName(LogEvent logEvent, TextWriter output) + { + if (logEvent.Properties.TryGetValue(ProcessNamePropertyName, out var processName)) + { + var processNameValue = ((ScalarValue)processName).Value.ToString(); + output.Write($" domain=\"{xmlSerializer.SerializeXmlValue(processNameValue, true)}\""); + } + } + + private void WriteLocationInfoClass(LogEvent logEvent, TextWriter output) + { + if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out var sourceContext)) + { + var sourceContextValue = ((ScalarValue)sourceContext).Value.ToString(); + output.Write($" class=\"{xmlSerializer.SerializeXmlValue(sourceContextValue, true)}\""); + } + } + + private void WriteLocationInfoMethod(LogEvent logEvent, TextWriter output) + { + if (logEvent.Properties.TryGetValue(MethodPropertyName, out var methodName)) + { + var methodNameValue = ((ScalarValue)methodName).Value.ToString(); + output.Write($" method=\"{xmlSerializer.SerializeXmlValue(methodNameValue, true)}\""); + } + } + + private void WriteHostName(LogEvent logEvent, TextWriter output) { - if (logEvent.Properties.TryGetValue(ThreadIdPropertyName, out LogEventPropertyValue threadId)) + if (logEvent.Properties.TryGetValue(MachineNamePropertyName, out var machineName)) { - output.Write($" thread=\"{((ScalarValue)threadId).Value}\""); + var machineNameValue = ((ScalarValue)machineName).Value.ToString(); + output.Write($" "); } } - private static void WriteMessage(LogEvent logEvent, TextWriter output) + private void WriteMessage(LogEvent logEvent, TextWriter output) { output.Write(""); - logEvent.RenderMessage(output); + xmlSerializer.SerializeXmlValue(output, logEvent.RenderMessage(), false); output.Write(""); } - private static void WriteException(LogEvent logEvent, TextWriter output) + private void WriteException(LogEvent logEvent, TextWriter output) { if (logEvent.Exception == null) { return; } - output.Write($"{logEvent.Exception}"); + output.Write(""); + xmlSerializer.SerializeXmlValue(output, logEvent.Exception.ToString(), false); + output.Write(""); } } -} \ No newline at end of file +} diff --git a/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4jTextFormatterShould.cs b/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4jTextFormatterShould.cs index 8ec8098..cd95d32 100644 --- a/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4jTextFormatterShould.cs +++ b/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4jTextFormatterShould.cs @@ -35,6 +35,32 @@ public void WriteLoggerAttribute() Deserialize().Root.Attribute("logger").Value.ShouldBe("source context"); } + [Theory] + [InlineData("Some < source context", "Some < source context")] + [InlineData("Some > source context", "Some > source context")] + [InlineData("Some & source context", "Some & source context")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" source context", "Some " source context")] + [InlineData("Some ' source context", "Some ' source context")] + [InlineData("Some \n source context", "Some source context")] + [InlineData("Some \r source context", "Some source context")] + [InlineData("Some \t source context", "Some source context")] + public void WriteEscapedLoggerAttribute(string sourceContext, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("SourceContext", new ScalarValue(sourceContext))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" logger=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Attribute("logger").Value.ShouldBe(sourceContext); + } + [Fact] public void WriteTimestampAttribute() { @@ -76,6 +102,32 @@ public void WriteTheadAttribute() Deserialize().Root.Attribute("thread").Value.ShouldBe("1"); } + [Theory] + [InlineData("Some < thread", "Some < thread")] + [InlineData("Some > thread", "Some > thread")] + [InlineData("Some & thread", "Some & thread")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" thread", "Some " thread")] + [InlineData("Some ' thread", "Some ' thread")] + [InlineData("Some \n thread", "Some thread")] + [InlineData("Some \r thread", "Some thread")] + [InlineData("Some \t thread", "Some thread")] + public void WriteEscapedTheadAttribute(string thread, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("ThreadId", new ScalarValue(thread))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" thread=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Attribute("thread").Value.ShouldBe(thread); + } + [Fact] public void WriteMessageElement() { @@ -89,6 +141,36 @@ public void WriteMessageElement() Deserialize().Root.Element(Namespace + "message").Value.ShouldBe("Some message"); } + [Theory] + [InlineData("Some < message", "Some < message")] + [InlineData("Some > message", "Some > message")] + [InlineData("Some & message", "Some & message")] + // The following characters should not be escaped in a XML element + [InlineData("Some \" message", "Some \" message")] + [InlineData("Some ' message", "Some ' message")] + [InlineData("Some \n message", "Some \n message")] + [InlineData("Some \r message", "Some \r message")] + [InlineData("Some \t message", "Some \t message")] + public void WriteEscapedMessageElement(string message, string expected) + { + // Arrange + var logEvent = Some.LogEvent(message: message); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($"{expected}"); + + // Lets make sure that the escaped XML can be deserialized back into its original form. + // + // "\r" are deserialized into "\n" by the .NET XML serializer, thus we need to + // compensate for that. + message = message.Replace("\r", "\n"); + + Deserialize().Root.Element(Namespace + "message").Value.ShouldBe(message); + } + [Fact] public void WriteExceptionElement() { @@ -103,12 +185,15 @@ public void WriteExceptionElement() } [Theory] - [InlineData("<", "<")] - [InlineData(">", ">")] - [InlineData("&", "&")] - // The following characters should not be escaped in the error message - [InlineData("\"", "\"")] - [InlineData("'", "'")] + [InlineData("Some < message", "Some < message")] + [InlineData("Some > message", "Some > message")] + [InlineData("Some & message", "Some & message")] + // The following characters should not be escaped in a XML element + [InlineData("Some \" message", "Some \" message")] + [InlineData("Some ' message", "Some ' message")] + [InlineData("Some \n message", "Some \n message")] + [InlineData("Some \r message", "Some \r message")] + [InlineData("Some \t message", "Some \t message")] public void WriteEscapedExceptionElement(string message, string expected) { // Arrange @@ -119,6 +204,14 @@ public void WriteEscapedExceptionElement(string message, string expected) // Assert output.ToString().ShouldContain($"System.DivideByZeroException: {expected}"); + + // Lets make sure that the escaped XML can be deserialized back into its original form. + // + // "\r" are deserialized into "\n" by the .NET XML serializer, thus we need to + // compensate for that. + message = message.Replace("\r", "\n"); + + Deserialize().Root.Element(Namespace + "throwable").Value.ShouldBe($"System.DivideByZeroException: {message}"); } private XDocument Deserialize() diff --git a/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4netTextFormatterShould.cs b/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4netTextFormatterShould.cs index 9c52093..26f84ca 100644 --- a/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4netTextFormatterShould.cs +++ b/test/Serilog.Sinks.Udp.Tests/Sinks/Udp/TextFormatters/Log4netTextFormatterShould.cs @@ -35,6 +35,32 @@ public void WriteLoggerAttribute() Deserialize().Root.Attribute("logger").Value.ShouldBe("source context"); } + [Theory] + [InlineData("Some < source context", "Some < source context")] + [InlineData("Some > source context", "Some > source context")] + [InlineData("Some & source context", "Some & source context")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" source context", "Some " source context")] + [InlineData("Some ' source context", "Some ' source context")] + [InlineData("Some \n source context", "Some source context")] + [InlineData("Some \r source context", "Some source context")] + [InlineData("Some \t source context", "Some source context")] + public void WriteEscapedLoggerAttribute(string sourceContext, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("SourceContext", new ScalarValue(sourceContext))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" logger=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Attribute("logger").Value.ShouldBe(sourceContext); + } + [Fact] public void WriteTimestampAttribute() { @@ -76,6 +102,32 @@ public void WriteTheadAttribute() Deserialize().Root.Attribute("thread").Value.ShouldBe("1"); } + [Theory] + [InlineData("Some < thread", "Some < thread")] + [InlineData("Some > thread", "Some > thread")] + [InlineData("Some & thread", "Some & thread")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" thread", "Some " thread")] + [InlineData("Some ' thread", "Some ' thread")] + [InlineData("Some \n thread", "Some thread")] + [InlineData("Some \r thread", "Some thread")] + [InlineData("Some \t thread", "Some thread")] + public void WriteEscapedTheadAttribute(string thread, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("ThreadId", new ScalarValue(thread))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" thread=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Attribute("thread").Value.ShouldBe(thread); + } + [Fact] public void WriteUsernameAttribute() { @@ -90,18 +142,70 @@ public void WriteUsernameAttribute() Deserialize().Root.Attribute("username").Value.ShouldBe("some user"); } + [Theory] + [InlineData("Some < username", "Some < username")] + [InlineData("Some > username", "Some > username")] + [InlineData("Some & username", "Some & username")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" username", "Some " username")] + [InlineData("Some ' username", "Some ' username")] + [InlineData("Some \n username", "Some username")] + [InlineData("Some \r username", "Some username")] + [InlineData("Some \t username", "Some username")] + public void WriteEscapedUsernameAttribute(string username, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("EnvironmentUserName", new ScalarValue(username))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" username=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Attribute("username").Value.ShouldBe(username); + } + [Fact] public void WriteDomainAttribute() { // Arrange var logEvent = Some.LogEvent(); - logEvent.AddOrUpdateProperty(new LogEventProperty("ProcessName", new ScalarValue("some domain"))); + logEvent.AddOrUpdateProperty(new LogEventProperty("ProcessName", new ScalarValue("process name"))); // Act formatter.Format(logEvent, output); // Assert - Deserialize().Root.Attribute("domain").Value.ShouldBe("some domain"); + Deserialize().Root.Attribute("domain").Value.ShouldBe("process name"); + } + + [Theory] + [InlineData("Some < process name", "Some < process name")] + [InlineData("Some > process name", "Some > process name")] + [InlineData("Some & process name", "Some & process name")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" process name", "Some " process name")] + [InlineData("Some ' process name", "Some ' process name")] + [InlineData("Some \n process name", "Some process name")] + [InlineData("Some \r process name", "Some process name")] + [InlineData("Some \t process name", "Some process name")] + public void WriteEscapedDomainAttribute(string processName, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("ProcessName", new ScalarValue(processName))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" domain=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Attribute("domain").Value.ShouldBe(processName); } [Fact] @@ -118,6 +222,32 @@ public void WriteClassAttribute() Deserialize().Root.Element(Namespace + "locationInfo").Attribute("class").Value.ShouldBe("source context"); } + [Theory] + [InlineData("Some < source context", "Some < source context")] + [InlineData("Some > source context", "Some > source context")] + [InlineData("Some & source context", "Some & source context")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" source context", "Some " source context")] + [InlineData("Some ' source context", "Some ' source context")] + [InlineData("Some \n source context", "Some source context")] + [InlineData("Some \r source context", "Some source context")] + [InlineData("Some \t source context", "Some source context")] + public void WriteEscapedClassAttribute(string sourceContext, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("SourceContext", new ScalarValue(sourceContext))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" class=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Element(Namespace + "locationInfo").Attribute("class").Value.ShouldBe(sourceContext); + } + [Fact] public void WriteMethodAttribute() { @@ -131,9 +261,35 @@ public void WriteMethodAttribute() // Assert Deserialize().Root.Element(Namespace + "locationInfo").Attribute("method").Value.ShouldBe("Void Method()"); } - + + [Theory] + [InlineData("Some < method", "Some < method")] + [InlineData("Some > method", "Some > method")] + [InlineData("Some & method", "Some & method")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" method", "Some " method")] + [InlineData("Some ' method", "Some ' method")] + [InlineData("Some \n method", "Some method")] + [InlineData("Some \r method", "Some method")] + [InlineData("Some \t method", "Some method")] + public void WriteEscapedMethodAttribute(string method, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("Method", new ScalarValue(method))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" method=\"{expected}\""); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Element(Namespace + "locationInfo").Attribute("method").Value.ShouldBe(method); + } + [Fact] - public void WriteMachineNameAttribute() + public void WriteHostNameAttribute() { // Arrange var logEvent = Some.LogEvent(); @@ -146,6 +302,32 @@ public void WriteMachineNameAttribute() Deserialize().Root.Element(Namespace + "properties").Element(Namespace + "data").Attribute("value").Value.ShouldBe("MachineName"); } + [Theory] + [InlineData("Some < hostname", "Some < hostname")] + [InlineData("Some > hostname", "Some > hostname")] + [InlineData("Some & hostname", "Some & hostname")] + // The following characters should be escaped in a XML attribute + [InlineData("Some \" hostname", "Some " hostname")] + [InlineData("Some ' hostname", "Some ' hostname")] + [InlineData("Some \n hostname", "Some hostname")] + [InlineData("Some \r hostname", "Some hostname")] + [InlineData("Some \t hostname", "Some hostname")] + public void WriteEscapedHostNameAttribute(string hostname, string expected) + { + // Arrange + var logEvent = Some.LogEvent(); + logEvent.AddOrUpdateProperty(new LogEventProperty("MachineName", new ScalarValue(hostname))); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($" "); + + // Lets make sure that the escaped XML can be deserialized back into its original form + Deserialize().Root.Element(Namespace + "properties").Element(Namespace + "data").Attribute("value").Value.ShouldBe(hostname); + } + [Fact] public void WriteMessageElement() { @@ -159,6 +341,36 @@ public void WriteMessageElement() Deserialize().Root.Element(Namespace + "message").Value.ShouldBe("Some message"); } + [Theory] + [InlineData("Some < message", "Some < message")] + [InlineData("Some > message", "Some > message")] + [InlineData("Some & message", "Some & message")] + // The following characters should not be escaped in a XML element + [InlineData("Some \" message", "Some \" message")] + [InlineData("Some ' message", "Some ' message")] + [InlineData("Some \n message", "Some \n message")] + [InlineData("Some \r message", "Some \r message")] + [InlineData("Some \t message", "Some \t message")] + public void WriteEscapedMessageElement(string message, string expected) + { + // Arrange + var logEvent = Some.LogEvent(message: message); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($"{expected}"); + + // Lets make sure that the escaped XML can be deserialized back into its original form. + // + // "\r" are deserialized into "\n" by the .NET XML serializer, thus we need to + // compensate for that. + message = message.Replace("\r", "\n"); + + Deserialize().Root.Element(Namespace + "message").Value.ShouldBe(message); + } + [Fact] public void WriteExceptionElement() { @@ -172,6 +384,36 @@ public void WriteExceptionElement() Deserialize().Root.Element(Namespace + "throwable").Value.ShouldNotBeNull(); } + [Theory] + [InlineData("Some < message", "Some < message")] + [InlineData("Some > message", "Some > message")] + [InlineData("Some & message", "Some & message")] + // The following characters should not be escaped in a XML element + [InlineData("Some \" message", "Some \" message")] + [InlineData("Some ' message", "Some ' message")] + [InlineData("Some \n message", "Some \n message")] + [InlineData("Some \r message", "Some \r message")] + [InlineData("Some \t message", "Some \t message")] + public void WriteEscapedExceptionElement(string message, string expected) + { + // Arrange + var logEvent = Some.LogEvent(exception: new DivideByZeroException(message)); + + // Act + formatter.Format(logEvent, output); + + // Assert + output.ToString().ShouldContain($"System.DivideByZeroException: {expected}"); + + // Lets make sure that the escaped XML can be deserialized back into its original form. + // + // "\r" are deserialized into "\n" by the .NET XML serializer, thus we need to + // compensate for that. + message = message.Replace("\r", "\n"); + + Deserialize().Root.Element(Namespace + "throwable").Value.ShouldBe($"System.DivideByZeroException: {message}"); + } + private XDocument Deserialize() { return XDocument.Parse(output.ToString());