Files
EmailBill/WebApi/Controllers/LogController.cs
2026-01-18 22:04:56 +08:00

283 lines
8.0 KiB
C#

using System.Text.RegularExpressions;
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 PagedResponse<LogEntry>.Fail("日志目录不存在");
}
// 确定要读取的日志文件
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 PagedResponse<LogEntry>.Done([], 0);
}
// 流式读取日志(边读边过滤,满足条件后停止)
var (logEntries, total) = await ReadLogsAsync(
logFilePath,
pageIndex,
pageSize,
searchKeyword,
logLevel);
var pagedData = logEntries;
return PagedResponse<LogEntry>.Done(pagedData.ToArray(), total);
}
catch (Exception ex)
{
logger.LogError(ex, "获取日志失败");
return PagedResponse<LogEntry>.Fail($"获取日志失败: {ex.Message}");
}
}
/// <summary>
/// 获取可用的日志日期列表
/// </summary>
[HttpGet]
public BaseResponse<string[]> GetAvailableDates()
{
try
{
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
if (!Directory.Exists(logDirectory))
{
return ((string[])[]).Ok();
}
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 dates.ToArray().Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取日志日期列表失败");
return $"获取日志日期列表失败: {ex.Message}".Fail<string[]>();
}
}
/// <summary>
/// 合并多行日志(已废弃,现在在流式读取中处理)
/// </summary>
private List<string> MergeMultiLineLog(string[] lines)
{
var mergedLines = new List<string>();
var currentLog = new StringBuilder();
// 日志行开始的正则表达式
var logStartPattern = new 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
// 使用正则表达式解析
// 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。
var match = Regex.Match(
line,
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{2,5})\] ([\s\S]*)$",
RegexOptions.Singleline
);
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)> ReadLogsAsync(
string path,
int pageIndex,
int pageSize,
string? searchKeyword,
string? logLevel)
{
var allLines = await ReadAllLinesAsync(path);
var merged = MergeMultiLineLog(allLines);
var parsed = new List<LogEntry>();
foreach (var line in merged)
{
var entry = ParseLogLine(line);
if (entry != null && PassFilter(entry, searchKeyword, logLevel))
{
parsed.Add(entry);
}
}
parsed.Reverse();
var total = parsed.Count;
var skip = Math.Max(0, (pageIndex - 1) * pageSize);
var pagedData = parsed.Skip(skip).Take(pageSize).ToList();
return (pagedData, total);
}
/// <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;
}