Skip to content

Commit

Permalink
Merge branch 'local-date'
Browse files Browse the repository at this point in the history
Closes #64
  • Loading branch information
ltrzesniewski committed Jul 25, 2024
2 parents cc2b40a + 8e7fccc commit b0d15f5
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 15 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +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 UTC date | A `DateTime` format string, default: `yyyy-MM-dd` |
| `%time` | The message UTC timestamp | 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.

Expand Down
5 changes: 5 additions & 0 deletions src/ZeroLog.Analyzers/Support/CompilerServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// ReSharper disable once CheckNamespace

namespace System.Runtime.CompilerServices;

internal static class IsExternalInit;
6 changes: 4 additions & 2 deletions src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ public sealed class DefaultFormatter : Formatter
/// <para>
/// The pattern is a string containing placeholders:
/// <list type="table">
/// <item><term><c>%date</c></term><description>The message UTC date (default format: <c>yyyy-MM-dd</c>).</description></item>
/// <item><term><c>%time</c></term><description>The message UTC timestamp (default format: <c>hh\:mm\:ss\.fffffff</c>).</description></item>
/// <item><term><c>%date</c></term><description>The message UTC date (recommended, default format: <c>yyyy-MM-dd</c>).</description></item>
/// <item><term><c>%localDate</c></term><description>The message local date (default format: <c>yyyy-MM-dd</c>).</description></item>
/// <item><term><c>%time</c></term><description>The message time of day in UTC (default format: <c>hh\:mm\:ss\.fffffff</c>).</description></item>
/// <item><term><c>%localTime</c></term><description>The message time of day converted to the local time zone (default format: <c>hh\:mm\:ss\.fffffff</c>).</description></item>
/// <item><term><c>%thread</c></term><description>The thread name (or ID) which logged the message.</description></item>
/// <item><term><c>%level</c></term><description>The log level in uppercase (specify the <c>pad</c> format to make each level 5 characters wide).</description></item>
/// <item><term><c>%logger</c></term><description>The logger name.</description></item>
Expand Down
31 changes: 29 additions & 2 deletions src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -75,7 +77,9 @@ private static IEnumerable<PatternPart> 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),
Expand Down Expand Up @@ -106,25 +110,27 @@ private static PatternPart ValidatePart(PatternPart part, string placeholderType
}

case PatternPartType.Date:
case PatternPartType.LocalDate:
{
if (part.Format is not null)
{
_ = new DateTime(2020, 01, 01, 03, 04, 05, 06).ToString(part.Format, CultureInfo.InvariantCulture);
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)
{
_ = new TimeSpan(0, 01, 02, 03, 04).ToString(part.Format, CultureInfo.InvariantCulture);
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:
Expand Down Expand Up @@ -224,6 +230,14 @@ public void WritePrefix(LoggedMessage message, Span<char> 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))
Expand All @@ -232,6 +246,14 @@ public void WritePrefix(LoggedMessage message, Span<char> 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;
Expand Down Expand Up @@ -311,13 +333,18 @@ public void WritePrefix(LoggedMessage message, Span<char> 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,
Expand Down
47 changes: 46 additions & 1 deletion src/ZeroLog.Tests/Formatting/PrefixWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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 ")]
Expand All @@ -42,7 +45,10 @@ 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 prefixWriter = new PrefixWriter(pattern)
{
LocalTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Etc/GMT+10")
};

var logMessage = new LogMessage("Foo");
logMessage.Initialize(new Log("Foo.Bar.TestLog"), LogLevel.Info);
Expand All @@ -56,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}")]
Expand All @@ -75,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()
{
Expand Down

0 comments on commit b0d15f5

Please sign in to comment.