diff --git a/src/NReco.Logging.File/Extensions/DateTimeExtensions.cs b/src/NReco.Logging.File/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..a27ba03 --- /dev/null +++ b/src/NReco.Logging.File/Extensions/DateTimeExtensions.cs @@ -0,0 +1,80 @@ +using System; + +#nullable enable + +namespace NReco.Logging.File.Extensions { + public static class DateTimeExtensions { + public static int GetFormattedLength(this DateTime dateTime) { + const int BaseCharCountInFormatO = 27; + + return BaseCharCountInFormatO + dateTime.Kind switch { + DateTimeKind.Local => 6, + DateTimeKind.Utc => 1, + _ => 0 + }; + } + +#if NETSTANDARD2_0 + public static bool TryFormatO(this DateTime dateTime, Span destination, out int charsWritten) { + var charsRequired = dateTime.GetFormattedLength(); + + if (destination.Length < charsRequired) { + charsWritten = 0; + return false; + } + + charsWritten = charsRequired; + + var year = (uint)dateTime.Year; + var month = (uint)dateTime.Month; + var day = (uint)dateTime.Day; + var hour = (uint)dateTime.Hour; + var minute = (uint)dateTime.Minute; + var second = (uint)dateTime.Second; + var tick = (uint)(dateTime.Ticks - (dateTime.Ticks / TimeSpan.TicksPerSecond * TimeSpan.TicksPerSecond)); + + year.WriteDigits(destination, 4); + destination[4] = '-'; + month.WriteDigits(destination.Slice(5), 2); + destination[7] = '-'; + day.WriteDigits(destination.Slice(8), 2); + destination[10] = 'T'; + hour.WriteDigits(destination.Slice(11), 2); + destination[13] = ':'; + minute.WriteDigits(destination.Slice(14), 2); + destination[16] = ':'; + second.WriteDigits(destination.Slice(17), 2); + destination[19] = '.'; + tick.WriteDigits(destination.Slice(20), 7); + + var kind = dateTime.Kind; + if (kind == DateTimeKind.Local) { + var offset = TimeZoneInfo.Local.GetUtcOffset(dateTime); + var offsetTotalMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); + + var sign = '+'; + if (offsetTotalMinutes < 0) { + sign = '-'; + offsetTotalMinutes = -offsetTotalMinutes; + } + + var offsetHours = Math.DivRem(offsetTotalMinutes, 60, out var offsetMinutes); + + destination[27] = sign; + ((uint)offsetHours).WriteDigits(destination.Slice(28), 2); + destination[30] = ':'; + ((uint)offsetMinutes).WriteDigits(destination.Slice(31), 2); + } + else if (kind == DateTimeKind.Utc) { + destination[27] = 'Z'; + } + + return true; + } +#else + public static bool TryFormatO(this DateTime dateTime, Span destination, out int charsWritten) { + return dateTime.TryFormat(destination, out charsWritten, format: "O"); + } +#endif + } +} diff --git a/src/NReco.Logging.File/Extensions/IntExtensions.cs b/src/NReco.Logging.File/Extensions/IntExtensions.cs new file mode 100644 index 0000000..aa0f406 --- /dev/null +++ b/src/NReco.Logging.File/Extensions/IntExtensions.cs @@ -0,0 +1,41 @@ +using System; + +#nullable enable + +namespace NReco.Logging.File.Extensions { + public static class IntExtensions { + public static int GetFormattedLength(this int value) { + return value == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs((double)value))) + (value > 0 ? 1 : 2); + } + +#if NETSTANDARD2_0 + public static bool TryFormat(this int value, Span destination, out int charsWritten) { + charsWritten = value.GetFormattedLength(); + if (destination.Length < charsWritten) { + charsWritten = 0; + return false; + } + + var dst = destination.Slice(0, charsWritten); + + if (value < 0) { + dst[0] = '-'; + dst = dst.Slice(1); + } + + ((uint)Math.Abs((long)value)).WriteDigits(dst, dst.Length); + return true; + } + + internal static void WriteDigits(this uint value, Span destination, int count) { + for (var cur = count - 1; cur > 0; cur--) { + uint temp = '0' + value; + value /= 10; + destination[cur] = (char)(temp - (value * 10)); + } + + destination[0] = (char)('0' + value); + } +#endif + } +} diff --git a/src/NReco.Logging.File/FileLogger.cs b/src/NReco.Logging.File/FileLogger.cs index a7b9488..0975e1a 100644 --- a/src/NReco.Logging.File/FileLogger.cs +++ b/src/NReco.Logging.File/FileLogger.cs @@ -3,7 +3,7 @@ * NReco file logging provider (https://github.com/nreco/logging) * Copyright 2017 Vitaliy Fedorchenko * Distributed under the MIT license - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,16 +13,11 @@ #endregion using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.IO; -using System.Collections.Concurrent; +using System.Buffers; using System.Text; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Configuration; - +using NReco.Logging.File.Extensions; namespace NReco.Logging.File { /// @@ -46,21 +41,16 @@ public bool IsEnabled(LogLevel logLevel) { } string GetShortLogLevel(LogLevel logLevel) { - switch (logLevel) { - case LogLevel.Trace: - return "TRCE"; - case LogLevel.Debug: - return "DBUG"; - case LogLevel.Information: - return "INFO"; - case LogLevel.Warning: - return "WARN"; - case LogLevel.Error: - return "FAIL"; - case LogLevel.Critical: - return "CRIT"; - } - return logLevel.ToString().ToUpper(); + return logLevel switch { + LogLevel.Trace => "TRCE", + LogLevel.Debug => "DBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "FAIL", + LogLevel.Critical => "CRIT", + LogLevel.None => "NONE", + _ => logLevel.ToString().ToUpper(), + }; } public void Log(LogLevel logLevel, EventId eventId, TState state, @@ -79,34 +69,71 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, if (!LoggerPrv.Options.FilterLogEntry(new LogMessage(logName, logLevel, eventId, message, exception))) return; - if (LoggerPrv.FormatLogEntry!=null) { + if (LoggerPrv.FormatLogEntry != null) { LoggerPrv.WriteEntry(LoggerPrv.FormatLogEntry( new LogMessage(logName, logLevel, eventId, message, exception))); - } else { - // default formatting logic - var logBuilder = new StringBuilder(); - if (!string.IsNullOrEmpty(message)) { - DateTime timeStamp = LoggerPrv.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now; - logBuilder.Append(timeStamp.ToString("o")); - logBuilder.Append('\t'); - logBuilder.Append(GetShortLogLevel(logLevel)); - logBuilder.Append("\t["); - logBuilder.Append(logName); - logBuilder.Append("]"); - logBuilder.Append("\t["); - logBuilder.Append(eventId); - logBuilder.Append("]\t"); - logBuilder.Append(message); + } + else { + const int MaxStackAllocatedBufferLength = 256; + DateTime timeStamp = LoggerPrv.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now; + var logMessageLength = CalculateLogMessageLength(timeStamp, eventId, message); + char[] charBuffer = null; + try { + Span buffer = logMessageLength <= MaxStackAllocatedBufferLength + ? stackalloc char[MaxStackAllocatedBufferLength] + : (charBuffer = ArrayPool.Shared.Rent(logMessageLength)); + + FormatLogEntryDefault(buffer, timeStamp, message, logLevel, eventId, exception); } - - if (exception != null) { - // exception message - logBuilder.AppendLine(exception.ToString()); + finally { + if (charBuffer is not null) { + ArrayPool.Shared.Return(charBuffer); + } } - LoggerPrv.WriteEntry(logBuilder.ToString()); } } - } + private void FormatLogEntryDefault(Span buffer, DateTime timeStamp, string message, + LogLevel logLevel, EventId eventId, Exception exception) { + // default formatting logic + using var logBuilder = new ValueStringBuilder(buffer); + if (!string.IsNullOrEmpty(message)) { + timeStamp.TryFormatO(logBuilder.RemainingRawChars, out var charsWritten); + logBuilder.AppendSpan(charsWritten); + logBuilder.Append('\t'); + logBuilder.Append(GetShortLogLevel(logLevel)); + logBuilder.Append("\t["); + logBuilder.Append(logName); + logBuilder.Append("]\t["); + if (eventId.Name is not null) { + logBuilder.Append(eventId.Name); + } + else { + eventId.Id.TryFormat(logBuilder.RemainingRawChars, out charsWritten); + logBuilder.AppendSpan(charsWritten); + } + logBuilder.Append("]\t"); + logBuilder.Append(message); + } + + if (exception != null) { + // exception message + logBuilder.Append(exception.ToString()); + logBuilder.Append(Environment.NewLine); + } + LoggerPrv.WriteEntry(logBuilder.ToString()); + } + private int CalculateLogMessageLength(DateTime timeStamp, EventId eventId, string message) { + return timeStamp.GetFormattedLength() + + 1 /* '\t' */ + + 4 /* GetShortLogLevel */ + + 2 /* "\t[" */ + + (logName?.Length ?? 0) + + 3 /* "]\t[" */ + + (eventId.Name?.Length ?? eventId.Id.GetFormattedLength()) + + 2 /* "]\t" */ + + (message?.Length ?? 0); + } + } } diff --git a/src/NReco.Logging.File/LogMessage.cs b/src/NReco.Logging.File/LogMessage.cs index 1a6c72f..79a51c1 100644 --- a/src/NReco.Logging.File/LogMessage.cs +++ b/src/NReco.Logging.File/LogMessage.cs @@ -3,7 +3,7 @@ * NReco file logging provider (https://github.com/nreco/logging) * Copyright 2017 Vitaliy Fedorchenko * Distributed under the MIT license - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,13 +13,11 @@ #endregion using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.Logging; namespace NReco.Logging.File { - public struct LogMessage { + public readonly struct LogMessage { public readonly string LogName; public readonly string Message; public readonly LogLevel LogLevel; diff --git a/src/NReco.Logging.File/NReco.Logging.File.csproj b/src/NReco.Logging.File/NReco.Logging.File.csproj index 341b401..6a2228a 100644 --- a/src/NReco.Logging.File/NReco.Logging.File.csproj +++ b/src/NReco.Logging.File/NReco.Logging.File.csproj @@ -2,12 +2,12 @@ Lightweight file logging provider implementation for NET6 / NET8+ / .NET Core without dependencies on logging frameworks. - + How to use: using NReco.Logging.File; -services.AddLogging(loggingBuilder => { +services.AddLogging(loggingBuilder => { loggingBuilder.AddFile("app.log", append:true); }); @@ -19,6 +19,7 @@ More details and examples: https://github.com/nreco/logging 1.2.1 Vitalii Fedorchenko netstandard2.0;net6.0;net8.0 + 12.0 true NReco.Logging.File NReco.Logging.File @@ -27,10 +28,10 @@ More details and examples: https://github.com/nreco/logging https://github.com/nreco/logging https://raw.githubusercontent.com/nreco/logging/master/LICENSE How to use: https://github.com/nreco/logging - + Version 1.2.1 changes: - Added different rolling name conventions #66 (unix-style is supported with 'Descending' convention) - + Version 1.2.0 changes: - NReco's FileLoggerExtensions should NOT be in the Microsoft.Extensions.Logging namespace #61 - Added net6 and net8 builds to reference appropriate Microsoft.Logging.Extensions versions @@ -65,7 +66,7 @@ Version 1.1.0 changes: Version 1.0.5 changes: - log file folder is created automatically if not exists - environment variables are expanded in the file path - + Version 1.0.4 changes: - added "File" provider alias for MVC Core 2 filtering rules - added 'rolling file' behaviour controlled with new FileLoggerProvider properties (FileSizeLimitBytes and MaxRollingFiles) @@ -81,11 +82,11 @@ Version 1.0.4 changes: false NReco.Logging.File.snk - + - - - + + + @@ -97,7 +98,7 @@ Version 1.0.4 changes: - + - + diff --git a/src/NReco.Logging.File/ValueStringBuilder.cs b/src/NReco.Logging.File/ValueStringBuilder.cs new file mode 100644 index 0000000..048d38b --- /dev/null +++ b/src/NReco.Logging.File/ValueStringBuilder.cs @@ -0,0 +1,277 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#nullable enable + +namespace System.Text { + internal ref partial struct ValueStringBuilder { + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length { + get => _pos; + set { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public void EnsureCapacity(int capacity) { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_chars.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) { + if (terminate) { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] { + get { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public override string ToString() { + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// Returns a span representing the remaining space available in the underlying storage of the builder. + public Span RemainingRawChars => _chars.Slice(_pos); + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) { + if (terminate) { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) { + charsWritten = _pos; + Dispose(); + return true; + } + else { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) { + if (_pos > _chars.Length - count) { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string? s) { + if (s == null) { + return; + } + + int count = s.Length; + + if (_pos > (_chars.Length - count)) { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s +#if !NET + .AsSpan() +#endif + .CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) { + int pos = _pos; + Span chars = _chars; + if ((uint)pos < (uint)chars.Length) { + chars[pos] = c; + _pos = pos + 1; + } + else { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) { + if (s == null) { + return; + } + + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else { + AppendSlow(s); + } + } + + private void AppendSlow(string s) { + int pos = _pos; + if (pos > _chars.Length - s.Length) { + Grow(s.Length); + } + + s +#if !NET + .AsSpan() +#endif + .CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) { + if (_pos > _chars.Length - count) { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) { + dst[i] = c; + } + _pos += count; + } + + public void Append(scoped ReadOnlySpan value) { + int pos = _pos; + if (pos > _chars.Length - value.Length) { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) { + int origPos = _pos; + if (origPos > _chars.Length - length) { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + int newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + char[] poolArray = ArrayPool.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() { + char[]? toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) { + ArrayPool.Shared.Return(toReturn); + } + } + } +} \ No newline at end of file diff --git a/test/NReco.Logging.Tests/DateTimeExtensionsTests.cs b/test/NReco.Logging.Tests/DateTimeExtensionsTests.cs new file mode 100644 index 0000000..095accd --- /dev/null +++ b/test/NReco.Logging.Tests/DateTimeExtensionsTests.cs @@ -0,0 +1,114 @@ +using NReco.Logging.File.Extensions; +using System; +using Xunit; + +namespace NReco.Logging.Tests { + public class DateTimeExtensionsTests { + + [Fact] + public void GetFormattedLengthOfDateTimeNow() { + var result = DateTime.Now.GetFormattedLength(); + + Assert.Equal(33, result); + } + + [Fact] + public void GetFormattedLengthOfDateTimeUtcNow() { + var result = DateTime.UtcNow.GetFormattedLength(); + + Assert.Equal(28, result); + } + + [Fact] + public void GetFormattedLengthOfDateTimeKindUnspecified() { + var result = new DateTime(2024, 01, 01, 01, 01, 01, DateTimeKind.Unspecified).GetFormattedLength(); + + Assert.Equal(27, result); + } + + [Fact] + public void GetFormattedLengthOfDefaultDateTime() { + var result = default(DateTime).GetFormattedLength(); + + Assert.Equal(27, result); + } + + [Fact] + public void TryFormatLocal() { + var testValue = DateTime.Now; + var expected = $"{testValue:O}"; + + Span span = stackalloc char[33]; + var result = testValue.TryFormatO(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(33, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatUtc() { + var testValue = DateTime.UtcNow; + var expected = $"{testValue:O}\0\0\0\0\0"; + + Span span = stackalloc char[33]; + var result = testValue.TryFormatO(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(28, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatUnspecified() { + var testValue = new DateTime(2024, 1, 1, 1, 1, 1, DateTimeKind.Unspecified); + var expected = $"{testValue:O}\0\0\0\0\0\0"; + + Span span = stackalloc char[33]; + var result = testValue.TryFormatO(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(27, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatDefault() { + var testValue = default(DateTime); + var expected = $"{testValue:O}\0\0\0\0\0\0"; + + Span span = stackalloc char[33]; + var result = testValue.TryFormatO(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(27, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatMaxValue() { + var testValue = DateTime.MaxValue; + var expected = $"{testValue:O}\0\0\0\0\0\0"; + + Span span = stackalloc char[33]; + var result = testValue.TryFormatO(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(27, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatIntoTooShortSpan() { + var testValue = DateTime.Now; + ReadOnlySpan expected = stackalloc char[25]; + + Span span = stackalloc char[25]; + var result = testValue.TryFormatO(span, out var charsWritten); + + Assert.False(result); + Assert.Equal(0, charsWritten); + Assert.True(span.SequenceEqual(expected)); + } + } +} diff --git a/test/NReco.Logging.Tests/FileProviderTests.cs b/test/NReco.Logging.Tests/FileProviderTests.cs index 2ddb9bc..135adaf 100644 --- a/test/NReco.Logging.Tests/FileProviderTests.cs +++ b/test/NReco.Logging.Tests/FileProviderTests.cs @@ -73,7 +73,7 @@ public void WriteToFileAndAppend() { factory.Dispose(); Assert.Equal(3, System.IO.File.ReadAllLines(tmpFile).Length); - + } finally { System.IO.File.Delete(tmpFile); } @@ -225,7 +225,7 @@ public void WriteDescendingRollingFile() { logger.LogInformation("TEST 0123456789"); if (i % 50 == 0) { System.Threading.Thread.Sleep(20); // give some time for log writer to handle the queue - } + } } factory.Dispose(); Assert.Equal(5, Directory.GetFiles(tmpFileDir, "test*.log").Length); @@ -293,14 +293,14 @@ public void WriteConcurrent() { [Fact] public void CreateDirectoryAutomatically() { - + var tmpFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "testfile.log"); try { var factory = new LoggerFactory(); - + factory.AddProvider(new FileLoggerProvider(tmpFile, new FileLoggerOptions())); - + var logger = factory.CreateLogger("TEST"); logger.LogInformation("Line1"); factory.Dispose(); @@ -363,7 +363,7 @@ public void CustomLogFileNameFormatter() { if (i > 0 && i % 5 == 0) Thread.Sleep(1000); // log writer works in another thread. Let him to process log messages in queue. logger.LogInformation("Line" + (i + 1).ToString()); - + } factory.Dispose(); @@ -417,19 +417,19 @@ public void FileOpenErrorHandling() { }); var errorHandled = false; - var factory2 = new LoggerFactory(); - toDispose.Add(factory2); + var factory3 = new LoggerFactory(); + toDispose.Add(factory3); var altLogFileName = Path.Combine(tmpDir, "testfile_after_err.log"); - factory2.AddProvider(new FileLoggerProvider(logFileName, new FileLoggerOptions() { + factory3.AddProvider(new FileLoggerProvider(logFileName, new FileLoggerOptions() { HandleFileError = (err) => { errorHandled = true; err.UseNewLogFileName(altLogFileName); } })); - var logger2 = factory2.CreateLogger("TEST"); - writeSomethingToLogger(logger2, 15); + var logger3 = factory3.CreateLogger("TEST"); + writeSomethingToLogger(logger3, 15); Assert.True(errorHandled); - factory2.Dispose(); + factory3.Dispose(); // ensure that alt file name was used var altLogFileInfo = new FileInfo(altLogFileName); Assert.True(altLogFileInfo.Exists); @@ -471,7 +471,7 @@ public void FileWriteErrorHandling(bool useNewLogFile) { var logFileWr = fileLogPrv.GetType() .GetField("fWriter", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) .GetValue(fileLogPrv); - // close file handler, this will cause an exception inside FileLoggerProvider.ProcessQueue + // close file handler, this will cause an exception inside FileLoggerProvider.ProcessQueue var logFileStream = (Stream)logFileWr.GetType() .GetField("LogFileStream", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) .GetValue(logFileWr); @@ -496,7 +496,7 @@ public void CustomFilterLogEntry() { var logFileName = Path.Combine(tmpDir, "testfile.log"); var factory = new LoggerFactory(); try { - factory.AddProvider(new FileLoggerProvider(logFileName, new FileLoggerOptions() { + factory.AddProvider(new FileLoggerProvider(logFileName, new FileLoggerOptions() { FilterLogEntry = (logEntry) => { return logEntry.LogName == "TEST"; } @@ -515,7 +515,7 @@ public void CustomFilterLogEntry() { } finally { CleanupTempDir(tmpDir, new[] { factory }); } - } + } } } diff --git a/test/NReco.Logging.Tests/IntExtensionsTests.cs b/test/NReco.Logging.Tests/IntExtensionsTests.cs new file mode 100644 index 0000000..7ac0678 --- /dev/null +++ b/test/NReco.Logging.Tests/IntExtensionsTests.cs @@ -0,0 +1,77 @@ +using NReco.Logging.File.Extensions; +using System; +using Xunit; + +namespace NReco.Logging.Tests { + public class IntExtensionsTests { + + [Fact] + public void GetFormattedLengthOfZero() { + var result = 0.GetFormattedLength(); + + Assert.Equal(1, result); + } + + [Fact] + public void GetFormattedLengthOfMinValue() { + var result = int.MinValue.GetFormattedLength(); + + Assert.Equal(11, result); + } + + [Fact] + public void GetFormattedLengthOfMaxValue() { + var result = int.MaxValue.GetFormattedLength(); + + Assert.Equal(10, result); + } + + [Fact] + public void TryFormatZero() { + const string expected = "0\0\0\0\0\0\0\0\0\0\0"; + + Span span = stackalloc char[11]; + var result = 0.TryFormat(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(1, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatMinValue() { + const string expected = "-2147483648"; + + Span span = stackalloc char[11]; + var result = int.MinValue.TryFormat(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(11, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatMaxValue() { + const string expected = "2147483647\0"; + + Span span = stackalloc char[11]; + var result = int.MaxValue.TryFormat(span, out var charsWritten); + + Assert.True(result); + Assert.Equal(10, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + + [Fact] + public void TryFormatIntoTooShortSpan() { + const string expected = "\0\0\0\0\0"; + + Span span = stackalloc char[5]; + var result = int.MaxValue.TryFormat(span, out var charsWritten); + + Assert.False(result); + Assert.Equal(0, charsWritten); + Assert.True(span.SequenceEqual(expected.AsSpan())); + } + } +} diff --git a/test/NReco.Logging.Tests/NReco.Logging.Tests.csproj b/test/NReco.Logging.Tests/NReco.Logging.Tests.csproj index b21e037..f1ce1ea 100644 --- a/test/NReco.Logging.Tests/NReco.Logging.Tests.csproj +++ b/test/NReco.Logging.Tests/NReco.Logging.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net481 NReco.Logging.Tests false