2026-01-22 19:06:58 +08:00
|
|
|
|
using System.Text.RegularExpressions;
|
2026-01-18 22:04:56 +08:00
|
|
|
|
|
|
|
|
|
|
namespace WebApi.Controllers;
|
2025-12-29 16:45:51 +08:00
|
|
|
|
|
|
|
|
|
|
[ApiController]
|
|
|
|
|
|
[Route("api/[controller]/[action]")]
|
|
|
|
|
|
public class LogController(ILogger<LogController> logger) : ControllerBase
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取日志列表(分页)
|
|
|
|
|
|
/// </summary>
|
2026-01-30 10:41:19 +08:00
|
|
|
|
[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
|
|
|
|
|
|
)
|
2025-12-29 16:45:51 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取日志目录
|
|
|
|
|
|
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
|
|
|
|
|
if (!Directory.Exists(logDirectory))
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return PagedResponse<LogEntry>.Fail("日志目录不存在");
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确定要读取的日志文件
|
|
|
|
|
|
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))
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return PagedResponse<LogEntry>.Done([], 0);
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 11:07:14 +08:00
|
|
|
|
// 流式读取日志(边读边过滤,满足条件后停止)
|
2026-01-04 16:43:32 +08:00
|
|
|
|
var (logEntries, total) = await ReadLogsAsync(
|
|
|
|
|
|
logFilePath,
|
|
|
|
|
|
pageIndex,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
searchKeyword,
|
2026-01-22 19:06:58 +08:00
|
|
|
|
logLevel,
|
|
|
|
|
|
className);
|
2025-12-29 16:45:51 +08:00
|
|
|
|
|
2025-12-30 11:07:14 +08:00
|
|
|
|
var pagedData = logEntries;
|
2025-12-29 16:45:51 +08:00
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return PagedResponse<LogEntry>.Done(pagedData.ToArray(), total);
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取日志失败");
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return PagedResponse<LogEntry>.Fail($"获取日志失败: {ex.Message}");
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 19:06:58 +08:00
|
|
|
|
/// <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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 16:45:51 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取可用的日志日期列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
2026-01-04 16:43:32 +08:00
|
|
|
|
public BaseResponse<string[]> GetAvailableDates()
|
2025-12-29 16:45:51 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
|
|
|
|
|
if (!Directory.Exists(logDirectory))
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return ((string[])[]).Ok();
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var logFiles = Directory.GetFiles(logDirectory, "log-*.txt");
|
|
|
|
|
|
var dates = logFiles
|
|
|
|
|
|
.Select(f => Path.GetFileNameWithoutExtension(f))
|
|
|
|
|
|
.Select(name => name.Replace("log-", ""))
|
|
|
|
|
|
.OrderByDescending(d => d)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return dates.ToArray().Ok();
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取日志日期列表失败");
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取日志日期列表失败: {ex.Message}".Fail<string[]>();
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 19:06:58 +08:00
|
|
|
|
/// <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[]>();
|
|
|
|
|
|
}
|
2026-01-30 10:41:19 +08:00
|
|
|
|
}
|
2026-01-22 19:06:58 +08:00
|
|
|
|
|
2025-12-30 11:07:14 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private List<string> MergeMultiLineLog(string[] lines)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mergedLines = new List<string>();
|
2026-01-01 11:58:21 +08:00
|
|
|
|
var currentLog = new StringBuilder();
|
2026-01-04 16:43:32 +08:00
|
|
|
|
|
2025-12-30 11:07:14 +08:00
|
|
|
|
// 日志行开始的正则表达式
|
2026-01-18 22:04:56 +08:00
|
|
|
|
var logStartPattern = new Regex(
|
2025-12-30 11:07:14 +08:00
|
|
|
|
@"^\[\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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 16:45:51 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析单行日志
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private LogEntry? ParseLogLine(string line)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-01-22 19:06:58 +08:00
|
|
|
|
// 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] [request-id] [SourceContext] Message here
|
2025-12-29 16:45:51 +08:00
|
|
|
|
// 使用正则表达式解析
|
2026-01-01 11:58:21 +08:00
|
|
|
|
// 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。
|
2026-01-18 22:04:56 +08:00
|
|
|
|
var match = Regex.Match(
|
2025-12-29 16:45:51 +08:00
|
|
|
|
line,
|
2026-01-22 19:06:58 +08:00
|
|
|
|
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{2,5})\] \[([^]]*)\] \[([^]]*)\] ([\s\S]*)$",
|
2026-01-18 22:04:56 +08:00
|
|
|
|
RegexOptions.Singleline
|
2025-12-29 16:45:51 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (match.Success)
|
|
|
|
|
|
{
|
2026-01-22 19:06:58 +08:00
|
|
|
|
var sourceContext = match.Groups[4].Value;
|
|
|
|
|
|
var (className, methodName) = ParseSourceContext(sourceContext);
|
|
|
|
|
|
|
2025-12-29 16:45:51 +08:00
|
|
|
|
return new LogEntry
|
|
|
|
|
|
{
|
|
|
|
|
|
Timestamp = match.Groups[1].Value,
|
|
|
|
|
|
Level = match.Groups[2].Value,
|
2026-01-22 19:06:58 +08:00
|
|
|
|
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
|
2025-12-29 16:45:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果不匹配标准格式,将整行作为消息
|
|
|
|
|
|
return new LogEntry
|
|
|
|
|
|
{
|
|
|
|
|
|
Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"),
|
|
|
|
|
|
Level = "LOG",
|
|
|
|
|
|
Message = line
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 11:07:14 +08:00
|
|
|
|
/// <summary>
|
2026-01-22 19:06:58 +08:00
|
|
|
|
/// 解析 SourceContext,提取类名
|
2025-12-30 11:07:14 +08:00
|
|
|
|
/// </summary>
|
2026-01-22 19:06:58 +08:00
|
|
|
|
private (string className, string methodName) ParseSourceContext(string sourceContext)
|
2025-12-30 11:07:14 +08:00
|
|
|
|
{
|
2026-01-22 19:06:58 +08:00
|
|
|
|
if (string.IsNullOrWhiteSpace(sourceContext))
|
|
|
|
|
|
{
|
|
|
|
|
|
return ("", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SourceContext 格式是完整的命名空间.类名,如: Service.Budget.BudgetStatsService
|
|
|
|
|
|
// 提取最后一个部分作为类名
|
|
|
|
|
|
var parts = sourceContext.Split('.');
|
|
|
|
|
|
if (parts.Length == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return ("", "");
|
|
|
|
|
|
}
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-22 19:06:58 +08:00
|
|
|
|
var className = parts[^1];
|
|
|
|
|
|
return (className, "");
|
|
|
|
|
|
}
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-22 19:06:58 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 读取日志
|
|
|
|
|
|
/// </summary>
|
2026-01-30 10:41:19 +08:00
|
|
|
|
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);
|
2026-01-22 19:06:58 +08:00
|
|
|
|
|
2026-01-30 10:41:19 +08:00
|
|
|
|
var merged = MergeMultiLineLog(allLines);
|
2026-01-22 19:06:58 +08:00
|
|
|
|
|
2026-01-30 10:41:19 +08:00
|
|
|
|
var parsed = new List<LogEntry>();
|
|
|
|
|
|
foreach (var line in merged)
|
2025-12-30 11:07:14 +08:00
|
|
|
|
{
|
2026-01-30 10:41:19 +08:00
|
|
|
|
var entry = ParseLogLine(line);
|
|
|
|
|
|
if (entry != null && PassFilter(entry, searchKeyword, logLevel, className))
|
|
|
|
|
|
{
|
|
|
|
|
|
parsed.Add(entry);
|
|
|
|
|
|
}
|
2025-12-30 11:07:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-01 11:58:21 +08:00
|
|
|
|
parsed.Reverse();
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-01 11:58:21 +08:00
|
|
|
|
var total = parsed.Count;
|
|
|
|
|
|
var skip = Math.Max(0, (pageIndex - 1) * pageSize);
|
|
|
|
|
|
var pagedData = parsed.Skip(skip).Take(pageSize).ToList();
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-01 11:58:21 +08:00
|
|
|
|
return (pagedData, total);
|
2025-12-30 11:07:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 检查日志条目是否通过过滤条件
|
|
|
|
|
|
/// </summary>
|
2026-01-30 10:41:19 +08:00
|
|
|
|
private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className)
|
2025-12-30 11:07:14 +08:00
|
|
|
|
{
|
2026-01-30 10:41:19 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(searchKeyword) &&
|
|
|
|
|
|
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-30 10:41:19 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(logLevel) &&
|
|
|
|
|
|
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-30 10:41:19 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(className) &&
|
|
|
|
|
|
!logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-12-30 11:07:14 +08:00
|
|
|
|
|
2026-01-30 10:41:19 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-01-22 19:06:58 +08:00
|
|
|
|
|
2025-12-29 16:45:51 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 读取文件所有行(支持共享读取)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<string[]> ReadAllLinesAsync(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
var lines = new List<string>();
|
2026-01-04 16:43:32 +08:00
|
|
|
|
|
2025-12-29 16:45:51 +08:00
|
|
|
|
using (var fileStream = new FileStream(
|
2026-01-04 16:43:32 +08:00
|
|
|
|
path,
|
|
|
|
|
|
FileMode.Open,
|
|
|
|
|
|
FileAccess.Read,
|
2025-12-29 16:45:51 +08:00
|
|
|
|
FileShare.ReadWrite))
|
|
|
|
|
|
using (var streamReader = new StreamReader(fileStream))
|
|
|
|
|
|
{
|
|
|
|
|
|
string? line;
|
|
|
|
|
|
while ((line = await streamReader.ReadLineAsync()) != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
lines.Add(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-04 16:43:32 +08:00
|
|
|
|
|
2025-12-29 16:45:51 +08:00
|
|
|
|
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;
|
2026-01-22 19:06:58 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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;
|
2025-12-29 16:45:51 +08:00
|
|
|
|
}
|