namespace WebApi.Controllers; [ApiController] [Route("api/[controller]/[action]")] public class LogController(ILogger logger) : ControllerBase { /// /// 获取日志列表(分页) /// [HttpGet] public async Task> 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 { 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 { Success = true, Data = [], Total = 0, Message = "日志文件不存在" }; } // 流式读取日志(边读边过滤,满足条件后停止) var (logEntries, total) = await ReadLogsStreamAsync( logFilePath, pageIndex, pageSize, searchKeyword, logLevel); var pagedData = logEntries; return new PagedResponse { Success = true, Data = pagedData.ToArray(), Total = total, Message = "获取日志成功" }; } catch (Exception ex) { logger.LogError(ex, "获取日志失败"); return new PagedResponse { Success = false, Data = [], Total = 0, Message = $"获取日志失败: {ex.Message}" }; } } /// /// 获取可用的日志日期列表 /// [HttpGet] public IActionResult GetAvailableDates() { try { var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs"); if (!Directory.Exists(logDirectory)) { return Ok(new { success = true, data = new List() }); } 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}" }); } } /// /// 合并多行日志(已废弃,现在在流式读取中处理) /// private List MergeMultiLineLog(string[] lines) { var mergedLines = new List(); var currentLog = new 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; } /// /// 解析单行日志 /// private LogEntry? ParseLogLine(string line) { try { // 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here // 使用正则表达式解析 // 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。 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]{2,5})\] ([\s\S]*)$", System.Text.RegularExpressions.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; } } /// /// 流式读取日志(真正的流式:只读取需要的数据,满足后立即停止) /// private async Task<(List entries, int total)> ReadLogsStreamAsync( string path, int pageIndex, int pageSize, string? searchKeyword, string? logLevel) { // 简化:一次性读取所有行,合并多行日志,过滤并在内存中分页 var allLines = await ReadAllLinesAsync(path); // 合并多行日志为独立条目 var merged = MergeMultiLineLog(allLines); var parsed = new List(); 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); } /// /// 检查日志条目是否通过过滤条件 /// 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; } /// /// 读取文件所有行(支持共享读取) /// private async Task ReadAllLinesAsync(string path) { var lines = new List(); 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(); } } /// /// 日志条目 /// public class LogEntry { /// /// 时间戳 /// public string Timestamp { get; set; } = string.Empty; /// /// 日志级别 /// public string Level { get; set; } = string.Empty; /// /// 日志消息 /// public string Message { get; set; } = string.Empty; }