Files
EmailBill/WebApi/Controllers/LogController.cs
SunCheng 704f58b1a1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
fix
2026-01-30 10:41:19 +08:00

504 lines
15 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.
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,
[FromQuery] string? className = 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,
className);
var pagedData = logEntries;
return PagedResponse<LogEntry>.Done(pagedData.ToArray(), total);
}
catch (Exception ex)
{
logger.LogError(ex, "获取日志失败");
return PagedResponse<LogEntry>.Fail($"获取日志失败: {ex.Message}");
}
}
/// <summary>
/// 根据请求ID查询关联日志
/// </summary>
[HttpGet]
public async Task<PagedResponse<LogEntry>> GetLogsByRequestIdAsync(
[FromQuery] string requestId,
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 50,
[FromQuery] string? date = null
)
{
try
{
if (string.IsNullOrEmpty(requestId))
{
return PagedResponse<LogEntry>.Fail("请求ID不能为空");
}
// 获取日志目录
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
if (!Directory.Exists(logDirectory))
{
return PagedResponse<LogEntry>.Fail("日志目录不存在");
}
// 确定要读取的日志文件
string[] logFiles;
if (!string.IsNullOrEmpty(date))
{
var logFilePath = Path.Combine(logDirectory, $"log-{date}.txt");
logFiles = System.IO.File.Exists(logFilePath) ? [logFilePath] : [];
}
else
{
// 读取最近7天的日志文件
logFiles = Directory.GetFiles(logDirectory, "log-*.txt")
.OrderByDescending(f => f)
.Take(7)
.ToArray();
}
var allLogs = new List<LogEntry>();
foreach (var logFile in logFiles)
{
var lines = await ReadAllLinesAsync(logFile);
var merged = MergeMultiLineLog(lines);
foreach (var line in merged)
{
var entry = ParseLogLine(line);
if (entry != null && entry.RequestId == requestId)
{
allLogs.Add(entry);
}
}
}
// 按时间倒序排序
allLogs = allLogs.OrderByDescending(l => l.Timestamp).ToList();
var total = allLogs.Count;
var skip = Math.Max(0, (pageIndex - 1) * pageSize);
var pagedData = allLogs.Skip(skip).Take(pageSize).ToList();
return PagedResponse<LogEntry>.Done(pagedData.ToArray(), total);
}
catch (Exception ex)
{
logger.LogError(ex, "根据请求ID查询日志失败: RequestId={RequestId}", requestId);
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>
[HttpGet]
public BaseResponse<string[]> GetAvailableClassNames([FromQuery] string? date = null)
{
try
{
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
if (!Directory.Exists(logDirectory))
{
return ((string[])[]).Ok();
}
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 ((string[])[]).Ok();
}
var classNames = new HashSet<string>();
var lines = ReadAllLinesAsync(logFilePath).GetAwaiter().GetResult();
var merged = MergeMultiLineLog(lines);
foreach (var line in merged)
{
var entry = ParseLogLine(line);
if (entry != null && !string.IsNullOrEmpty(entry.ClassName))
{
classNames.Add(entry.ClassName);
}
}
return classNames.OrderBy(c => c).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] [request-id] [SourceContext] 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)
{
var sourceContext = match.Groups[4].Value;
var (className, methodName) = ParseSourceContext(sourceContext);
return new LogEntry
{
Timestamp = match.Groups[1].Value,
Level = match.Groups[2].Value,
RequestId = match.Groups[3].Value,
ClassName = className,
MethodName = methodName,
Message = match.Groups[5].Value
};
}
// 尝试解析旧的日志格式没有请求ID有 SourceContext
var oldMatch = 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 (oldMatch.Success)
{
var sourceContext = oldMatch.Groups[3].Value;
var (className, methodName) = ParseSourceContext(sourceContext);
return new LogEntry
{
Timestamp = oldMatch.Groups[1].Value,
Level = oldMatch.Groups[2].Value,
RequestId = "",
ClassName = className,
MethodName = methodName,
Message = oldMatch.Groups[4].Value
};
}
// 尝试解析更旧的日志格式没有请求ID和 SourceContext
var veryOldMatch = 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 (veryOldMatch.Success)
{
return new LogEntry
{
Timestamp = veryOldMatch.Groups[1].Value,
Level = veryOldMatch.Groups[2].Value,
RequestId = "",
ClassName = "",
MethodName = "",
Message = veryOldMatch.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>
/// 解析 SourceContext提取类名
/// </summary>
private (string className, string methodName) ParseSourceContext(string sourceContext)
{
if (string.IsNullOrWhiteSpace(sourceContext))
{
return ("", "");
}
// SourceContext 格式是完整的命名空间.类名,如: Service.Budget.BudgetStatsService
// 提取最后一个部分作为类名
var parts = sourceContext.Split('.');
if (parts.Length == 0)
{
return ("", "");
}
var className = parts[^1];
return (className, "");
}
/// <summary>
/// 读取日志
/// </summary>
private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
string path,
int pageIndex,
int pageSize,
string? searchKeyword,
string? logLevel,
string? className)
{
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, className))
{
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, string? className)
{
if (!string.IsNullOrEmpty(searchKeyword) &&
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!string.IsNullOrEmpty(logLevel) &&
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!string.IsNullOrEmpty(className) &&
!logEntry.ClassName.Equals(className, 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;
/// <summary>
/// 请求ID
/// </summary>
public string RequestId { get; set; } = string.Empty;
/// <summary>
/// 类名
/// </summary>
public string ClassName { get; set; } = string.Empty;
/// <summary>
/// 方法名
/// </summary>
public string MethodName { get; set; } = string.Empty;
}