359 lines
11 KiB
C#
359 lines
11 KiB
C#
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
|
||
)
|
||
{
|
||
try
|
||
{
|
||
// 获取日志目录
|
||
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||
if (!Directory.Exists(logDirectory))
|
||
{
|
||
return new PagedResponse<LogEntry>
|
||
{
|
||
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<LogEntry>
|
||
{
|
||
Success = true,
|
||
Data = [],
|
||
Total = 0,
|
||
Message = "日志文件不存在"
|
||
};
|
||
}
|
||
|
||
// 流式读取日志(边读边过滤,满足条件后停止)
|
||
var (logEntries, total) = await ReadLogsStreamAsync(
|
||
logFilePath,
|
||
pageIndex,
|
||
pageSize,
|
||
searchKeyword,
|
||
logLevel);
|
||
|
||
var pagedData = logEntries;
|
||
|
||
return new PagedResponse<LogEntry>
|
||
{
|
||
Success = true,
|
||
Data = pagedData.ToArray(),
|
||
Total = total,
|
||
Message = "获取日志成功"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "获取日志失败");
|
||
return new PagedResponse<LogEntry>
|
||
{
|
||
Success = false,
|
||
Data = [],
|
||
Total = 0,
|
||
Message = $"获取日志失败: {ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取可用的日志日期列表
|
||
/// </summary>
|
||
[HttpGet]
|
||
public IActionResult GetAvailableDates()
|
||
{
|
||
try
|
||
{
|
||
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||
if (!Directory.Exists(logDirectory))
|
||
{
|
||
return Ok(new { success = true, data = new List<string>() });
|
||
}
|
||
|
||
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}" });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
||
/// </summary>
|
||
[Obsolete("Use ReadLogsStreamAsync instead")]
|
||
private List<string> MergeMultiLineLog(string[] lines)
|
||
{
|
||
var mergedLines = new List<string>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析单行日志
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 流式读取日志(真正的流式:只读取需要的数据,满足后立即停止)
|
||
/// </summary>
|
||
private async Task<(List<LogEntry> entries, int total)> ReadLogsStreamAsync(
|
||
string path,
|
||
int pageIndex,
|
||
int pageSize,
|
||
string? searchKeyword,
|
||
string? logLevel)
|
||
{
|
||
var filteredEntries = new List<LogEntry>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查日志条目是否通过过滤条件
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <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;
|
||
} |