diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 172c836..dfd9f05 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,9 +15,9 @@ jobs:
uses: actions/checkout@v3
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 7.0.x
+ dotnet-version: 8.0.x
- name: Restore
run: dotnet restore src/ZeroLog.sln
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index ef054f5..ba76029 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -3,7 +3,7 @@
4
true
true
- 11.0
+ 12.0
false
$(DefaultItemExcludes);*.DotSettings;*.ncrunchproject
embedded
diff --git a/src/ZeroLog.Analyzers.Tests/ZeroLog.Analyzers.Tests.csproj b/src/ZeroLog.Analyzers.Tests/ZeroLog.Analyzers.Tests.csproj
index d326faf..f7e4971 100644
--- a/src/ZeroLog.Analyzers.Tests/ZeroLog.Analyzers.Tests.csproj
+++ b/src/ZeroLog.Analyzers.Tests/ZeroLog.Analyzers.Tests.csproj
@@ -15,9 +15,9 @@
-
-
-
+
+
+
diff --git a/src/ZeroLog.Impl.Base/Log.cs b/src/ZeroLog.Impl.Base/Log.cs
index 40593fe..7ccf62e 100644
--- a/src/ZeroLog.Impl.Base/Log.cs
+++ b/src/ZeroLog.Impl.Base/Log.cs
@@ -13,12 +13,18 @@ public sealed partial class Log
private LogLevel _logLevel = LogLevel.None;
internal string Name { get; }
+ internal byte[] NameUtf8 { get; }
+
internal string CompactName { get; }
+ internal byte[] CompactNameUtf8 { get; }
internal Log(string name)
{
Name = name;
+ NameUtf8 = Encoding.UTF8.GetBytes(Name);
+
CompactName = GetCompactName(name);
+ CompactNameUtf8 = Encoding.UTF8.GetBytes(CompactName);
}
///
diff --git a/src/ZeroLog.Impl.Full/Appenders/StreamAppender.cs b/src/ZeroLog.Impl.Full/Appenders/StreamAppender.cs
index b944e48..375ceb4 100644
--- a/src/ZeroLog.Impl.Full/Appenders/StreamAppender.cs
+++ b/src/ZeroLog.Impl.Full/Appenders/StreamAppender.cs
@@ -13,9 +13,10 @@ public abstract class StreamAppender : Appender
{
private byte[] _byteBuffer = Array.Empty();
+ private readonly Formatter _formatter = new DefaultFormatter();
private Encoding _encoding = Encoding.UTF8;
+ private Utf8Formatter? _utf8Formatter;
private bool _useSpanGetBytes;
- private Formatter? _formatter;
///
/// The stream to write to.
@@ -30,6 +31,9 @@ protected internal Encoding Encoding
get => _encoding;
set
{
+ if (ReferenceEquals(value, _encoding))
+ return;
+
_encoding = value;
UpdateEncodingSpecificData();
}
@@ -40,8 +44,15 @@ protected internal Encoding Encoding
///
public Formatter Formatter
{
- get => _formatter ??= new DefaultFormatter();
- init => _formatter = value;
+ get => _formatter;
+ init
+ {
+ if (ReferenceEquals(value, _formatter))
+ return;
+
+ _formatter = value;
+ UpdateEncodingSpecificData();
+ }
}
///
@@ -64,21 +75,25 @@ public override void Dispose()
///
public override void WriteMessage(LoggedMessage message)
{
- if (Stream is null)
+ if (Stream is not { } stream)
return;
- if (_useSpanGetBytes)
+ if (_utf8Formatter is { } utf8Formatter)
+ {
+ stream.Write(utf8Formatter.FormatMessage(message));
+ }
+ else if (_useSpanGetBytes)
{
- var chars = Formatter.FormatMessage(message);
+ var chars = _formatter.FormatMessage(message);
var byteCount = _encoding.GetBytes(chars, _byteBuffer);
- Stream.Write(_byteBuffer, 0, byteCount);
+ stream.Write(_byteBuffer, 0, byteCount);
}
else
{
- Formatter.FormatMessage(message);
- var charBuffer = Formatter.GetBuffer(out var charCount);
+ _formatter.FormatMessage(message);
+ var charBuffer = _formatter.GetBuffer(out var charCount);
var byteCount = _encoding.GetBytes(charBuffer, 0, charCount, _byteBuffer, 0);
- Stream.Write(_byteBuffer, 0, byteCount);
+ stream.Write(_byteBuffer, 0, byteCount);
}
}
@@ -91,7 +106,14 @@ public override void Flush()
private void UpdateEncodingSpecificData()
{
- var maxBytes = _encoding.GetMaxByteCount(LogManager.OutputBufferSize);
+ if (_encoding is UTF8Encoding && _formatter.AsUtf8Formatter() is { } utf8Formatter)
+ {
+ // Fast path
+ _utf8Formatter = utf8Formatter;
+ return;
+ }
+
+ _utf8Formatter = null;
// The base Encoding class allocates buffers in all non-abstract GetBytes overloads in order to call the abstract
// GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) in the end.
@@ -99,6 +121,8 @@ private void UpdateEncodingSpecificData()
// and it skips safety checks as those are guaranteed by the Span struct. In that case, we can call this overload directly.
_useSpanGetBytes = OverridesSpanGetBytes(_encoding.GetType());
+ var maxBytes = _encoding.GetMaxByteCount(LogManager.OutputBufferSize);
+
if (_byteBuffer.Length < maxBytes)
_byteBuffer = GC.AllocateUninitializedArray(maxBytes);
}
diff --git a/src/ZeroLog.Impl.Full/Configuration/ZeroLogConfiguration.cs b/src/ZeroLog.Impl.Full/Configuration/ZeroLogConfiguration.cs
index 419d9a1..2f083f9 100644
--- a/src/ZeroLog.Impl.Full/Configuration/ZeroLogConfiguration.cs
+++ b/src/ZeroLog.Impl.Full/Configuration/ZeroLogConfiguration.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
+using System.Text;
using ZeroLog.Appenders;
using ZeroLog.Formatting;
@@ -16,6 +17,9 @@ public sealed class ZeroLogConfiguration
private LoggerConfigurationCollection _loggers = new();
+ private string _nullDisplayString = "null";
+ private string _truncatedMessageSuffix = " [TRUNCATED]";
+
internal event Action? ApplyChangesRequested;
///
@@ -86,7 +90,17 @@ public bool UseBackgroundThread
///
/// Default: "null"
///
- public string NullDisplayString { get; set; } = "null";
+ public string NullDisplayString
+ {
+ get => _nullDisplayString;
+ set
+ {
+ _nullDisplayString = value;
+ NullDisplayStringUtf8 = Encoding.UTF8.GetBytes(value);
+ }
+ }
+
+ internal byte[] NullDisplayStringUtf8 { get; private set; }
///
/// The string which is appended to a message when it is truncated.
@@ -94,7 +108,17 @@ public bool UseBackgroundThread
///
/// Default: " [TRUNCATED]"
///
- public string TruncatedMessageSuffix { get; set; } = " [TRUNCATED]";
+ public string TruncatedMessageSuffix
+ {
+ get => _truncatedMessageSuffix;
+ set
+ {
+ _truncatedMessageSuffix = value;
+ TruncatedMessageSuffixUtf8 = Encoding.UTF8.GetBytes(value);
+ }
+ }
+
+ internal byte[] TruncatedMessageSuffixUtf8 { get; private set; }
///
/// The time an appender will be put into quarantine (not used to log messages) after it throws an exception.
@@ -121,6 +145,15 @@ public bool UseBackgroundThread
///
public ILoggerConfigurationCollection Loggers => _loggers;
+ ///
+ /// Creates a new ZeroLog configuration.
+ ///
+ public ZeroLogConfiguration()
+ {
+ NullDisplayStringUtf8 = Encoding.UTF8.GetBytes(NullDisplayString);
+ TruncatedMessageSuffixUtf8 = Encoding.UTF8.GetBytes(TruncatedMessageSuffix);
+ }
+
///
/// Applies the changes made to this object since the call to
/// or the last call to .
diff --git a/src/ZeroLog.Impl.Full/EnumArg.cs b/src/ZeroLog.Impl.Full/EnumArg.cs
index e59caf3..7d336c4 100644
--- a/src/ZeroLog.Impl.Full/EnumArg.cs
+++ b/src/ZeroLog.Impl.Full/EnumArg.cs
@@ -1,8 +1,10 @@
using System;
+using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Text.Unicode;
using ZeroLog.Configuration;
using ZeroLog.Support;
@@ -44,6 +46,27 @@ public bool TryFormat(Span destination, out int charsWritten, ZeroLogConfi
return TryAppendNumericValue(destination, out charsWritten);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryFormat(Span destination, out int bytesWritten, ZeroLogConfiguration config)
+ {
+ var enumString = GetUtf8String(config);
+
+ if (enumString != null)
+ {
+ if (enumString.Length <= destination.Length)
+ {
+ enumString.CopyTo(destination);
+ bytesWritten = enumString.Length;
+ return true;
+ }
+
+ bytesWritten = 0;
+ return false;
+ }
+
+ return TryAppendNumericValue(destination, out bytesWritten);
+ }
+
[MethodImpl(MethodImplOptions.NoInlining)]
private bool TryAppendNumericValue(Span destination, out int charsWritten)
{
@@ -53,11 +76,35 @@ private bool TryAppendNumericValue(Span destination, out int charsWritten)
return unchecked((long)_value).TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
}
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private bool TryAppendNumericValue(Span destination, out int bytesWritten)
+ {
+#if NET8_0_OR_GREATER
+ if (_value <= long.MaxValue || !EnumCache.IsEnumSigned(_typeHandle))
+ return _value.TryFormat(destination, out bytesWritten, default, CultureInfo.InvariantCulture);
+
+ return unchecked((long)_value).TryFormat(destination, out bytesWritten, default, CultureInfo.InvariantCulture);
+#else
+ Span buffer = stackalloc char[32];
+
+ if (TryAppendNumericValue(buffer, out var charsWritten))
+ return Utf8.FromUtf16(buffer.Slice(0, charsWritten), destination, out _, out bytesWritten) == OperationStatus.Done;
+
+ bytesWritten = 0;
+ return false;
+#endif
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string? GetString(ZeroLogConfiguration config)
=> EnumCache.GetString(_typeHandle, _value, out var enumRegistered)
?? GetStringSlow(enumRegistered, config);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public byte[]? GetUtf8String(ZeroLogConfiguration config)
+ => EnumCache.GetUtf8String(_typeHandle, _value, out var enumRegistered)
+ ?? GetUtf8StringSlow(enumRegistered, config);
+
[MethodImpl(MethodImplOptions.NoInlining)]
private string? GetStringSlow(bool enumRegistered, ZeroLogConfiguration config)
{
@@ -71,6 +118,19 @@ private bool TryAppendNumericValue(Span destination, out int charsWritten)
return EnumCache.GetString(_typeHandle, _value, out _);
}
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private byte[]? GetUtf8StringSlow(bool enumRegistered, ZeroLogConfiguration config)
+ {
+ if (enumRegistered || !config.AutoRegisterEnums)
+ return null;
+
+ if (Type is not { } type)
+ return null;
+
+ LogManager.RegisterEnum(type);
+ return EnumCache.GetUtf8String(_typeHandle, _value, out _);
+ }
+
public bool TryGetValue(out T result)
where T : unmanaged
{
diff --git a/src/ZeroLog.Impl.Full/EnumCache.cs b/src/ZeroLog.Impl.Full/EnumCache.cs
index e8cc974..c012c60 100644
--- a/src/ZeroLog.Impl.Full/EnumCache.cs
+++ b/src/ZeroLog.Impl.Full/EnumCache.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text;
using InlineIL;
using ZeroLog.Support;
using static InlineIL.IL.Emit;
@@ -43,6 +44,18 @@ public static bool IsRegistered(Type enumType)
return null;
}
+ public static byte[]? GetUtf8String(IntPtr typeHandle, ulong value, out bool registered)
+ {
+ if (_enums.TryGetValue(typeHandle, out var values))
+ {
+ registered = true;
+ return values.TryGetUtf8String(value);
+ }
+
+ registered = false;
+ return null;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SuppressMessage("ReSharper", "UnusedParameter.Global")]
[SuppressMessage("ReSharper", "EntityNameCapturedOnly.Global")]
@@ -192,11 +205,13 @@ public static EnumStrings Create(Type enumType)
}
public abstract string? TryGetString(ulong value);
+ public abstract byte[]? TryGetUtf8String(ulong value);
}
private sealed class ArrayEnumStrings : EnumStrings
{
private readonly string[] _strings;
+ private readonly byte[][] _utf8Strings;
public static bool CanHandle(IEnumerable enumItems)
=> enumItems.All(i => i.Value < 32);
@@ -205,37 +220,53 @@ public ArrayEnumStrings(List enumItems)
{
if (enumItems.Count == 0)
{
- _strings = Array.Empty();
+ _strings = [];
+ _utf8Strings = [];
return;
}
var maxValue = enumItems.Select(i => i.Value).Max();
_strings = new string[maxValue + 1];
+ _utf8Strings = new byte[maxValue + 1][];
foreach (var item in enumItems)
+ {
_strings[item.Value] = item.Name;
+ _utf8Strings[item.Value] = Encoding.UTF8.GetBytes(item.Name);
+ }
}
public override string? TryGetString(ulong value)
=> value < (ulong)_strings.Length
? _strings[unchecked((int)value)]
: null;
+
+ public override byte[]? TryGetUtf8String(ulong value)
+ => value < (ulong)_strings.Length
+ ? _utf8Strings[unchecked((int)value)]
+ : null;
}
private sealed class DictionaryEnumStrings : EnumStrings
{
- private readonly Dictionary _strings = new();
+ private readonly Dictionary _strings = new();
public DictionaryEnumStrings(List enumItems)
{
foreach (var item in enumItems)
- _strings[item.Value] = item.Name;
+ _strings[item.Value] = (item.Name, Encoding.UTF8.GetBytes(item.Name));
}
public override string? TryGetString(ulong value)
{
_strings.TryGetValue(value, out var str);
- return str;
+ return str.utf16;
+ }
+
+ public override byte[]? TryGetUtf8String(ulong value)
+ {
+ _strings.TryGetValue(value, out var str);
+ return str.utf8;
}
}
diff --git a/src/ZeroLog.Impl.Full/Formatting/ByteBufferBuilder.cs b/src/ZeroLog.Impl.Full/Formatting/ByteBufferBuilder.cs
new file mode 100644
index 0000000..86cf06d
--- /dev/null
+++ b/src/ZeroLog.Impl.Full/Formatting/ByteBufferBuilder.cs
@@ -0,0 +1,145 @@
+#if NET8_0_OR_GREATER
+
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Unicode;
+
+namespace ZeroLog.Formatting;
+
+[SuppressMessage("ReSharper", "ReplaceSliceWithRangeIndexer")]
+internal ref struct ByteBufferBuilder
+{
+ private readonly Span _buffer;
+ private int _pos;
+
+ public int Length => _pos;
+
+ public ByteBufferBuilder(Span buffer)
+ {
+ _buffer = buffer;
+ _pos = 0;
+ }
+
+ public ReadOnlySpan GetOutput()
+ => _buffer.Slice(0, _pos);
+
+ public Span GetRemainingBuffer()
+ => _buffer.Slice(_pos);
+
+ public void IncrementPos(int chars)
+ => _pos += chars;
+
+ ///
+ /// Appends a character, but does nothing if there is no more room for it.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AppendAscii(char value)
+ {
+ if (_pos < _buffer.Length)
+ _buffer[_pos++] = (byte)value;
+ }
+
+ public bool TryAppendWhole(ReadOnlySpan value)
+ {
+ if (value.Length <= _buffer.Length - _pos)
+ {
+ value.CopyTo(_buffer.Slice(_pos));
+ _pos += value.Length;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool TryAppendWhole(ReadOnlySpan value)
+ {
+ if (Utf8.FromUtf16(value, _buffer.Slice(_pos), out _, out var bytesWritten) == OperationStatus.Done)
+ {
+ _pos += bytesWritten;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool TryAppendPartial(ReadOnlySpan value)
+ {
+ if (value.Length <= _buffer.Length - _pos)
+ {
+ value.CopyTo(_buffer.Slice(_pos));
+ _pos += value.Length;
+ return true;
+ }
+
+ var length = _buffer.Length - _pos;
+ value.Slice(0, length).CopyTo(_buffer.Slice(_pos));
+ _pos += length;
+ return false;
+ }
+
+ public bool TryAppendPartialChars(scoped ReadOnlySpan value)
+ {
+ var status = Utf8.FromUtf16(value, _buffer.Slice(_pos), out _, out var bytesWritten);
+ _pos += bytesWritten;
+ return status == OperationStatus.Done;
+ }
+
+ public void TryAppendPartialAscii(char value, int count)
+ {
+ if (count > 0)
+ {
+ count = Math.Min(count, _buffer.Length - _pos);
+ _buffer.Slice(_pos, count).Fill((byte)value);
+ _pos += count;
+ }
+ }
+
+ public bool TryAppendAscii(char value)
+ {
+ if (_pos < _buffer.Length)
+ {
+ _buffer[_pos] = (byte)value;
+ ++_pos;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool TryAppend(DateTime value, string? format)
+ {
+ if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture))
+ return false;
+
+ _pos += charsWritten;
+ return true;
+ }
+
+ public bool TryAppend(TimeSpan value, string? format)
+ {
+ if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture))
+ return false;
+
+ _pos += charsWritten;
+ return true;
+ }
+
+ public bool TryAppend(T value, string? format = null)
+ where T : struct, IUtf8SpanFormattable
+ {
+ if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture))
+ return false;
+
+ _pos += charsWritten;
+ return true;
+ }
+
+ public override string ToString()
+ => Encoding.UTF8.GetString(GetOutput());
+}
+
+#endif
diff --git a/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs b/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs
index 56c2e15..f0addd4 100644
--- a/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs
+++ b/src/ZeroLog.Impl.Full/Formatting/DefaultFormatter.cs
@@ -1,3 +1,4 @@
+using System.Text;
using ZeroLog.Appenders;
namespace ZeroLog.Formatting;
@@ -84,4 +85,45 @@ protected override void WriteMessage(LoggedMessage message)
WriteLine(message.Exception.ToString());
}
}
+
+#if NET8_0_OR_GREATER
+
+ ///
+ public override Utf8Formatter AsUtf8Formatter()
+ => new DefaultUtf8Formatter(this);
+
+ private sealed class DefaultUtf8Formatter(DefaultFormatter formatter) : Utf8Formatter
+ {
+ private readonly PrefixWriter? _prefixWriter = formatter._prefixWriter;
+ private readonly byte[] _jsonSeparator = Encoding.UTF8.GetBytes(formatter.JsonSeparator);
+
+ protected override void WriteMessage(LoggedMessage message)
+ {
+ if (_prefixWriter != null)
+ {
+ _prefixWriter.WritePrefix(message, GetRemainingBuffer(), out var bytesWritten);
+ AdvanceBy(bytesWritten);
+ }
+
+ Write(message.MessageUtf8);
+
+ if (message.KeyValues.Count != 0)
+ {
+ Write(_jsonSeparator);
+
+ JsonWriterUtf8.WriteJsonToStringBuffer(message.KeyValues, GetRemainingBuffer(), out var bytesWritten);
+ AdvanceBy(bytesWritten);
+ }
+
+ WriteLine();
+
+ if (message.Exception != null)
+ {
+ // This allocates, but there's no better way to get the details.
+ WriteLine(Encoding.UTF8.GetBytes(message.Exception.ToString()));
+ }
+ }
+ }
+
+#endif
}
diff --git a/src/ZeroLog.Impl.Full/Formatting/Formatter.cs b/src/ZeroLog.Impl.Full/Formatting/Formatter.cs
index dc790e3..826b64d 100644
--- a/src/ZeroLog.Impl.Full/Formatting/Formatter.cs
+++ b/src/ZeroLog.Impl.Full/Formatting/Formatter.cs
@@ -31,6 +31,12 @@ public ReadOnlySpan FormatMessage(LoggedMessage message)
/// The message to format.
protected abstract void WriteMessage(LoggedMessage message);
+ ///
+ /// Tries to convert this formatter to an UTF-8 formatter.
+ ///
+ public virtual Utf8Formatter? AsUtf8Formatter()
+ => null;
+
///
/// Appends text to the output.
///
diff --git a/src/ZeroLog.Impl.Full/Formatting/HexUtils.cs b/src/ZeroLog.Impl.Full/Formatting/HexUtils.cs
index 7ff8462..387e1c0 100644
--- a/src/ZeroLog.Impl.Full/Formatting/HexUtils.cs
+++ b/src/ZeroLog.Impl.Full/Formatting/HexUtils.cs
@@ -17,4 +17,18 @@ public static unsafe void AppendValueAsHex(byte* valuePtr, int size, Span
destination[2 * index + 1] = _hexTable[char0Index];
}
}
+
+ public static unsafe void AppendValueAsHex(byte* valuePtr, int size, Span destination)
+ {
+ var hexTableUtf8 = "0123456789abcdef"u8;
+
+ for (var index = 0; index < size; ++index)
+ {
+ var char0Index = valuePtr[index] & 0xf;
+ var char1Index = (valuePtr[index] & 0xf0) >> 4;
+
+ destination[2 * index] = hexTableUtf8[char1Index];
+ destination[2 * index + 1] = hexTableUtf8[char0Index];
+ }
+ }
}
diff --git a/src/ZeroLog.Impl.Full/Formatting/JsonWriterUtf8.cs b/src/ZeroLog.Impl.Full/Formatting/JsonWriterUtf8.cs
new file mode 100644
index 0000000..74c05b6
--- /dev/null
+++ b/src/ZeroLog.Impl.Full/Formatting/JsonWriterUtf8.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace ZeroLog.Formatting;
+
+#if NET8_0_OR_GREATER
+
+internal static unsafe class JsonWriterUtf8
+{
+ public static void WriteJsonToStringBuffer(KeyValueList keyValueList, Span destination, out int charsWritten)
+ {
+ var builder = new ByteBufferBuilder(destination);
+
+ builder.AppendAscii('{');
+ builder.AppendAscii(' ');
+
+ var first = true;
+
+ foreach (var keyValue in keyValueList)
+ {
+ if (!first)
+ {
+ builder.AppendAscii(',');
+ builder.AppendAscii(' ');
+ }
+
+ AppendString(ref builder, keyValue.Key);
+
+ builder.AppendAscii(':');
+ builder.AppendAscii(' ');
+
+ AppendJsonValue(ref builder, keyValue);
+
+ first = false;
+ }
+
+ builder.AppendAscii(' ');
+ builder.AppendAscii('}');
+
+ charsWritten = builder.Length;
+ }
+
+ private static void AppendJsonValue(ref ByteBufferBuilder builder, in LoggedKeyValue keyValue)
+ {
+ if (keyValue.IsBoolean)
+ builder.TryAppendWhole(keyValue.Value.SequenceEqual(bool.TrueString) ? "true"u8 : "false"u8);
+ else if (keyValue.IsNumeric)
+ builder.TryAppendWhole(keyValue.Value);
+ else if (keyValue.IsNull)
+ builder.TryAppendWhole("null"u8);
+ else
+ AppendString(ref builder, keyValue.Value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void AppendString(ref ByteBufferBuilder builder, ReadOnlySpan value)
+ {
+ builder.AppendAscii('"');
+
+ foreach (var c in value)
+ AppendEscapedChar(c, ref builder);
+
+ builder.AppendAscii('"');
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void AppendEscapedChar(char c, ref ByteBufferBuilder builder)
+ {
+ // Escape characters based on https://tools.ietf.org/html/rfc7159
+
+ if (c is '\\' or '"' or <= '\u001F')
+ AppendControlChar(c, ref builder);
+ else
+ builder.TryAppendPartialChars(new ReadOnlySpan(in c));
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void AppendControlChar(char c, ref ByteBufferBuilder builder)
+ {
+ switch (c)
+ {
+ case '"':
+ builder.TryAppendWhole(@"\"""u8);
+ break;
+
+ case '\\':
+ builder.TryAppendWhole(@"\\"u8);
+ break;
+
+ case '\b':
+ builder.TryAppendWhole(@"\b"u8);
+ break;
+
+ case '\t':
+ builder.TryAppendWhole(@"\t"u8);
+ break;
+
+ case '\n':
+ builder.TryAppendWhole(@"\n"u8);
+ break;
+
+ case '\f':
+ builder.TryAppendWhole(@"\f"u8);
+ break;
+
+ case '\r':
+ builder.TryAppendWhole(@"\r"u8);
+ break;
+
+ default:
+ {
+ var prefix = @"\u00"u8;
+ var destination = builder.GetRemainingBuffer();
+
+ if (destination.Length >= prefix.Length + 2)
+ {
+ builder.TryAppendWhole(prefix);
+
+ var byteValue = unchecked((byte)c);
+ HexUtils.AppendValueAsHex(&byteValue, 1, builder.GetRemainingBuffer());
+ builder.IncrementPos(2);
+ }
+
+ break;
+ }
+ }
+ }
+}
+
+#endif
diff --git a/src/ZeroLog.Impl.Full/Formatting/LoggedMessage.cs b/src/ZeroLog.Impl.Full/Formatting/LoggedMessage.cs
index a5e03f7..462ed48 100644
--- a/src/ZeroLog.Impl.Full/Formatting/LoggedMessage.cs
+++ b/src/ZeroLog.Impl.Full/Formatting/LoggedMessage.cs
@@ -1,5 +1,6 @@
using System;
using System.Runtime.CompilerServices;
+using System.Text;
using System.Threading;
using ZeroLog.Configuration;
@@ -10,8 +11,13 @@ namespace ZeroLog.Formatting;
///
public sealed class LoggedMessage
{
- private readonly char[] _messageBuffer;
- private int _messageLength;
+ private readonly char[] _charBuffer;
+ private readonly byte[] _byteBuffer;
+ private readonly KeyValueList _keyValues;
+
+ private int _charBufferLength;
+ private int _byteBufferLength;
+
private ZeroLogConfiguration _config;
private LogMessage _message = LogMessage.Empty;
@@ -38,18 +44,25 @@ public sealed class LoggedMessage
///
/// The logged message text.
///
- public ReadOnlySpan Message => _messageBuffer.AsSpan(0, _messageLength);
+ public ReadOnlySpan Message => GetMessageUtf16();
+
+ ///
+ /// The logged message text, encoded in UTF-8.
+ ///
+ public ReadOnlySpan MessageUtf8 => GetMessageUtf8();
///
/// The logged message metadata as a list of key/value pairs.
///
- public KeyValueList KeyValues { get; }
+ public KeyValueList KeyValues => GetKeyValues();
internal LoggedMessage(int bufferSize, ZeroLogConfiguration config)
{
_config = config;
- _messageBuffer = GC.AllocateUninitializedArray(bufferSize);
- KeyValues = new KeyValueList(bufferSize);
+ _charBuffer = GC.AllocateUninitializedArray(bufferSize);
+ _byteBuffer = GC.AllocateUninitializedArray(bufferSize * Utf8Formatter.MaxUtf8BytesPerChar);
+
+ _keyValues = new KeyValueList(bufferSize);
SetMessage(LogMessage.Empty);
}
@@ -58,31 +71,88 @@ private LoggedMessage(LoggedMessage other)
{
_config = other._config;
- _messageBuffer = other._messageBuffer.AsSpan(0, other._messageLength).ToArray();
- _messageLength = other._messageLength;
+ _charBuffer = other.Message.ToArray();
+ _charBufferLength = other._charBufferLength;
- KeyValues = new KeyValueList(other.KeyValues);
+ _byteBuffer = other.MessageUtf8.ToArray();
+ _byteBufferLength = other._byteBufferLength;
+
+ _keyValues = new KeyValueList(other.KeyValues);
_message = other._message.CloneMetadata();
}
internal void SetMessage(LogMessage message)
{
_message = message;
+ _charBufferLength = -1;
+ _byteBufferLength = -1;
+ _keyValues.Clear();
- try
- {
#if DEBUG
- _messageBuffer.AsSpan().Fill((char)0);
+ _charBuffer.AsSpan().Clear();
+ _byteBuffer.AsSpan().Clear();
#endif
+ }
+
+ private ReadOnlySpan GetMessageUtf16()
+ {
+ if (_charBufferLength < 0)
+ FormatMessage();
- _messageLength = _message.WriteTo(_messageBuffer, _config, LogMessage.FormatType.Formatted, KeyValues);
+ return _charBuffer.AsSpan(0, _charBufferLength);
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ void FormatMessage()
+ {
+ try
+ {
+ _charBufferLength = _byteBufferLength >= 0
+ ? Encoding.UTF8.GetChars(_byteBuffer.AsSpan(0, _byteBufferLength), _charBuffer)
+ : _message.WriteTo(_charBuffer, _config, LogMessage.FormatType.Formatted, _keyValues);
+ }
+ catch (Exception ex)
+ {
+ HandleFormattingError(ex);
+ }
}
- catch (Exception ex)
+ }
+
+ private ReadOnlySpan GetMessageUtf8()
+ {
+ if (_byteBufferLength < 0)
+ FormatMessage();
+
+ return _byteBuffer.AsSpan(0, _byteBufferLength);
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ void FormatMessage()
{
- HandleFormattingError(ex);
+ try
+ {
+#if NET8_0_OR_GREATER
+ _byteBufferLength = _charBufferLength >= 0
+ ? Encoding.UTF8.GetBytes(_charBuffer.AsSpan(0, _charBufferLength), _byteBuffer)
+ : _message.WriteTo(_byteBuffer, _config, LogMessage.FormatType.Formatted, _keyValues);
+#else
+ _byteBufferLength = Encoding.UTF8.GetBytes(GetMessageUtf16(), _byteBuffer);
+#endif
+ }
+ catch (Exception ex)
+ {
+ HandleFormattingError(ex);
+ _byteBufferLength = Encoding.UTF8.GetBytes(_charBuffer.AsSpan(0, _charBufferLength), _byteBuffer);
+ }
}
}
+ private KeyValueList GetKeyValues()
+ {
+ if (_charBufferLength < 0 && _byteBufferLength < 0)
+ GetMessageUtf8();
+
+ return _keyValues;
+ }
+
internal void UpdateConfiguration(ZeroLogConfiguration config)
{
_config = config;
@@ -93,20 +163,20 @@ private void HandleFormattingError(Exception ex)
{
try
{
- var builder = new CharBufferBuilder(_messageBuffer);
+ var builder = new CharBufferBuilder(_charBuffer);
builder.TryAppendPartial("An error occurred during formatting: ");
builder.TryAppendPartial(ex.Message);
builder.TryAppendPartial(" - Unformatted message: ");
var length = _message.WriteTo(builder.GetRemainingBuffer(), _config, LogMessage.FormatType.Unformatted, null);
- _messageLength = builder.Length + length;
+ _charBufferLength = builder.Length + length;
}
catch
{
- var builder = new CharBufferBuilder(_messageBuffer);
+ var builder = new CharBufferBuilder(_charBuffer);
builder.TryAppendPartial("An error occurred during formatting: ");
builder.TryAppendPartial(ex.Message);
- _messageLength = builder.Length;
+ _charBufferLength = builder.Length;
}
}
diff --git a/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs b/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs
index 21978bd..c29997f 100644
--- a/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs
+++ b/src/ZeroLog.Impl.Full/Formatting/PrefixWriter.cs
@@ -310,6 +310,123 @@ public void WritePrefix(LoggedMessage message, Span destination, out int c
charsWritten = builder.Length;
}
+#if NET8_0_OR_GREATER
+
+ public void WritePrefix(LoggedMessage message, Span destination, out int bytesWritten)
+ {
+ var builder = new ByteBufferBuilder(destination);
+
+ foreach (var part in _parts)
+ {
+ var partStartOffset = builder.Length;
+
+ switch (part.Type)
+ {
+ case PatternPartType.String:
+ {
+ if (!builder.TryAppendPartial(part.ValueUtf8))
+ goto endOfLoop;
+
+ break;
+ }
+
+ case PatternPartType.Date:
+ {
+ if (!builder.TryAppend(message.Timestamp, part.Format))
+ goto endOfLoop;
+
+ break;
+ }
+
+ case PatternPartType.Time:
+ {
+ if (!builder.TryAppend(message.Timestamp.TimeOfDay, part.Format))
+ goto endOfLoop;
+
+ break;
+ }
+
+ case PatternPartType.Thread:
+ {
+ var thread = message.Thread;
+
+ if (thread != null)
+ {
+ if (thread.Name != null)
+ {
+ if (!builder.TryAppendPartialChars(thread.Name))
+ goto endOfLoop;
+ }
+ else
+ {
+ if (!builder.TryAppend(thread.ManagedThreadId))
+ goto endOfLoop;
+ }
+ }
+ else
+ {
+ if (!builder.TryAppendAscii('0'))
+ goto endOfLoop;
+ }
+
+ break;
+ }
+
+ case PatternPartType.Level:
+ {
+ var levelString = message.Level switch
+ {
+ LogLevel.Trace => "TRACE"u8,
+ LogLevel.Debug => "DEBUG"u8,
+ LogLevel.Info => "INFO"u8,
+ LogLevel.Warn => "WARN"u8,
+ LogLevel.Error => "ERROR"u8,
+ LogLevel.Fatal => "FATAL"u8,
+ _ => "???"u8
+ };
+
+ if (!builder.TryAppendWhole(levelString))
+ goto endOfLoop;
+
+ break;
+ }
+
+ case PatternPartType.Logger:
+ {
+ if (!builder.TryAppendPartial(message.Logger?.NameUtf8))
+ goto endOfLoop;
+
+ break;
+ }
+
+ case PatternPartType.LoggerCompact:
+ {
+ if (!builder.TryAppendPartial(message.Logger?.CompactNameUtf8))
+ goto endOfLoop;
+
+ break;
+ }
+
+ case PatternPartType.Column:
+ {
+ if (part.FormatInt is { } column)
+ builder.TryAppendPartialAscii(' ', column - builder.Length);
+
+ continue;
+ }
+ }
+
+ if (part.FormatInt is { } fieldLength)
+ builder.TryAppendPartialAscii(' ', fieldLength - builder.Length + partStartOffset);
+ }
+
+ endOfLoop:
+
+ bytesWritten = builder.Length;
+ }
+
+#endif
+
#endif
private enum PatternPartType
@@ -330,6 +447,7 @@ private readonly struct PatternPart
public PatternPartType Type { get; }
public string? Format { get; }
public int? FormatInt { get; }
+ public byte[]? ValueUtf8 { get; }
public PatternPart(PatternPartType type, string? format = null)
{
@@ -349,6 +467,7 @@ public PatternPart(string value)
{
Type = PatternPartType.String;
Format = value;
+ ValueUtf8 = Encoding.UTF8.GetBytes(value);
}
}
}
diff --git a/src/ZeroLog.Impl.Full/Formatting/Utf8Formatter.cs b/src/ZeroLog.Impl.Full/Formatting/Utf8Formatter.cs
new file mode 100644
index 0000000..57b568d
--- /dev/null
+++ b/src/ZeroLog.Impl.Full/Formatting/Utf8Formatter.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Text;
+
+namespace ZeroLog.Formatting;
+
+///
+/// A formatter which converts a logged message to UTF-8 encoded text.
+///
+public abstract class Utf8Formatter
+{
+ ///
+ /// This is equal to UTF8Encoding.MaxUtf8BytesPerChar.
+ ///
+ ///
+ /// Code points encoded as 4 bytes in UTF-8 are represented by a surrogate pair in UTF-16.
+ ///
+ internal const int MaxUtf8BytesPerChar = 3;
+
+ private static readonly byte[] _newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);
+
+ private readonly byte[] _buffer = GC.AllocateUninitializedArray(LogManager.OutputBufferSize * MaxUtf8BytesPerChar);
+ private int _position;
+
+ ///
+ /// Formats the given message to text.
+ ///
+ /// The message to format.
+ /// A span representing the text to log.
+ public ReadOnlySpan FormatMessage(LoggedMessage message)
+ {
+ _position = 0;
+ WriteMessage(message);
+ return GetOutput();
+ }
+
+ ///
+ /// Formats the given message to text.
+ ///
+ ///
+ /// Call to append text to the output.
+ ///
+ /// The message to format.
+ protected abstract void WriteMessage(LoggedMessage message);
+
+ ///
+ /// Appends text to the output.
+ ///
+ /// The value to write.
+ protected internal void Write(ReadOnlySpan value)
+ {
+ var charCount = Math.Min(value.Length, _buffer.Length - _position);
+ value.Slice(0, charCount).CopyTo(_buffer.AsSpan(_position));
+ _position += charCount;
+ }
+
+ ///
+ /// Appends text followed by a newline to the output.
+ ///
+ /// The value to write.
+ protected internal void WriteLine(ReadOnlySpan value)
+ {
+ Write(value);
+ WriteLine();
+ }
+
+ ///
+ /// Appends a newline to the output.
+ ///
+ ///
+ /// If the buffer is full, the newline will be inserted by overwriting the last characters.
+ ///
+ protected internal void WriteLine()
+ {
+ if (_position <= _buffer.Length - _newLineBytes.Length)
+ {
+ _newLineBytes.AsSpan().CopyTo(_buffer.AsSpan(_position));
+ _position += _newLineBytes.Length;
+ }
+ else
+ {
+ // Make sure to end the string with a newline
+ _newLineBytes.AsSpan().CopyTo(_buffer.AsSpan(_buffer.Length - _newLineBytes.Length));
+ }
+ }
+
+ ///
+ /// Returns a span of the current output.
+ ///
+ protected internal Span GetOutput()
+ => _buffer.AsSpan(0, _position);
+
+ ///
+ /// Returns a span of the remaining buffer. Call after modifying it.
+ ///
+ protected Span GetRemainingBuffer()
+ => _buffer.AsSpan(_position);
+
+ ///
+ /// Advances the position on the buffer returned by by .
+ ///
+ /// The character count to advance the position by.
+ protected void AdvanceBy(int charCount)
+ => _position += charCount;
+
+ internal byte[] GetBuffer(out int length)
+ {
+ length = _position;
+ return _buffer;
+ }
+}
diff --git a/src/ZeroLog.Impl.Full/LogMessage.Impl.cs b/src/ZeroLog.Impl.Full/LogMessage.Impl.cs
index 064b139..287da21 100644
--- a/src/ZeroLog.Impl.Full/LogMessage.Impl.cs
+++ b/src/ZeroLog.Impl.Full/LogMessage.Impl.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Text;
using System.Threading;
using ZeroLog.Configuration;
@@ -24,11 +25,13 @@ unsafe partial class LogMessage
internal bool IsTruncated => _isTruncated;
internal string? ConstantMessage { get; }
+ internal byte[]? ConstantMessageUtf8 { get; }
internal bool ReturnToPool { get; set; }
internal LogMessage(string message)
{
ConstantMessage = message;
+ ConstantMessageUtf8 = Encoding.UTF8.GetBytes(message);
_strings = Array.Empty();
}
diff --git a/src/ZeroLog.Impl.Full/LogMessage.OutputUtf8.cs b/src/ZeroLog.Impl.Full/LogMessage.OutputUtf8.cs
new file mode 100644
index 0000000..8317bdf
--- /dev/null
+++ b/src/ZeroLog.Impl.Full/LogMessage.OutputUtf8.cs
@@ -0,0 +1,417 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text.Unicode;
+using ZeroLog.Configuration;
+using ZeroLog.Formatting;
+
+namespace ZeroLog;
+
+#if NET8_0_OR_GREATER
+
+unsafe partial class LogMessage
+{
+ [SuppressMessage("ReSharper", "ReplaceSliceWithRangeIndexer")]
+ internal int WriteTo(Span outputBuffer,
+ ZeroLogConfiguration config,
+ FormatType formatType,
+ KeyValueList? keyValueList)
+ {
+ keyValueList?.Clear();
+
+ if (ConstantMessageUtf8 is not null)
+ {
+ var length = Math.Min(ConstantMessageUtf8.Length, outputBuffer.Length);
+ ConstantMessageUtf8.AsSpan(0, length).CopyTo(outputBuffer);
+ return length;
+ }
+
+ var dataPointer = _startOfBuffer;
+ var endOfData = _dataPointer;
+ var bufferIndex = 0;
+
+ while (dataPointer < endOfData)
+ {
+ if (keyValueList != null)
+ {
+ var argType = *(ArgumentType*)dataPointer;
+ if (argType == ArgumentType.KeyString) // KeyString never has a format flag
+ {
+ if (!TryWriteKeyValue(ref dataPointer, keyValueList, config)) // TODO UTF-8
+ goto outputTruncated;
+
+ continue;
+ }
+ }
+
+ var isTruncated = !TryWriteArg(ref dataPointer, outputBuffer.Slice(bufferIndex), out var charsWritten, formatType, config);
+ bufferIndex += charsWritten;
+
+ if (isTruncated)
+ goto outputTruncated;
+ }
+
+ if (_isTruncated)
+ goto outputTruncated;
+
+ return bufferIndex;
+
+ outputTruncated:
+ {
+ var suffix = config.TruncatedMessageSuffixUtf8;
+
+ if (bufferIndex + suffix.Length <= outputBuffer.Length)
+ {
+ // The suffix fits in the remaining buffer.
+ suffix.CopyTo(outputBuffer.Slice(bufferIndex));
+ return bufferIndex + suffix.Length;
+ }
+
+ var idx = outputBuffer.Length - suffix.Length;
+
+ if (idx >= 0)
+ {
+ // The suffix fits at the end of the buffer, but overwrites output data.
+ suffix.CopyTo(outputBuffer.Slice(idx));
+ }
+ else
+ {
+ // The suffix is too large to fit in the buffer.
+ suffix.AsSpan(0, outputBuffer.Length).CopyTo(outputBuffer);
+ }
+
+ return outputBuffer.Length;
+ }
+ }
+
+ private bool TryWriteArg(ref byte* dataPointer, Span outputBuffer, out int bytesWritten, FormatType formatType, ZeroLogConfiguration config)
+ {
+ var argType = *(ArgumentType*)dataPointer;
+ dataPointer += sizeof(ArgumentType);
+
+ var format = default(string);
+
+ if ((argType & ArgumentType.FormatFlag) != 0)
+ {
+ argType &= ~ArgumentType.FormatFlag;
+
+ var stringIndex = *dataPointer;
+ ++dataPointer;
+
+ if (formatType == FormatType.Formatted)
+ format = _strings[stringIndex];
+ }
+
+ if (formatType == FormatType.KeyValue)
+ {
+ format = argType switch
+ {
+ ArgumentType.DateTime => "yyyy-MM-dd HH:mm:ss",
+ ArgumentType.TimeSpan => @"hh\:mm\:ss\.fffffff",
+ ArgumentType.DateOnly => @"yyyy-MM-dd",
+ ArgumentType.TimeOnly => @"HH\:mm\:ss\.fffffff",
+ ArgumentType.DateTimeOffset => @"yyyy-MM-dd HH:mm:ss zzz",
+ _ => null
+ };
+ }
+
+ switch (argType)
+ {
+ case ArgumentType.None:
+ {
+ bytesWritten = 0;
+ return true;
+ }
+
+ case ArgumentType.String:
+ {
+ var stringIndex = *dataPointer;
+ ++dataPointer;
+
+ var value = _strings[stringIndex] ?? string.Empty;
+ var status = Utf8.FromUtf16(value, outputBuffer, out _, out bytesWritten);
+ return status == OperationStatus.Done;
+ }
+
+ case ArgumentType.Null:
+ {
+ var value = config.NullDisplayStringUtf8;
+
+ if (value.Length <= outputBuffer.Length)
+ {
+ value.CopyTo(outputBuffer);
+ bytesWritten = value.Length;
+ }
+ else
+ {
+ bytesWritten = 0;
+ return false;
+ }
+
+ return true;
+ }
+
+ case ArgumentType.Boolean:
+ {
+ var valuePtr = (bool*)dataPointer;
+ dataPointer += sizeof(bool);
+
+ var status = Utf8.FromUtf16(*valuePtr ? bool.TrueString : bool.FalseString, outputBuffer, out _, out var valueBytesWritten);
+ if (status == OperationStatus.Done)
+ {
+ bytesWritten = valueBytesWritten;
+ return true;
+ }
+
+ bytesWritten = 0;
+ return false;
+ }
+
+ case ArgumentType.Byte:
+ {
+ var valuePtr = dataPointer;
+ dataPointer += sizeof(byte);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.SByte:
+ {
+ var valuePtr = (sbyte*)dataPointer;
+ dataPointer += sizeof(sbyte);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Char:
+ {
+ var valuePtr = (char*)dataPointer;
+ dataPointer += sizeof(char);
+
+ var status = Utf8.FromUtf16(new ReadOnlySpan(in *valuePtr), outputBuffer, out _, out var valueBytesWritten);
+ if (status == OperationStatus.Done)
+ {
+ bytesWritten = valueBytesWritten;
+ return true;
+ }
+
+ bytesWritten = 0;
+ return false;
+ }
+
+ case ArgumentType.Int16:
+ {
+ var valuePtr = (short*)dataPointer;
+ dataPointer += sizeof(short);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.UInt16:
+ {
+ var valuePtr = (ushort*)dataPointer;
+ dataPointer += sizeof(ushort);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Int32:
+ {
+ var valuePtr = (int*)dataPointer;
+ dataPointer += sizeof(int);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.UInt32:
+ {
+ var valuePtr = (uint*)dataPointer;
+ dataPointer += sizeof(uint);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Int64:
+ {
+ var valuePtr = (long*)dataPointer;
+ dataPointer += sizeof(long);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.UInt64:
+ {
+ var valuePtr = (ulong*)dataPointer;
+ dataPointer += sizeof(ulong);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.IntPtr:
+ {
+ var valuePtr = (nint*)dataPointer;
+ dataPointer += sizeof(nint);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.UIntPtr:
+ {
+ var valuePtr = (nuint*)dataPointer;
+ dataPointer += sizeof(nuint);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Single:
+ {
+ var valuePtr = (float*)dataPointer;
+ dataPointer += sizeof(float);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Double:
+ {
+ var valuePtr = (double*)dataPointer;
+ dataPointer += sizeof(double);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Decimal:
+ {
+ var valuePtr = (decimal*)dataPointer;
+ dataPointer += sizeof(decimal);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Guid:
+ {
+ var valuePtr = (Guid*)dataPointer;
+ dataPointer += sizeof(Guid);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format);
+ }
+
+ case ArgumentType.DateTime:
+ {
+ var valuePtr = (DateTime*)dataPointer;
+ dataPointer += sizeof(DateTime);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.TimeSpan:
+ {
+ var valuePtr = (TimeSpan*)dataPointer;
+ dataPointer += sizeof(TimeSpan);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.DateOnly:
+ {
+ var valuePtr = (DateOnly*)dataPointer;
+ dataPointer += sizeof(DateOnly);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.TimeOnly:
+ {
+ var valuePtr = (TimeOnly*)dataPointer;
+ dataPointer += sizeof(TimeOnly);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.DateTimeOffset:
+ {
+ var valuePtr = (DateTimeOffset*)dataPointer;
+ dataPointer += sizeof(DateTimeOffset);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, format, CultureInfo.InvariantCulture);
+ }
+
+ case ArgumentType.Enum:
+ {
+ var valuePtr = (EnumArg*)dataPointer;
+ dataPointer += sizeof(EnumArg);
+
+ return valuePtr->TryFormat(outputBuffer, out bytesWritten, config);
+ }
+
+ case ArgumentType.StringSpan:
+ {
+ var lengthInChars = *(int*)dataPointer;
+ dataPointer += sizeof(int);
+
+ var status = Utf8.FromUtf16(new ReadOnlySpan(dataPointer, lengthInChars), outputBuffer, out _, out bytesWritten);
+ dataPointer += lengthInChars * sizeof(char);
+ return status == OperationStatus.Done;
+ }
+
+ case ArgumentType.Utf8StringSpan:
+ {
+ var lengthInBytes = *(int*)dataPointer;
+ dataPointer += sizeof(int);
+
+ var valueBytes = new ReadOnlySpan(dataPointer, lengthInBytes);
+ var lengthToCopy = Math.Min(lengthInBytes, outputBuffer.Length);
+
+ valueBytes.CopyTo(outputBuffer.Slice(0, lengthToCopy));
+ dataPointer += lengthInBytes;
+ bytesWritten = lengthToCopy;
+ return lengthInBytes == lengthToCopy;
+ }
+
+ case ArgumentType.Unmanaged:
+ {
+ var headerPtr = (UnmanagedArgHeader*)dataPointer;
+ dataPointer += sizeof(UnmanagedArgHeader);
+
+ // TODO UTF-8
+ bytesWritten = 0;
+
+ // if (formatType == FormatType.Formatted)
+ // {
+ // if (!headerPtr->TryAppendTo(dataPointer, outputBuffer, out bytesWritten, format, config))
+ // return false;
+ // }
+ // else
+ // {
+ // if (!headerPtr->TryAppendUnformattedTo(dataPointer, outputBuffer, out bytesWritten))
+ // return false;
+ // }
+
+ dataPointer += headerPtr->Size;
+ return true;
+ }
+
+ case ArgumentType.KeyString:
+ {
+ ++dataPointer;
+
+ if (dataPointer < _dataPointer)
+ SkipArg(ref dataPointer);
+
+ bytesWritten = 0;
+ return true;
+ }
+
+ case ArgumentType.EndOfTruncatedMessage:
+ {
+ bytesWritten = 0;
+ return false;
+ }
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+}
+
+#endif
diff --git a/src/ZeroLog.Impl.Full/ZeroLog.Impl.Full.csproj b/src/ZeroLog.Impl.Full/ZeroLog.Impl.Full.csproj
index 43c308f..7b55c7f 100644
--- a/src/ZeroLog.Impl.Full/ZeroLog.Impl.Full.csproj
+++ b/src/ZeroLog.Impl.Full/ZeroLog.Impl.Full.csproj
@@ -1,6 +1,6 @@
- net7.0;net6.0
+ net8.0;net7.0;net6.0
ZeroLog
enable
diff --git a/src/ZeroLog.Tests.NetStandard/ZeroLog.Tests.NetStandard.csproj b/src/ZeroLog.Tests.NetStandard/ZeroLog.Tests.NetStandard.csproj
index f99df16..ad92f20 100644
--- a/src/ZeroLog.Tests.NetStandard/ZeroLog.Tests.NetStandard.csproj
+++ b/src/ZeroLog.Tests.NetStandard/ZeroLog.Tests.NetStandard.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
diff --git a/src/ZeroLog.Tests/SanityChecks.should_export_expected_types.verified.txt b/src/ZeroLog.Tests/SanityChecks.should_export_expected_types.verified.txt
index 816d514..896ce66 100644
--- a/src/ZeroLog.Tests/SanityChecks.should_export_expected_types.verified.txt
+++ b/src/ZeroLog.Tests/SanityChecks.should_export_expected_types.verified.txt
@@ -18,6 +18,7 @@
ZeroLog.Formatting.KeyValueList+Enumerator,
ZeroLog.Formatting.LoggedKeyValue,
ZeroLog.Formatting.LoggedMessage,
+ ZeroLog.Formatting.Utf8Formatter,
ZeroLog.Log,
ZeroLog.Log+DebugInterpolatedStringHandler,
ZeroLog.Log+ErrorInterpolatedStringHandler,
diff --git a/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet6_0.verified.txt b/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet6_0.verified.txt
index ab69ef9..7666719 100644
--- a/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet6_0.verified.txt
+++ b/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet6_0.verified.txt
@@ -130,6 +130,7 @@ namespace ZeroLog.Formatting
{
protected Formatter() { }
protected void AdvanceBy(int charCount) { }
+ public virtual ZeroLog.Formatting.Utf8Formatter? AsUtf8Formatter() { }
public System.ReadOnlySpan FormatMessage(ZeroLog.Formatting.LoggedMessage message) { }
protected System.Span GetOutput() { }
protected System.Span GetRemainingBuffer() { }
@@ -170,11 +171,24 @@ namespace ZeroLog.Formatting
public ZeroLog.LogLevel Level { get; }
public string? LoggerName { get; }
public System.ReadOnlySpan Message { get; }
+ public System.ReadOnlySpan MessageUtf8 { get; }
public System.Threading.Thread? Thread { get; }
public System.DateTime Timestamp { get; }
public ZeroLog.Formatting.LoggedMessage Clone() { }
public override string ToString() { }
}
+ public abstract class Utf8Formatter
+ {
+ protected Utf8Formatter() { }
+ protected void AdvanceBy(int charCount) { }
+ public System.ReadOnlySpan FormatMessage(ZeroLog.Formatting.LoggedMessage message) { }
+ protected System.Span GetOutput() { }
+ protected System.Span GetRemainingBuffer() { }
+ protected void Write(System.ReadOnlySpan value) { }
+ protected void WriteLine() { }
+ protected void WriteLine(System.ReadOnlySpan value) { }
+ protected abstract void WriteMessage(ZeroLog.Formatting.LoggedMessage message);
+ }
}
namespace ZeroLog
{
diff --git a/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet7_0.verified.txt b/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet7_0.verified.txt
index ab69ef9..7666719 100644
--- a/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet7_0.verified.txt
+++ b/src/ZeroLog.Tests/SanityChecks.should_have_expected_public_api.DotNet7_0.verified.txt
@@ -130,6 +130,7 @@ namespace ZeroLog.Formatting
{
protected Formatter() { }
protected void AdvanceBy(int charCount) { }
+ public virtual ZeroLog.Formatting.Utf8Formatter? AsUtf8Formatter() { }
public System.ReadOnlySpan FormatMessage(ZeroLog.Formatting.LoggedMessage message) { }
protected System.Span GetOutput() { }
protected System.Span GetRemainingBuffer() { }
@@ -170,11 +171,24 @@ namespace ZeroLog.Formatting
public ZeroLog.LogLevel Level { get; }
public string? LoggerName { get; }
public System.ReadOnlySpan Message { get; }
+ public System.ReadOnlySpan MessageUtf8 { get; }
public System.Threading.Thread? Thread { get; }
public System.DateTime Timestamp { get; }
public ZeroLog.Formatting.LoggedMessage Clone() { }
public override string ToString() { }
}
+ public abstract class Utf8Formatter
+ {
+ protected Utf8Formatter() { }
+ protected void AdvanceBy(int charCount) { }
+ public System.ReadOnlySpan FormatMessage(ZeroLog.Formatting.LoggedMessage message) { }
+ protected System.Span GetOutput() { }
+ protected System.Span GetRemainingBuffer() { }
+ protected void Write(System.ReadOnlySpan value) { }
+ protected void WriteLine() { }
+ protected void WriteLine(System.ReadOnlySpan value) { }
+ protected abstract void WriteMessage(ZeroLog.Formatting.LoggedMessage message);
+ }
}
namespace ZeroLog
{
diff --git a/src/ZeroLog.Tests/ZeroLog.Tests.csproj b/src/ZeroLog.Tests/ZeroLog.Tests.csproj
index a7d16ec..2979ec2 100644
--- a/src/ZeroLog.Tests/ZeroLog.Tests.csproj
+++ b/src/ZeroLog.Tests/ZeroLog.Tests.csproj
@@ -9,9 +9,9 @@
-
-
-
+
+
+
diff --git a/src/ZeroLog/ZeroLog.csproj b/src/ZeroLog/ZeroLog.csproj
index 55a71af..5e6574d 100644
--- a/src/ZeroLog/ZeroLog.csproj
+++ b/src/ZeroLog/ZeroLog.csproj
@@ -1,6 +1,6 @@
- net7.0;net6.0;netstandard2.0
+ net8.0;net7.0;net6.0;netstandard2.0
enable
true