From 6634cec4fe8a3870ae988cfa1d6eb808bbc0b8ba Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Tue, 23 Jul 2024 00:02:27 +0200 Subject: [PATCH 1/3] Add %localDate and %localTime to PrefixWriter --- README.md | 6 ++-- .../Support/CompilerServices.cs | 5 +++ .../Formatting/DefaultFormatter.cs | 2 ++ .../Formatting/PrefixWriter.cs | 31 +++++++++++++++++-- .../Formatting/PrefixWriterTests.cs | 10 +++++- 5 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/ZeroLog.Analyzers/Support/CompilerServices.cs diff --git a/README.md b/README.md index 4b98bf9..75a4c4d 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,10 @@ The prefix pattern is a string with the following placeholders: | Placeholder | Effect | Format | |------------------|---------------------------------------------------------------|------------------------------------------------------------| -| `%date` | The message UTC date | A `DateTime` format string, default: `yyyy-MM-dd` | -| `%time` | The message UTC timestamp | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | +| `%date` | The message date in UTC | A `DateTime` format string, default: `yyyy-MM-dd` | +| `%localDate` | The message date converted to the local time zone | A `DateTime` format string, default: `yyyy-MM-dd` | +| `%time` | The message timestamp in UTC | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | +| `%localTime` | The message timestamp converted to the local time zone | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | | `%thread` | The thread name (or ID) which logged the message | | | `%level` | The log level in uppercase | `pad` is equivalent to `5` (the longest level length) | | `%logger` | The logger name | | diff --git a/src/ZeroLog.Analyzers/Support/CompilerServices.cs b/src/ZeroLog.Analyzers/Support/CompilerServices.cs new file mode 100644 index 0000000..6d60b5b --- /dev/null +++ b/src/ZeroLog.Analyzers/Support/CompilerServices.cs @@ -0,0 +1,5 @@ +// ReSharper disable once CheckNamespace + +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit; diff --git a/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs b/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs index 56c2e15..efaaae8 100644 --- a/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs +++ b/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs @@ -29,7 +29,9 @@ public sealed class DefaultFormatter : Formatter /// The pattern is a string containing placeholders: /// /// %dateThe message UTC date (default format: yyyy-MM-dd). + /// %localDateThe message local date (default format: yyyy-MM-dd). /// %timeThe message UTC timestamp (default format: hh\:mm\:ss\.fffffff). + /// %localTimeThe message local timestamp (default format: hh\:mm\:ss\.fffffff). /// %threadThe thread name (or ID) which logged the message. /// %levelThe log level in uppercase (specify the pad format to make each level 5 characters wide). /// %loggerThe logger name. diff --git a/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs b/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs index 469f425..6197ccd 100644 --- a/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs +++ b/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs @@ -30,6 +30,8 @@ internal class PrefixWriter public string Pattern { get; } + internal TimeZoneInfo? LocalTimeZone { get; init; } // For unit tests + [SuppressMessage("ReSharper", "ConvertToPrimaryConstructor")] public PrefixWriter(string pattern) { @@ -75,7 +77,9 @@ private static IEnumerable ParsePattern(string pattern) var part = placeholderType.ToLowerInvariant() switch { "date" => new PatternPart(PatternPartType.Date, format), + "localdate" => new PatternPart(PatternPartType.LocalDate, format), "time" => new PatternPart(PatternPartType.Time, format), + "localtime" => new PatternPart(PatternPartType.LocalTime, format), "thread" => new PatternPart(PatternPartType.Thread, format), "level" => new PatternPart(PatternPartType.Level, format), "logger" => new PatternPart(PatternPartType.Logger, format), @@ -106,6 +110,7 @@ private static PatternPart ValidatePart(PatternPart part, string placeholderType } case PatternPartType.Date: + case PatternPartType.LocalDate: { if (part.Format is not null) { @@ -113,10 +118,11 @@ private static PatternPart ValidatePart(PatternPart part, string placeholderType return part; } - return new PatternPart(PatternPartType.Date, "yyyy-MM-dd", null); + return new PatternPart(part.Type, "yyyy-MM-dd", null); } case PatternPartType.Time: + case PatternPartType.LocalTime: { if (part.Format is not null) { @@ -124,7 +130,7 @@ private static PatternPart ValidatePart(PatternPart part, string placeholderType return part; } - return new PatternPart(PatternPartType.Time, @"hh\:mm\:ss\.fffffff", null); + return new PatternPart(part.Type, @"hh\:mm\:ss\.fffffff", null); } case PatternPartType.Level: @@ -224,6 +230,14 @@ public void WritePrefix(LoggedMessage message, Span destination, out int c break; } + case PatternPartType.LocalDate: + { + if (!builder.TryAppend(ToLocalDate(message.Timestamp), part.Format)) + goto endOfLoop; + + break; + } + case PatternPartType.Time: { if (!builder.TryAppend(message.Timestamp.TimeOfDay, part.Format)) @@ -232,6 +246,14 @@ public void WritePrefix(LoggedMessage message, Span destination, out int c break; } + case PatternPartType.LocalTime: + { + if (!builder.TryAppend(ToLocalDate(message.Timestamp).TimeOfDay, part.Format)) + goto endOfLoop; + + break; + } + case PatternPartType.Thread: { var thread = message.Thread; @@ -311,13 +333,18 @@ public void WritePrefix(LoggedMessage message, Span destination, out int c charsWritten = builder.Length; } + private DateTime ToLocalDate(DateTime value) + => LocalTimeZone is not null ? TimeZoneInfo.ConvertTimeFromUtc(value, LocalTimeZone) : value.ToLocalTime(); + #endif private enum PatternPartType { String, Date, + LocalDate, Time, + LocalTime, Thread, Level, Logger, diff --git a/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs b/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs index 87f0842..6643022 100644 --- a/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs +++ b/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs @@ -15,7 +15,9 @@ public class PrefixWriterTests [TestCase("", "")] [TestCase("foo", "foo")] [TestCase("%date", "2020-01-02")] + [TestCase("%localDate", "2020-01-01")] [TestCase("%time", "03:04:05.0060000")] + [TestCase("%localTime", "17:04:05.0060000")] [TestCase("%level", "INFO")] [TestCase("%logger", "Foo.Bar.TestLog")] [TestCase("%loggerCompact", "FB.TestLog")] @@ -31,6 +33,7 @@ public class PrefixWriterTests [TestCase("%{level}Bar", "INFOBar")] [TestCase("%{level}%{logger}", "INFOFoo.Bar.TestLog")] [TestCase("%{date:dd MM yyyy}", "02 01 2020")] + [TestCase("%{localDate:dd MM yyyy HH mm ss}", "01 01 2020 17 04 05")] [TestCase("%{date:lol}", "lol")] [TestCase("%{time:hh\\:mm}", "03:04")] [TestCase("%{level:pad}", "INFO ")] @@ -42,7 +45,12 @@ public class PrefixWriterTests [TestCase("abc%{column:10}def%{column:15}ghi", "abc def ghi")] public void should_write_prefix(string pattern, string expectedResult) { - var prefixWriter = new PrefixWriter(pattern); + var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Etc/GMT+10"); + + var prefixWriter = new PrefixWriter(pattern) + { + LocalTimeZone = localTimeZone + }; var logMessage = new LogMessage("Foo"); logMessage.Initialize(new Log("Foo.Bar.TestLog"), LogLevel.Info); From c2e62e5de00d3c8c901068f5f7dfdef61e71d09e Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Tue, 23 Jul 2024 19:09:38 +0200 Subject: [PATCH 2/3] Add PrefixWriter allocation tests --- .../Formatting/PrefixWriterTests.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs b/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs index 6643022..8575d1a 100644 --- a/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs +++ b/src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs @@ -45,11 +45,9 @@ public class PrefixWriterTests [TestCase("abc%{column:10}def%{column:15}ghi", "abc def ghi")] public void should_write_prefix(string pattern, string expectedResult) { - var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Etc/GMT+10"); - var prefixWriter = new PrefixWriter(pattern) { - LocalTimeZone = localTimeZone + LocalTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Etc/GMT+10") }; var logMessage = new LogMessage("Foo"); @@ -64,7 +62,9 @@ public void should_write_prefix(string pattern, string expectedResult) [Test] [TestCase("%{date:\\}")] + [TestCase("%{localDate:\\}")] [TestCase("%{time:\\}")] + [TestCase("%{localTime:\\}")] [TestCase("%{level:-3}")] [TestCase("%{level:lol}")] [TestCase("%{logger:-3}")] @@ -83,6 +83,43 @@ public void should_throw_on_invalid_format(string pattern) PrefixWriter.IsValidPattern(pattern).ShouldBeFalse(); } + [Test] + [TestCase("")] + [TestCase("foo")] + [TestCase("%date")] + [TestCase("%localDate")] + [TestCase("%time")] + [TestCase("%localTime")] + [TestCase("%thread")] + [TestCase("%level")] + [TestCase("%logger")] + [TestCase("%loggerCompact")] + [TestCase("%newline")] + [TestCase("abc%{column:10}def")] + [TestCase("foo %level bar %logger baz")] + [TestCase("%{date:dd MM yyyy HH mm ss}")] + [TestCase("%{localDate:dd MM yyyy HH mm ss}")] + [TestCase("%{level:pad}")] + public void should_not_allocate(string pattern) + { + var prefixWriter = new PrefixWriter(pattern); + + var logMessage = new LogMessage("Foo"); + logMessage.Initialize(new Log("Foo.Bar.TestLog"), LogLevel.Info); + logMessage.Timestamp = new DateTime(2020, 01, 02, 03, 04, 05, 06); + + var buffer = new char[256]; + var formattedLogMessage = new LoggedMessage(256, ZeroLogConfiguration.Default); + formattedLogMessage.SetMessage(logMessage); + + GcTester.ShouldNotAllocate(() => + { + prefixWriter.WritePrefix(formattedLogMessage, buffer, out _); + }); + + PrefixWriter.IsValidPattern(pattern).ShouldBeTrue(); + } + [Test, RequiresThread] public void should_write_thread_name() { From 8e7fccc74d149a2cf010b5eb9fcda6d25220788c Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Tue, 23 Jul 2024 19:23:32 +0200 Subject: [PATCH 3/3] Update docs --- README.md | 24 +++++++++---------- .../Formatting/DefaultFormatter.cs | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 75a4c4d..68875a3 100644 --- a/README.md +++ b/README.md @@ -130,18 +130,18 @@ The output format of the built-in appenders may be customized through the `Forma The prefix pattern is a string with the following placeholders: -| Placeholder | Effect | Format | -|------------------|---------------------------------------------------------------|------------------------------------------------------------| -| `%date` | The message date in UTC | A `DateTime` format string, default: `yyyy-MM-dd` | -| `%localDate` | The message date converted to the local time zone | A `DateTime` format string, default: `yyyy-MM-dd` | -| `%time` | The message timestamp in UTC | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | -| `%localTime` | The message timestamp converted to the local time zone | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | -| `%thread` | The thread name (or ID) which logged the message | | -| `%level` | The log level in uppercase | `pad` is equivalent to `5` (the longest level length) | -| `%logger` | The logger name | | -| `%loggerCompact` | The logger name, with the namespace shortened to its initials | | -| `%newline` | Equivalent to `Environment.NewLine` | | -| `%column` | Inserts padding spaces until a given column index | The column index to reach | +| Placeholder | Effect | Format | +|------------------|------------------------------------------------------------------|------------------------------------------------------------| +| `%date` | The message date in UTC (recommended, also contains time of day) | A `DateTime` format string, default: `yyyy-MM-dd` | +| `%localDate` | The message date converted to the local time zone | A `DateTime` format string, default: `yyyy-MM-dd` | +| `%time` | The message time of day (in UTC) | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | +| `%localTime` | The message time of day (converted to the local time zone) | A `TimeSpan` format string, default: `hh\:mm\:ss\.fffffff` | +| `%thread` | The thread name (or ID) which logged the message | | +| `%level` | The log level in uppercase | `pad` is equivalent to `5` (the longest level length) | +| `%logger` | The logger name | | +| `%loggerCompact` | The logger name, with the namespace shortened to its initials | | +| `%newline` | Equivalent to `Environment.NewLine` | | +| `%column` | Inserts padding spaces until a given column index | The column index to reach | Prefixes can be written in the form `%{prefix}` or `%{prefix:format}` to define a format string. String placeholders accept an integer format string which defines their minimum length. For instance, `%{logger:20}` will always be at least 20 characters wide. diff --git a/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs b/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs index efaaae8..7b15fc0 100644 --- a/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs +++ b/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs @@ -28,10 +28,10 @@ public sealed class DefaultFormatter : Formatter /// /// The pattern is a string containing placeholders: /// - /// %dateThe message UTC date (default format: yyyy-MM-dd). + /// %dateThe message UTC date (recommended, default format: yyyy-MM-dd). /// %localDateThe message local date (default format: yyyy-MM-dd). - /// %timeThe message UTC timestamp (default format: hh\:mm\:ss\.fffffff). - /// %localTimeThe message local timestamp (default format: hh\:mm\:ss\.fffffff). + /// %timeThe message time of day in UTC (default format: hh\:mm\:ss\.fffffff). + /// %localTimeThe message time of day converted to the local time zone (default format: hh\:mm\:ss\.fffffff). /// %threadThe thread name (or ID) which logged the message. /// %levelThe log level in uppercase (specify the pad format to make each level 5 characters wide). /// %loggerThe logger name.