157 lines
5.8 KiB
C#
157 lines
5.8 KiB
C#
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
|
|
{
|
|
// 忽略异常 (比如文件夹正被打开,或者试图删除根目录但被占用)
|
|
}
|
|
}
|
|
}
|
|
} |