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}" }); } } /// /// 合并多行日志(已废弃,现在在流式读取中处理) /// [Obsolete("Use ReadLogsStreamAsync instead")] private List MergeMultiLineLog(string[] lines) { var mergedLines = new List(); 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; } /// /// 解析单行日志 /// 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; } } /// /// 流式读取日志(真正的流式:只读取需要的数据,满足后立即停止) /// private async Task<(List entries, int total)> ReadLogsStreamAsync( string path, int pageIndex, int pageSize, string? searchKeyword, string? logLevel) { var filteredEntries = new List(); 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); } /// /// 检查日志条目是否通过过滤条件 /// 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; }