Files
EmailBill/WebApi/Controllers/LogController.cs
孙诚 8ba279e957
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
优化日志加载和清理功能,增加流式读取和定期清理日志服务
2025-12-30 11:07:14 +08:00

359 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class LogController(ILogger<LogController> logger) : ControllerBase
{
/// <summary>
/// 获取日志列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<LogEntry>> GetListAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 50,
[FromQuery] string? searchKeyword = null,
[FromQuery] string? logLevel = null,
[FromQuery] string? date = null
)
{
try
{
// 获取日志目录
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
if (!Directory.Exists(logDirectory))
{
return new PagedResponse<LogEntry>
{
Success = true,
Data = [],
Total = 0,
Message = "日志目录不存在"
};
}
// 确定要读取的日志文件
string logFilePath;
if (!string.IsNullOrEmpty(date))
{
logFilePath = Path.Combine(logDirectory, $"log-{date}.txt");
}
else
{
// 默认读取今天的日志
var today = DateTime.Now.ToString("yyyyMMdd");
logFilePath = Path.Combine(logDirectory, $"log-{today}.txt");
}
// 检查文件是否存在
if (!System.IO.File.Exists(logFilePath))
{
return new PagedResponse<LogEntry>
{
Success = true,
Data = [],
Total = 0,
Message = "日志文件不存在"
};
}
// 流式读取日志(边读边过滤,满足条件后停止)
var (logEntries, total) = await ReadLogsStreamAsync(
logFilePath,
pageIndex,
pageSize,
searchKeyword,
logLevel);
var pagedData = logEntries;
return new PagedResponse<LogEntry>
{
Success = true,
Data = pagedData.ToArray(),
Total = total,
Message = "获取日志成功"
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取日志失败");
return new PagedResponse<LogEntry>
{
Success = false,
Data = [],
Total = 0,
Message = $"获取日志失败: {ex.Message}"
};
}
}
/// <summary>
/// 获取可用的日志日期列表
/// </summary>
[HttpGet]
public IActionResult GetAvailableDates()
{
try
{
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
if (!Directory.Exists(logDirectory))
{
return Ok(new { success = true, data = new List<string>() });
}
var logFiles = Directory.GetFiles(logDirectory, "log-*.txt");
var dates = logFiles
.Select(f => Path.GetFileNameWithoutExtension(f))
.Select(name => name.Replace("log-", ""))
.OrderByDescending(d => d)
.ToList();
return Ok(new { success = true, data = dates });
}
catch (Exception ex)
{
logger.LogError(ex, "获取日志日期列表失败");
return Ok(new { success = false, message = $"获取日志日期列表失败: {ex.Message}" });
}
}
/// <summary>
/// 合并多行日志(已废弃,现在在流式读取中处理)
/// </summary>
[Obsolete("Use ReadLogsStreamAsync instead")]
private List<string> MergeMultiLineLog(string[] lines)
{
var mergedLines = new List<string>();
var currentLog = new System.Text.StringBuilder();
// 日志行开始的正则表达式
var logStartPattern = new System.Text.RegularExpressions.Regex(
@"^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}\]"
);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line))
continue;
// 检查是否是新的日志条目
if (logStartPattern.IsMatch(line))
{
// 保存之前的日志
if (currentLog.Length > 0)
{
mergedLines.Add(currentLog.ToString());
currentLog.Clear();
}
currentLog.Append(line);
}
else
{
// 这是上一条日志的延续,添加换行符后追加
if (currentLog.Length > 0)
{
currentLog.Append('\n').Append(line);
}
}
}
// 添加最后一条日志
if (currentLog.Length > 0)
{
mergedLines.Add(currentLog.ToString());
}
return mergedLines;
}
/// <summary>
/// 解析单行日志
/// </summary>
private LogEntry? ParseLogLine(string line)
{
try
{
// 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here
// 使用正则表达式解析
var match = System.Text.RegularExpressions.Regex.Match(
line,
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{3})\] (.*)$"
);
if (match.Success)
{
return new LogEntry
{
Timestamp = match.Groups[1].Value,
Level = match.Groups[2].Value,
Message = match.Groups[3].Value
};
}
// 如果不匹配标准格式,将整行作为消息
return new LogEntry
{
Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"),
Level = "LOG",
Message = line
};
}
catch
{
return null;
}
}
/// <summary>
/// 流式读取日志(真正的流式:只读取需要的数据,满足后立即停止)
/// </summary>
private async Task<(List<LogEntry> entries, int total)> ReadLogsStreamAsync(
string path,
int pageIndex,
int pageSize,
string? searchKeyword,
string? logLevel)
{
var filteredEntries = new List<LogEntry>();
var currentLog = new System.Text.StringBuilder();
var logStartPattern = new System.Text.RegularExpressions.Regex(
@"^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}\]");
// 计算需要读取的最大条目数取最近的N条日志用于倒序分页
// 由于日志倒序显示,我们读取足够的数据以覆盖当前页
var maxEntriesToRead = pageIndex * pageSize + pageSize; // 多读一页用于判断是否有下一页
using var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);
using var streamReader = new StreamReader(fileStream);
string? line;
var readCount = 0;
while ((line = await streamReader.ReadLineAsync()) != null)
{
if (string.IsNullOrWhiteSpace(line))
continue;
// 检查是否是新的日志条目
if (logStartPattern.IsMatch(line))
{
// 处理之前累积的日志
if (currentLog.Length > 0)
{
var logEntry = ParseLogLine(currentLog.ToString());
if (logEntry != null && PassFilter(logEntry, searchKeyword, logLevel))
{
filteredEntries.Add(logEntry);
readCount++;
// 如果已读取足够数据,提前退出
if (readCount >= maxEntriesToRead)
{
break;
}
}
currentLog.Clear();
}
currentLog.Append(line);
}
else
{
// 这是上一条日志的延续
if (currentLog.Length > 0)
{
currentLog.Append('\n').Append(line);
}
}
}
// 处理最后一条日志(如果循环正常结束或刚好在日志边界退出)
if (currentLog.Length > 0 && readCount < maxEntriesToRead)
{
var logEntry = ParseLogLine(currentLog.ToString());
if (logEntry != null && PassFilter(logEntry, searchKeyword, logLevel))
{
filteredEntries.Add(logEntry);
}
}
// 倒序排列(最新的在前面)
filteredEntries.Reverse();
// 计算分页
var skip = (pageIndex - 1) * pageSize;
var pagedData = filteredEntries.Skip(skip).Take(pageSize).ToList();
// total 返回 -1 表示未知(避免扫描整个文件)
// 前端可以根据返回数据量判断是否有下一页
return (pagedData, -1);
}
/// <summary>
/// 检查日志条目是否通过过滤条件
/// </summary>
private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel)
{
if (!string.IsNullOrEmpty(searchKeyword) &&
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!string.IsNullOrEmpty(logLevel) &&
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
/// <summary>
/// 读取文件所有行(支持共享读取)
/// </summary>
private async Task<string[]> ReadAllLinesAsync(string path)
{
var lines = new List<string>();
using (var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite))
using (var streamReader = new StreamReader(fileStream))
{
string? line;
while ((line = await streamReader.ReadLineAsync()) != null)
{
lines.Add(line);
}
}
return lines.ToArray();
}
}
/// <summary>
/// 日志条目
/// </summary>
public class LogEntry
{
/// <summary>
/// 时间戳
/// </summary>
public string Timestamp { get; set; } = string.Empty;
/// <summary>
/// 日志级别
/// </summary>
public string Level { get; set; } = string.Empty;
/// <summary>
/// 日志消息
/// </summary>
public string Message { get; set; } = string.Empty;
}