- 使用 ConcurrentQueue + 后台线程批量写入 - 新增 LogLevel、LogCategory、LogEntry、LogFormatter - 新增 GameLog 业务日志入口 - 新增 LogSettingsProfiles 平台化配置 - 支持按 session 分文件、大小轮转与队列满时低优先级丢弃
261 lines
7.5 KiB
C#
261 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace AibisDream.Kit
|
|
{
|
|
internal sealed class FileLogger
|
|
{
|
|
private readonly ConcurrentQueue<LogEntry> _queue = new();
|
|
private readonly AutoResetEvent _wakeEvent = new(false);
|
|
private readonly ManualResetEventSlim _drainedEvent = new(true);
|
|
private readonly List<LogEntry> _batch = new();
|
|
private readonly string _directory;
|
|
private readonly LogSettings _settings;
|
|
private readonly Thread _worker;
|
|
private readonly object _writerLock = new();
|
|
|
|
private volatile bool _isRunning = true;
|
|
private StreamWriter _writer;
|
|
private string _currentFilePath;
|
|
private long _currentFileBytes;
|
|
private int _queuedCount;
|
|
private int _droppedLowPriorityCount;
|
|
|
|
public string CurrentFilePath => _currentFilePath;
|
|
|
|
public FileLogger(string directory, LogSettings settings)
|
|
{
|
|
_directory = directory;
|
|
_settings = settings ?? LogSettings.CreateDefault();
|
|
Directory.CreateDirectory(_directory);
|
|
DeleteOldFiles();
|
|
OpenNewFile();
|
|
|
|
_worker = new Thread(WriterLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "AIBIS File Logger"
|
|
};
|
|
_worker.Start();
|
|
}
|
|
|
|
public void Write(string content)
|
|
{
|
|
Write(LogEntry.System(LogLevel.Info, LogCategory.General, content));
|
|
}
|
|
|
|
public void Write(LogEntry entry)
|
|
{
|
|
if (!_isRunning || !_settings.ShouldWrite(entry.Level))
|
|
return;
|
|
|
|
if (TryEnqueue(entry))
|
|
{
|
|
if (entry.Level >= LogLevel.Error)
|
|
_wakeEvent.Set();
|
|
return;
|
|
}
|
|
|
|
if (entry.Level < LogLevel.Error)
|
|
Interlocked.Increment(ref _droppedLowPriorityCount);
|
|
}
|
|
|
|
public void Flush(TimeSpan timeout)
|
|
{
|
|
_wakeEvent.Set();
|
|
_drainedEvent.Wait(timeout);
|
|
|
|
lock (_writerLock)
|
|
{
|
|
_writer?.Flush();
|
|
}
|
|
}
|
|
|
|
public void OnDestroy()
|
|
{
|
|
_isRunning = false;
|
|
_wakeEvent.Set();
|
|
|
|
if (!_worker.Join(_settings.ShutdownDrainMilliseconds))
|
|
Flush(TimeSpan.FromMilliseconds(_settings.ShutdownDrainMilliseconds));
|
|
|
|
lock (_writerLock)
|
|
{
|
|
_writer?.Flush();
|
|
_writer?.Dispose();
|
|
_writer = null;
|
|
}
|
|
|
|
if (!_worker.IsAlive)
|
|
{
|
|
_wakeEvent.Dispose();
|
|
_drainedEvent.Dispose();
|
|
}
|
|
}
|
|
|
|
private bool TryEnqueue(LogEntry entry)
|
|
{
|
|
while (true)
|
|
{
|
|
var current = Volatile.Read(ref _queuedCount);
|
|
if (current >= _settings.QueueCapacity && entry.Level < LogLevel.Error)
|
|
return false;
|
|
|
|
if (Interlocked.CompareExchange(ref _queuedCount, current + 1, current) == current)
|
|
break;
|
|
}
|
|
|
|
_drainedEvent.Reset();
|
|
_queue.Enqueue(entry);
|
|
_wakeEvent.Set();
|
|
return true;
|
|
}
|
|
|
|
private void WriterLoop()
|
|
{
|
|
while (_isRunning || !_queue.IsEmpty)
|
|
{
|
|
_wakeEvent.WaitOne(_settings.FlushIntervalMilliseconds);
|
|
DrainQueue();
|
|
}
|
|
|
|
DrainQueue();
|
|
}
|
|
|
|
private void DrainQueue()
|
|
{
|
|
_batch.Clear();
|
|
while (_batch.Count < _settings.BatchSize && _queue.TryDequeue(out var entry))
|
|
{
|
|
Interlocked.Decrement(ref _queuedCount);
|
|
_batch.Add(entry);
|
|
}
|
|
|
|
var dropped = Interlocked.Exchange(ref _droppedLowPriorityCount, 0);
|
|
if (dropped > 0)
|
|
{
|
|
_batch.Add(LogEntry.System(
|
|
LogLevel.Warning,
|
|
LogCategory.General,
|
|
$"Dropped {dropped} low-priority log entries because the queue was full."));
|
|
}
|
|
|
|
if (_batch.Count == 0)
|
|
{
|
|
if (_queue.IsEmpty)
|
|
_drainedEvent.Set();
|
|
return;
|
|
}
|
|
|
|
lock (_writerLock)
|
|
{
|
|
foreach (var entry in _batch)
|
|
{
|
|
WriteEntry(entry);
|
|
}
|
|
|
|
_writer?.Flush();
|
|
}
|
|
|
|
if (!_queue.IsEmpty)
|
|
_wakeEvent.Set();
|
|
|
|
if (_queue.IsEmpty)
|
|
_drainedEvent.Set();
|
|
}
|
|
|
|
private void WriteEntry(LogEntry entry)
|
|
{
|
|
try
|
|
{
|
|
var line = LogFormatter.Format(entry);
|
|
var byteCount = Encoding.UTF8.GetByteCount(line) + Environment.NewLine.Length;
|
|
if (_currentFileBytes + byteCount > _settings.MaxFileBytes)
|
|
OpenNewFile();
|
|
|
|
_writer.WriteLine(line);
|
|
_currentFileBytes += byteCount;
|
|
}
|
|
catch
|
|
{
|
|
_isRunning = false;
|
|
}
|
|
}
|
|
|
|
private void OpenNewFile()
|
|
{
|
|
_writer?.Flush();
|
|
_writer?.Dispose();
|
|
|
|
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
|
var platform = _settings.Platform;
|
|
var sessionId = _settings.SessionId;
|
|
var baseFileName = $"{timestamp}_{platform}_v{_settings.AppVersion}_session-{sessionId}";
|
|
_currentFilePath = BuildUniquePath(baseFileName);
|
|
|
|
_writer = new StreamWriter(new FileStream(_currentFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read), Encoding.UTF8)
|
|
{
|
|
AutoFlush = false
|
|
};
|
|
_currentFileBytes = 0;
|
|
DeleteOldFiles();
|
|
}
|
|
|
|
private void DeleteOldFiles()
|
|
{
|
|
var directory = new DirectoryInfo(_directory);
|
|
if (!directory.Exists)
|
|
return;
|
|
|
|
var files = directory.GetFiles("*.log");
|
|
Array.Sort(files, (a, b) => b.LastWriteTimeUtc.CompareTo(a.LastWriteTimeUtc));
|
|
|
|
long totalBytes = 0;
|
|
for (var i = 0; i < files.Length; i++)
|
|
{
|
|
totalBytes += files[i].Length;
|
|
var shouldDelete = i >= _settings.MaxSessionFiles || totalBytes > _settings.MaxTotalBytes;
|
|
if (!shouldDelete)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
files[i].Delete();
|
|
}
|
|
catch
|
|
{
|
|
// Best-effort cleanup only.
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string SanitizeFileName(string fileName)
|
|
{
|
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
|
{
|
|
fileName = fileName.Replace(invalid, '_');
|
|
}
|
|
|
|
return fileName;
|
|
}
|
|
|
|
private string BuildUniquePath(string baseFileName)
|
|
{
|
|
var safeBase = SanitizeFileName(baseFileName);
|
|
var path = Path.Combine(_directory, safeBase + ".log");
|
|
var index = 1;
|
|
while (File.Exists(path))
|
|
{
|
|
path = Path.Combine(_directory, $"{safeBase}_{index}.log");
|
|
index++;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
}
|
|
}
|