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