using System.Text.RegularExpressions; 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, [FromQuery] string? className = null ) { try { // 获取日志目录 var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs"); if (!Directory.Exists(logDirectory)) { return PagedResponse.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.Done([], 0); } // 流式读取日志(边读边过滤,满足条件后停止) var (logEntries, total) = await ReadLogsAsync( logFilePath, pageIndex, pageSize, searchKeyword, logLevel, className); var pagedData = logEntries; return PagedResponse.Done(pagedData.ToArray(), total); } catch (Exception ex) { logger.LogError(ex, "获取日志失败"); return PagedResponse.Fail($"获取日志失败: {ex.Message}"); } } /// /// 根据请求ID查询关联日志 /// [HttpGet] public async Task> GetLogsByRequestIdAsync( [FromQuery] string requestId, [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 50, [FromQuery] string? date = null ) { try { if (string.IsNullOrEmpty(requestId)) { return PagedResponse.Fail("请求ID不能为空"); } // 获取日志目录 var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs"); if (!Directory.Exists(logDirectory)) { return PagedResponse.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(); 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.Done(pagedData.ToArray(), total); } catch (Exception ex) { logger.LogError(ex, "根据请求ID查询日志失败: RequestId={RequestId}", requestId); return PagedResponse.Fail($"查询失败: {ex.Message}"); } } /// /// 获取可用的日志日期列表 /// [HttpGet] public BaseResponse 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(); } } /// /// 获取可用的类名列表 /// [HttpGet] public BaseResponse 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(); 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(); } } /// /// 合并多行日志(已废弃,现在在流式读取中处理) /// private List MergeMultiLineLog(string[] lines) { var mergedLines = new List(); 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; } /// /// 解析单行日志 /// 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; } } /// /// 解析 SourceContext,提取类名 /// 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, ""); } /// /// 读取日志 /// private async Task<(List 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(); 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); } /// /// 检查日志条目是否通过过滤条件 /// 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; } /// /// 读取文件所有行(支持共享读取) /// 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; /// /// 请求ID /// public string RequestId { get; set; } = string.Empty; /// /// 类名 /// public string ClassName { get; set; } = string.Empty; /// /// 方法名 /// public string MethodName { get; set; } = string.Empty; }