From fde467069d70b9068d372107275ca7e63b0efd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Sun, 30 Apr 2023 11:28:09 +0200 Subject: [PATCH] Add support for Serilog.Enrichers.WithCaller / locationInfo Also update README and CHANGELOG for version 1.1.0 --- CHANGELOG.md | 5 ++ README.md | 13 +++- src/Log4NetTextFormatter.cs | 40 +++++++++++- ...g4NetTextFormatterTest.Caller.verified.xml | 4 ++ ...TextFormatterTest.CallerLog4J.verified.xml | 4 ++ ...FormatterTest.CallerNonScalar.verified.xml | 3 + ...tFormatterTest.CallerWithFile.verified.xml | 4 ++ tests/Log4NetTextFormatterTest.cs | 64 +++++++++++++++++++ 8 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 tests/Log4NetTextFormatterTest.Caller.verified.xml create mode 100644 tests/Log4NetTextFormatterTest.CallerLog4J.verified.xml create mode 100644 tests/Log4NetTextFormatterTest.CallerNonScalar.verified.xml create mode 100644 tests/Log4NetTextFormatterTest.CallerWithFile.verified.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d7b90..75188bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0][1.1.0] - 2023-05-02 + +- Add support for caller information (class, method, file, line) through the [Serilog.Enrichers.WithCaller](https://www.nuget.org/packages/Serilog.Enrichers.WithCaller/) package. + ## [1.0.2][1.0.2] - 2023-02-11 - Add a new `Log4NetTextFormatter.Log4JFormatter` static property which is configured for the log4j XML layout. This static property is also useful when using the [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration/) package where it can be used with the following accessor: @@ -80,6 +84,7 @@ Still trying to figure out how to make everything fit together with [MinVer](htt - Implement log4j compatibility mode. +[1.1.0]: https://github.com/serilog-contrib/serilog-formatting-log4net/compare/1.0.2...1.1.0 [1.0.2]: https://github.com/serilog-contrib/serilog-formatting-log4net/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/serilog-contrib/serilog-formatting-log4net/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/serilog-contrib/serilog-formatting-log4net/compare/1.0.0-rc.4...1.0.0 diff --git a/README.md b/README.md index 21b4eb0..7832536 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,17 @@ Include the machine name in log4net events by using [Serilog.Enrichers.Environme var loggerConfiguration = new LoggerConfiguration().Enrich.WithMachineName(); ``` -Combining these three enrichers will produce a log event including `thread`, `domain` and `username` attributes plus a `log4net:HostName` property containing the machine name: +### Caller + +Include caller information (class, method, file, line) by using [Serilog.Enrichers.WithCaller](https://www.nuget.org/packages/Serilog.Enrichers.WithCaller/): + +```c# +var loggerConfiguration = new LoggerConfiguration().Enrich.WithCaller(includeFileInfo: true); +``` + +### All together + +Combining these four enrichers will produce a log event including `thread`, `domain` and `username` attributes, a `log4net:HostName` property containing the machine name and a `locationInfo` element: ```xml @@ -200,6 +210,7 @@ Combining these three enrichers will produce a log event including `thread`, `do The message + ``` diff --git a/src/Log4NetTextFormatter.cs b/src/Log4NetTextFormatter.cs index 6dfc62b..723f873 100644 --- a/src/Log4NetTextFormatter.cs +++ b/src/Log4NetTextFormatter.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Xml; using Serilog.Core; using Serilog.Events; @@ -25,7 +26,7 @@ public class Log4NetTextFormatter : ITextFormatter /// private static readonly string[] SpecialProperties = { Constants.SourceContextPropertyName, OutputProperties.MessagePropertyName, - ThreadIdPropertyName, UserNamePropertyName, MachineNamePropertyName + ThreadIdPropertyName, UserNamePropertyName, MachineNamePropertyName, CallerPropertyName }; /// @@ -46,6 +47,17 @@ public class Log4NetTextFormatter : ITextFormatter /// https://github.com/serilog/serilog-enrichers-environment/blob/v2.1.3/src/Serilog.Enrichers.Environment/Enrichers/MachineNameEnricher.cs#L36 private const string MachineNamePropertyName = "MachineName"; + /// + /// The name of the caller property, set by Serilog.Enrichers.WithCaller + /// + /// https://github.com/pmetz-steelcase/Serilog.Enrichers.WithCaller/blob/1.2.0/Serilog.Enrichers.WithCaller/CallerEnricher.cs#L66 + private const string CallerPropertyName = "Caller"; + + /// + /// The regular exception matching "class", "method", and optionally "file" and "line" for the caller property. + /// + private static readonly Regex CallerRegex = new(@"(?.*)\.(?.*\(.*\))(?: (?.*):(?\d+))?", RegexOptions.Compiled); + private readonly Log4NetTextFormatterOptions _options; private readonly bool _usesLog4JCompatibility; @@ -139,6 +151,7 @@ private void WriteEvent(LogEvent logEvent, XmlWriter writer) } WriteMessage(logEvent, writer); WriteException(logEvent, writer); + WriteLocationInfo(logEvent, writer); writer.WriteEndElement(); } @@ -423,6 +436,31 @@ private void WriteException(LogEvent logEvent, XmlWriter writer) } } + /// + /// Write location information associated to the log event, i.e. class, method, file and line where the event originated. + /// If the event was not enriched with caller information then nothing is written. + /// + /// The log event. + /// The XML writer. + /// https://github.com/apache/logging-log4net/blob/rel/2.0.8/src/Layout/XmlLayout.cs#L297-L307 + /// https://github.com/apache/log4j/blob/v1_2_17/src/main/java/org/apache/log4j/xml/XMLLayout.java#L170-L181 + private void WriteLocationInfo(LogEvent logEvent, XmlWriter writer) + { + if (logEvent.Properties.TryGetValue(CallerPropertyName, out var callerProperty) && callerProperty is ScalarValue { Value: string caller }) + { + var match = CallerRegex.Match(caller); + if (match.Success) + { + WriteStartElement(writer, "locationInfo"); + writer.WriteAttributeString("class", match.Groups[1].Value); + writer.WriteAttributeString("method", match.Groups[2].Value); + writer.WriteAttributeString("file", match.Groups[3].Value); + writer.WriteAttributeString("line", match.Groups[4].Value); + writer.WriteEndElement(); + } + } + } + /// /// Start writing an XML element, taking into account the configured . /// diff --git a/tests/Log4NetTextFormatterTest.Caller.verified.xml b/tests/Log4NetTextFormatterTest.Caller.verified.xml new file mode 100644 index 0000000..4f214f6 --- /dev/null +++ b/tests/Log4NetTextFormatterTest.Caller.verified.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Log4NetTextFormatterTest.CallerLog4J.verified.xml b/tests/Log4NetTextFormatterTest.CallerLog4J.verified.xml new file mode 100644 index 0000000..a726c28 --- /dev/null +++ b/tests/Log4NetTextFormatterTest.CallerLog4J.verified.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Log4NetTextFormatterTest.CallerNonScalar.verified.xml b/tests/Log4NetTextFormatterTest.CallerNonScalar.verified.xml new file mode 100644 index 0000000..134c646 --- /dev/null +++ b/tests/Log4NetTextFormatterTest.CallerNonScalar.verified.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/Log4NetTextFormatterTest.CallerWithFile.verified.xml b/tests/Log4NetTextFormatterTest.CallerWithFile.verified.xml new file mode 100644 index 0000000..65c727e --- /dev/null +++ b/tests/Log4NetTextFormatterTest.CallerWithFile.verified.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Log4NetTextFormatterTest.cs b/tests/Log4NetTextFormatterTest.cs index 10426e1..03bf2cc 100644 --- a/tests/Log4NetTextFormatterTest.cs +++ b/tests/Log4NetTextFormatterTest.cs @@ -583,6 +583,70 @@ public Task MachineNamePropertyStructureValue() return Verify(output); } + [Fact] + public Task Caller() + { + // Arrange + using var output = new StringWriter(); + var logEvent = CreateLogEvent(properties: new LogEventProperty("Caller", new ScalarValue("Fully.Qualified.ClassName.MethodName(System.String)"))); + + var formatter = new Log4NetTextFormatter(); + + // Act + formatter.Format(logEvent, output); + + // Assert + return Verify(output); + } + + [Fact] + public Task CallerNonScalar() + { + // Arrange + using var output = new StringWriter(); + var logEvent = CreateLogEvent(properties: new LogEventProperty("Caller", new CustomLogEventPropertyValue(""))); + + var formatter = new Log4NetTextFormatter(); + + // Act + formatter.Format(logEvent, output); + + // Assert + return Verify(output); + } + + [Fact] + public Task CallerWithFile() + { + // Arrange + using var output = new StringWriter(); + var logEvent = CreateLogEvent(properties: new LogEventProperty("Caller", new ScalarValue("Fully.Qualified.ClassName.MethodName(System.String) /Absolute/Path/To/FileName.cs:123"))); + + var formatter = new Log4NetTextFormatter(); + + // Act + formatter.Format(logEvent, output); + + // Assert + return Verify(output); + } + + [Fact] + public Task CallerLog4J() + { + // Arrange + using var output = new StringWriter(); + var logEvent = CreateLogEvent(properties: new LogEventProperty("Caller", new ScalarValue("Fully.Qualified.ClassName.MethodName(System.String)"))); + + var formatter = new Log4NetTextFormatter(c => c.UseLog4JCompatibility()); + + // Act + formatter.Format(logEvent, output); + + // Assert + return Verify(output); + } + [Fact] public Task SequenceProperty() {