增加日志组件

This commit is contained in:
2026-01-15 18:56:39 +08:00
parent 801ffb25fd
commit 2754cdff15
13 changed files with 1153 additions and 142 deletions

View File

@@ -0,0 +1,157 @@
using Serilog;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Ayay.SerilogLogs
{
/// <summary>
/// 日志清理工具
/// <para>弥补 Serilog 原生清理功能的不足,支持“按天数”和“按总大小”进行全局清理。</para>
/// <para>✅ 已适配多级目录结构,会自动清理删除文件后留下的空文件夹。</para>
/// </summary>
public static class LogCleaner
{
/// <summary>
/// 异步执行清理任务
/// </summary>
/// <param name="opts">配置选项</param>
public static void RunAsync(LogOptions opts)
{
string rootPath = opts.LogRootPath; // 日志根目录
int maxDays = opts.MaxRetentionDays; // 最大保留天数
long maxBytes = opts.MaxTotalLogSize; // 最大总字节数
Task.Run(() =>
{
try
{
CleanUp(rootPath, maxDays, maxBytes);
}
catch (Exception ex)
{
Log.Error(ex, "[LogCleaner] 日志自动清理任务执行失败");
}
});
}
private static void CleanUp(string rootPath, int maxDays, long maxBytes)
{
var dirInfo = new DirectoryInfo(rootPath);
if (!dirInfo.Exists) return;
// =========================================================
// 第一步:清理文件 (递归查找所有子目录)
// =========================================================
// 获取所有 .txt 文件,按最后修改时间升序排列 (最旧的在前面)
// SearchOption.AllDirectories 确保了能扫描到 2026-01-15 这种子文件夹里的内容
var allFiles = dirInfo.GetFiles("*.txt", SearchOption.AllDirectories)
.OrderBy(f => f.LastWriteTime)
.ToList();
if (allFiles.Count == 0) return;
long currentTotalSize = 0;
var cutOffDate = DateTime.Now.Date.AddDays(-maxDays); // 只保留到今天之前的 N 天
int deletedCount = 0;
// --- 策略 A: 按时间清理 ---
for (int i = allFiles.Count - 1; i >= 0; i--)
{
var file = allFiles[i];
if (file.LastWriteTime.Date < cutOffDate)
{
try
{
file.Delete();
allFiles.RemoveAt(i);
deletedCount++;
}
catch { /* 忽略占用 */ }
}
else
{
currentTotalSize += file.Length;
}
}
if (deletedCount > 0)
{
Log.ForContext("SourceContext", LogModules.Core)
.Information("[LogCleaner] 时间策略: 已删除 {Count} 个过期文件", deletedCount);
}
// --- 策略 B: 按总大小清理 ---
if (currentTotalSize > maxBytes)
{
int sizeDeletedCount = 0;
long freedBytes = 0;
foreach (var file in allFiles)
{
if (currentTotalSize <= maxBytes) break;
try
{
long len = file.Length;
file.Delete();
currentTotalSize -= len;
freedBytes += len;
sizeDeletedCount++;
}
catch { /* 忽略占用 */ }
}
if (sizeDeletedCount > 0)
{
Log.ForContext("SourceContext", LogModules.Core)
.Warning("[LogCleaner] 空间策略: 已删除 {Count} 个旧文件, 释放 {SizeMB:F2} MB",
sizeDeletedCount, freedBytes / 1024.0 / 1024.0);
}
}
// =========================================================
// 第二步:清理空文件夹 (新增逻辑)
// =========================================================
// 目的:当 2026-01-01 文件夹里的文件都被删光后,这个文件夹本身也应该被删掉
DeleteEmptyDirectories(dirInfo);
}
/// <summary>
/// 递归删除空文件夹
/// </summary>
private static void DeleteEmptyDirectories(DirectoryInfo dir)
{
// 1. 先递归处理子文件夹
foreach (var subDir in dir.GetDirectories())
{
DeleteEmptyDirectories(subDir);
}
// 2. 检查当前文件夹是否为空 (且不是根目录自己)
try
{
// 重新刷新状态
dir.Refresh();
// 如果没有文件 且 没有子文件夹
if (dir.GetFiles().Length == 0 && dir.GetDirectories().Length == 0)
{
// 注意:不要删除根目录 LogRootPath
// 这里虽然逻辑上递归会删到底,但通常外层调用时传入的是 Logs 根目录,
// 只要 LogBootstrapper 里保证 Logs 存在,这里删掉子目录没问题。
// 为了安全,可以判断一下是否是根目录的直接子级,或者简单地 try catch 忽略根目录无法删除的异常。
dir.Delete();
}
}
catch
{
// 忽略异常 (比如文件夹正被打开,或者试图删除根目录但被占用)
}
}
}
}