using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using PCL.Core.Utils.Exts; namespace PCL.Core.Logging; public sealed class Logger : IAsyncDisposable { public Logger(LoggerConfiguration configuration) { Configuration = configuration; _CreateNewFile(); _processingTask = _ProcessLogQueueAsync(); } // Data stream private StreamWriter? _currentStream; private FileStream? _currentFile; private readonly List _files = []; // Statis private long _droppedCount; public long DroppedLogCount => Interlocked.Read(ref _droppedCount); // Processor private readonly Task _processingTask; private readonly Channel _logChannel = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true }); public ReadOnlyCollection CurrentLogFiles => _files.AsReadOnly(); public LoggerConfiguration Configuration { get; } private void _CreateNewFile() { var now = DateTime.Now; var nameFormat = (Configuration.FileNameFormat ?? $"Launch-{now.ToString("yyyy-M-d", CultureInfo.InvariantCulture)}-{{0}}") + ".log"; var filename = nameFormat.Replace("{0}", now.ToString("HHmmssfff", CultureInfo.InvariantCulture)); var filePath = Path.Combine(Configuration.StoreFolder, filename); _files.Add(filePath); var lastWriter = _currentStream; var lastFile = _currentFile; Directory.CreateDirectory(Configuration.StoreFolder); _currentFile = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); _currentStream = new StreamWriter(_currentFile); _ = Task.Run(async () => { if (lastWriter is not null) try { await lastWriter.DisposeAsync().ConfigureAwait(false); } catch (Exception) { /* Don't care */ } if (lastFile is not null) try { await lastFile.DisposeAsync().ConfigureAwait(false); } catch (Exception) { /* Don't care */ } if (!Configuration.AutoDeleteOldFile) return; var logFiles = Directory.GetFiles( Configuration.StoreFolder, "*.log", SearchOption.TopDirectoryOnly); var needToDelete = logFiles.Select(x => new FileInfo(x)) .OrderBy(x => x.CreationTime) .Take(logFiles.Length - Configuration.MaxKeepOldFile); foreach (var logFile in needToDelete) logFile.Delete(); }); } public void Trace(string message) => Log($"[{_GetTimeFormatted()}] [TRA] {message}"); public void Debug(string message) => Log($"[{_GetTimeFormatted()}] [DBG] {message}"); public void Info(string message) => Log($"[{_GetTimeFormatted()}] [INFO] {message}"); public void Warn(string message) => Log($"[{_GetTimeFormatted()}] [WARN] {message}"); public void Error(string message) => Log($"[{_GetTimeFormatted()}] [ERR!] {message}"); public void Fatal(string message) => Log($"[{_GetTimeFormatted()}] [FTL!] {message}"); private static string _GetTimeFormatted() => $"{DateTime.Now:HH:mm:ss.fff}"; public void Log(string message) { if (_disposed) return; if (!_logChannel.Writer.TryWrite(message)) { Interlocked.Increment(ref _droppedCount); Console.WriteLine($"Log dropped error: {message}"); } } private async Task _ProcessLogQueueAsync() { const int maxBatchLines = 198; var writeTimeout = TimeSpan.FromMilliseconds(325); var batch = new StringBuilder(4096); var lineCount = 0u; var lastFlush = Stopwatch.GetTimestamp(); try { while (!_disposed || _logChannel.Reader.TryPeek(out _)) { if (_logChannel.Reader.TryRead(out var message)) { #if DEBUG message = message.ReplaceLineBreak("\r\n"); Console.WriteLine(message); System.Diagnostics.Debug.WriteLine(message); #endif batch.AppendLine(message); lineCount++; var elapsed = Stopwatch.GetElapsedTime(lastFlush); if (lineCount >= maxBatchLines || elapsed > writeTimeout) { await DoRefreshAsync().ConfigureAwait(false); } } else { if (lineCount != 0) { await DoRefreshAsync().ConfigureAwait(false); } await Task.Delay(80).ConfigureAwait(false); } } async Task DoRefreshAsync() { await _DoWriteAsync(batch).ConfigureAwait(false); batch.Clear(); lineCount = 0; lastFlush = Stopwatch.GetTimestamp(); } } catch (Exception e) { // 出错了先干到标准输出流中吧 Orz Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occurred while processing log queue: {e.Message}"); throw; } } private async Task _DoWriteAsync(StringBuilder ctx) { try { if (_currentFile?.Length >= Configuration.MaxFileSize) { _CreateNewFile(); } await _currentStream!.WriteAsync(ctx).ConfigureAwait(false); await _currentStream.FlushAsync().ConfigureAwait(false); } catch (Exception e) { Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occurred while writing log file: {e.Message}"); await File.AppendAllTextAsync(Path.Combine(Configuration.StoreFolder, "Error.log"), $"[{_GetTimeFormatted}] LogCycle Error: {e}\n"); throw; } } private bool _disposed; public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; _logChannel.Writer.Complete(); await _processingTask.ConfigureAwait(false); if (_currentStream is not null) await _currentStream.DisposeAsync().ConfigureAwait(false); if (_currentFile is not null) await _currentFile.DisposeAsync().ConfigureAwait(false); } }