diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index 333e848..93c0780 100644 --- a/EonaCat.Logger/EonaCat.Logger.csproj +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -3,7 +3,7 @@ .netstandard2.1; net6.0; net7.0; net8.0; net4.8; icon.ico latest - 1.2.6 + 1.2.7 EonaCat (Jeroen Saey) true EonaCat (Jeroen Saey) @@ -24,7 +24,7 @@ - 1.2.6+{chash:10}.{c:ymd} + 1.2.7+{chash:10}.{c:ymd} true true v[0-9]* diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index cb60394..c2a2f24 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -1,10 +1,13 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using EonaCat.Logger.EonaCatCoreLogger.Internal; +using EonaCat.Logger.Managers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,8 +21,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger; [ProviderAlias("EonaCatFileLogger")] public class FileLoggerProvider : BatchingLoggerProvider { - private static readonly object WriteLock = new(); - private static readonly object RollOverLock = new(); private readonly string _fileNamePrefix; private readonly int _maxFileSize; private readonly int _maxRetainedFiles; @@ -29,6 +30,9 @@ public class FileLoggerProvider : BatchingLoggerProvider private string _logFile; private bool _rollingOver; private int _rollOverCount; + private ConcurrentDictionary _buffer = new ConcurrentDictionary(); + + public event EventHandler OnError; /// /// Creates an instance of the @@ -70,60 +74,97 @@ public class FileLoggerProvider : BatchingLoggerProvider CancellationToken cancellationToken) { Directory.CreateDirectory(_path); - + foreach (var group in messages.GroupBy(GetGrouping)) { LogFile = GetFullName(group.Key); var fileInfo = new FileInfo(LogFile); - if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) - if (_maxRolloverFiles > 0 && _rollOverCount >= 0) - { - if (_rollOverCount < _maxRolloverFiles) - { - var rollOverFile = LogFile.Replace(".log", $"_{++_rollOverCount}.log"); - if (File.Exists(rollOverFile)) File.Delete(rollOverFile); - fileInfo.CopyTo(rollOverFile); - File.WriteAllText(LogFile, string.Empty); - } - else - { - lock (RollOverLock) - { - _rollingOver = true; - MoveRolloverLogFiles(); - _rollingOver = false; - } - } - } - - while (_rollingOver) await Task.Delay(100, cancellationToken).ConfigureAwait(false); - - lock (WriteLock) + var currentMessages = string.Join(string.Empty, group.Select(item => item.Message)); + if (!_buffer.TryAdd(LogFile, currentMessages)) { - var tries = 0; - var completed = false; + _buffer[LogFile] += currentMessages; + } - while (!completed) - try - { - var file = new StreamWriter(LogFile, true); - foreach (var item in group) file.Write(item.Message); - file.Close(); - completed = true; - } - catch (Exception exc) - { - tries++; - Task.Delay(100); - if (tries >= _maxTries) - throw; - } + await MoveRolloverLogFilesAsync(fileInfo, cancellationToken).ConfigureAwait(false); + + if (await TryWriteToFileAsync(cancellationToken)) + { + // Clear buffer on success + _buffer.Clear(); + } + else if (await WriteToTempFileAsync(cancellationToken)) + { + // Fallback to temp file } DeleteOldLogFiles(); } } + private async Task TryWriteToFileAsync(CancellationToken cancellationToken) + { + if (!_buffer.ContainsKey(LogFile)) return true; + + var tries = 0; + var completed = false; + + while (!completed) + { + try + { + using (var file = new StreamWriter(LogFile, true)) + { + await file.WriteAsync(_buffer[LogFile]).ConfigureAwait(false); + } + + completed = true; + _buffer.TryRemove(LogFile, out _); + return true; // Success + } + catch (Exception) + { + tries++; + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + if (tries >= _maxTries) + { + OnError?.Invoke(this, new ErrorMessage { Message = "Cannot write to log folder"}); + return false; // Failure after retries + } + } + } + return false; + } + + private async Task WriteToTempFileAsync(CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(LogFile) || !_buffer.ContainsKey(LogFile)) + { + return false; + } + + var tempLogFolder = Path.Combine(Path.GetTempPath(), "EonaCatLogs"); + var tempLogFile = $"{Path.Combine(tempLogFolder, Path.GetFileNameWithoutExtension(LogFile))}.log"; + + try + { + Directory.CreateDirectory(tempLogFolder); + + // Create new temp file + using (var file = new StreamWriter(tempLogFile, true)) + { + await file.WriteAsync(_buffer[LogFile]).ConfigureAwait(false); + } + + _buffer.TryRemove(LogFile, out _); + return true; + } + catch (Exception exception) + { + OnError?.Invoke(this, new ErrorMessage { Message = "Cannot write to temp folder ", Exception = exception}); + return false; + } + } + private string GetFullName((int Year, int Month, int Day) group) { var hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix); @@ -148,31 +189,78 @@ public class FileLoggerProvider : BatchingLoggerProvider destination.LastAccessTime = origin.LastAccessTime; } + private async Task MoveRolloverLogFilesAsync(FileInfo fileInfo, CancellationToken cancellationToken) + { + try + { + if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) + { + if (_maxRolloverFiles > 0 && _rollOverCount >= 0) + { + if (_rollOverCount < _maxRolloverFiles) + { + var rollOverFile = LogFile.Replace(".log", $"_{++_rollOverCount}.log"); + if (File.Exists(rollOverFile)) + { + File.Delete(rollOverFile); + } + fileInfo.CopyTo(rollOverFile); + File.WriteAllText(LogFile, string.Empty); + } + else + { + lock (_rollOverLock) + { + _rollingOver = true; + MoveRolloverLogFiles(); + _rollingOver = false; + } + } + } + } + + while (_rollingOver) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception exception) + { + OnError?.Invoke(this, new ErrorMessage { Message = "Cannot rollover files" }); + } + } + + private object _rollOverLock { get; set; } = new object(); + /// - /// Rollover logFiles + /// Rollover logFiles /// - protected void MoveRolloverLogFiles() + private void MoveRolloverLogFiles() { if (_maxRolloverFiles > 0 && _rollOverCount >= 0) + { if (_rollOverCount >= _maxRolloverFiles) { var maxRollover = _rollOverCount; - var hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix); + bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix); IEnumerable files; if (hasPrefix) + { files = new DirectoryInfo(_path).GetFiles(_fileNamePrefix + "*").OrderBy(x => x.CreationTime); + } else + { files = new DirectoryInfo(_path).GetFiles("*").OrderBy(x => x.CreationTime); + } - for (var i = files.Count() - 1; i >= 0; i--) + for (int i = files.Count() - 1; i >= 0; i--) { var currentFile = files.ElementAt(i); if (i == 0) { // Temporary move first file - var newFilename2 = Path.GetFileName(currentFile.FullName).Replace(".log", $"_{i + 1}.log"); - MoveFile(currentFile.FullName, - $@"{Path.GetDirectoryName(currentFile.FullName)}\{newFilename2}"); + var newFilename2 = Path.GetFileName(currentFile.FullName).Replace($".log", $"_{i + 1}.log"); + MoveFile(currentFile.FullName, $@"{Path.GetDirectoryName(currentFile.FullName)}\{newFilename2}"); continue; } @@ -186,9 +274,9 @@ public class FileLoggerProvider : BatchingLoggerProvider var newFilename = Path.GetFileName(currentFile.FullName).Replace($"_{i}.log", $"_{i + 1}.log"); MoveFile(currentFile.FullName, $@"{Path.GetDirectoryName(currentFile.FullName)}\{newFilename}"); } - _rollOverCount = 0; } + } } /// diff --git a/EonaCat.Logger/Managers/LogHelper.cs b/EonaCat.Logger/Managers/LogHelper.cs index 9da5bb0..5e766e9 100644 --- a/EonaCat.Logger/Managers/LogHelper.cs +++ b/EonaCat.Logger/Managers/LogHelper.cs @@ -112,12 +112,15 @@ internal static class LogHelper string.IsNullOrWhiteSpace(message)) return; var logLevel = logType.ToLogLevel(); - if (IsLogLevelEnabled(settings, logLevel)) Log(logger, logLevel, message); + if (IsLogLevelEnabled(settings, logType)) + { + Log(logger, logLevel, message); + } } - public static bool IsLogLevelEnabled(LoggerSettings settings, LogLevel logLevel) + private static bool IsLogLevelEnabled(LoggerSettings settings, ELogType logType) { - return settings.MinLogType.ToLogLevel() <= logLevel; + return settings.MinLogType != ELogType.NONE && settings.MinLogType >= logType; } private static void Log(ILogger logger, LogLevel logLevel, string message)