A:日志集成

This commit is contained in:
jimmy committed 2025-12-28 15:05:59 +08:00
1 parent f610bf5e34
commit 1b5d57bd19
4 files changed
+315 -18

No files matched your search

@@ -6,10 +6,24 @@ namespace Sharp.Wpf.DevTools.Services;
public interface ILogReaderService public interface ILogReaderService
{ {
/// <summary> /// <summary>
/// 读取日志文件 /// 读取日志文件(完整读取,不推荐用于大文件)
/// </summary> /// </summary>
Task<List<LogEntry>> ReadLogsAsync(string? filePath = null); Task<List<LogEntry>> ReadLogsAsync(string? filePath = null);
/// <summary>
/// 分页读取日志文件(推荐)
/// </summary>
/// <param name="filePath">日志文件路径</param>
/// <param name="skip">跳过的行数</param>
/// <param name="take">读取的行数</param>
/// <returns>日志条目列表和总行数</returns>
Task<(List<LogEntry> Logs, int TotalLines)> ReadLogsPagedAsync(string filePath, int skip = 0, int take = 100);
/// <summary>
/// 获取日志文件的总行数
/// </summary>
Task<int> GetLogFileLineCountAsync(string filePath);
/// <summary> /// <summary>
/// 获取日志文件列表 /// 获取日志文件列表
/// </summary> /// </summary>
@@ -1,10 +1,11 @@
using System.IO; using System.IO;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Sharp.Wpf.DevTools.Services; namespace Sharp.Wpf.DevTools.Services;
/// <summary> /// <summary>
/// 日志读取服务实现(简化版本) /// 日志读取服务实现(支持 JSON 和文本格式)
/// </summary> /// </summary>
public class LogReaderService : ILogReaderService public class LogReaderService : ILogReaderService
{ {
@@ -42,25 +43,72 @@ public class LogReaderService : ILogReaderService
await Task.Run(() => await Task.Run(() =>
{ {
using var reader = new StreamReader(filePath); // 使用 FileShare.ReadWrite 允许 Serilog 同时写入
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
string? line; string? line;
while ((line = reader.ReadLine()) != null) while ((line = reader.ReadLine()) != null)
{ {
var match = LogPattern.Match(line); if (string.IsNullOrWhiteSpace(line))
if (match.Success) continue;
// 尝试解析为 JSON 格式(Serilog JSON)
if (line.TrimStart().StartsWith("{"))
{ {
logs.Add(new LogEntry try
{ {
Timestamp = DateTime.Parse(match.Groups[1].Value), var jsonLog = JsonSerializer.Deserialize<JsonLogEntry>(line);
Level = match.Groups[2].Value, if (jsonLog != null)
Message = match.Groups[3].Value, {
FullContent = line var message = jsonLog.RenderedMessage ?? jsonLog.MessageTemplate ?? "";
}); var fullContent = line;
// 如果有异常信息,格式化显示
if (!string.IsNullOrEmpty(jsonLog.Exception))
{
fullContent = $"[{jsonLog.Timestamp:yyyy-MM-dd HH:mm:ss}] [{jsonLog.Level}] {message}\n\n异常详情:\n{jsonLog.Exception}";
}
logs.Add(new LogEntry
{
Timestamp = jsonLog.Timestamp,
Level = jsonLog.Level ?? "Info",
Message = message,
FullContent = fullContent
});
}
}
catch
{
// JSON 解析失败,当作普通文本
logs.Add(new LogEntry
{
Timestamp = DateTime.Now,
Level = "Unknown",
Message = line,
FullContent = line
});
}
} }
else if (logs.Count > 0) else
{ {
// 多行日志(如堆栈跟踪)追加到上一条 // 文本格式日志
logs[^1].FullContent += Environment.NewLine + line; var match = LogPattern.Match(line);
if (match.Success)
{
logs.Add(new LogEntry
{
Timestamp = DateTime.Parse(match.Groups[1].Value),
Level = match.Groups[2].Value,
Message = match.Groups[3].Value,
FullContent = line
});
}
else if (logs.Count > 0)
{
// 多行日志(如堆栈跟踪)追加到上一条
logs[^1].FullContent += Environment.NewLine + line;
}
} }
} }
}); });
@@ -68,6 +116,121 @@ public class LogReaderService : ILogReaderService
return logs.OrderByDescending(l => l.Timestamp).ToList(); return logs.OrderByDescending(l => l.Timestamp).ToList();
} }
public async Task<(List<LogEntry> Logs, int TotalLines)> ReadLogsPagedAsync(string filePath, int skip = 0, int take = 100)
{
if (!File.Exists(filePath))
return (new List<LogEntry>(), 0);
var logs = new List<LogEntry>();
var totalLines = 0;
await Task.Run(() =>
{
// 使用 FileShare.ReadWrite 允许 Serilog 同时写入
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
// 先统计总行数(用于进度显示)
var allLines = new List<string>();
string? line;
while ((line = reader.ReadLine()) != null)
{
if (!string.IsNullOrWhiteSpace(line))
allLines.Add(line);
}
totalLines = allLines.Count;
// 倒序处理(最新日志在前)
allLines.Reverse();
// 跳过指定行数,然后读取 take 行
var linesToProcess = allLines.Skip(skip).Take(take).ToList();
foreach (var logLine in linesToProcess)
{
// 尝试解析为 JSON 格式(Serilog JSON)
if (logLine.TrimStart().StartsWith("{"))
{
try
{
var jsonLog = JsonSerializer.Deserialize<JsonLogEntry>(logLine);
if (jsonLog != null)
{
var message = jsonLog.RenderedMessage ?? jsonLog.MessageTemplate ?? "";
var fullContent = logLine;
// 如果有异常信息,格式化显示
if (!string.IsNullOrEmpty(jsonLog.Exception))
{
fullContent = $"[{jsonLog.Timestamp:yyyy-MM-dd HH:mm:ss}] [{jsonLog.Level}] {message}\n\n异常详情:\n{jsonLog.Exception}";
}
logs.Add(new LogEntry
{
Timestamp = jsonLog.Timestamp,
Level = jsonLog.Level ?? "Info",
Message = message,
FullContent = fullContent
});
}
}
catch
{
// JSON 解析失败,当作普通文本
logs.Add(new LogEntry
{
Timestamp = DateTime.Now,
Level = "Unknown",
Message = logLine,
FullContent = logLine
});
}
}
else
{
// 文本格式日志
var match = LogPattern.Match(logLine);
if (match.Success)
{
logs.Add(new LogEntry
{
Timestamp = DateTime.Parse(match.Groups[1].Value),
Level = match.Groups[2].Value,
Message = match.Groups[3].Value,
FullContent = logLine
});
}
else if (logs.Count > 0)
{
// 多行日志(如堆栈跟踪)追加到上一条
logs[^1].FullContent += Environment.NewLine + logLine;
}
}
}
});
return (logs, totalLines);
}
public async Task<int> GetLogFileLineCountAsync(string filePath)
{
if (!File.Exists(filePath))
return 0;
var count = 0;
await Task.Run(() =>
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
while (reader.ReadLine() != null)
{
count++;
}
});
return count;
}
public List<string> GetLogFiles() public List<string> GetLogFiles()
{ {
var logPath = Path.Combine(AppContext.BaseDirectory, "logs"); var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
@@ -94,3 +257,15 @@ public class LogReaderService : ILogReaderService
}); });
} }
} }
/// <summary>
/// JSON 格式日志条目(用于反序列化 Serilog JSON 输出)
/// </summary>
internal class JsonLogEntry
{
public DateTime Timestamp { get; set; }
public string? Level { get; set; }
public string? MessageTemplate { get; set; }
public string? RenderedMessage { get; set; }
public string? Exception { get; set; }
}
@@ -15,6 +15,10 @@ public class LogViewerViewModel : ViewModelBase
private string? _selectedLogFile; private string? _selectedLogFile;
private LogEntry? _selectedLog; private LogEntry? _selectedLog;
private string _searchText = string.Empty; private string _searchText = string.Empty;
private int _currentLoadedLines = 0;
private int _totalLines = 0;
private int _pageSize = 100;
private string _statusText = "请选择日志文件";
public LogViewerViewModel(ILogReaderService logReader) public LogViewerViewModel(ILogReaderService logReader)
{ {
@@ -23,6 +27,7 @@ public class LogViewerViewModel : ViewModelBase
LoadLogsCommand = new AsyncCommand(LoadLogsAsync); LoadLogsCommand = new AsyncCommand(LoadLogsAsync);
RefreshCommand = new AsyncCommand(RefreshAsync); RefreshCommand = new AsyncCommand(RefreshAsync);
ClearLogsCommand = new AsyncCommand(ClearLogsAsync); ClearLogsCommand = new AsyncCommand(ClearLogsAsync);
LoadMoreCommand = new AsyncCommand(LoadMoreAsync, () => CanLoadMore);
LogFiles = new ObservableCollection<string>(_logReader.GetLogFiles()); LogFiles = new ObservableCollection<string>(_logReader.GetLogFiles());
SelectedLogFile = LogFiles.FirstOrDefault(); SelectedLogFile = LogFiles.FirstOrDefault();
@@ -31,6 +36,7 @@ public class LogViewerViewModel : ViewModelBase
public IAsyncCommand LoadLogsCommand { get; } public IAsyncCommand LoadLogsCommand { get; }
public IAsyncCommand RefreshCommand { get; } public IAsyncCommand RefreshCommand { get; }
public IAsyncCommand ClearLogsCommand { get; } public IAsyncCommand ClearLogsCommand { get; }
public AsyncCommand LoadMoreCommand { get; }
public ObservableCollection<string> LogFiles { get; } public ObservableCollection<string> LogFiles { get; }
public ObservableCollection<LogEntry> Logs { get; } = new(); public ObservableCollection<LogEntry> Logs { get; } = new();
@@ -59,6 +65,55 @@ public class LogViewerViewModel : ViewModelBase
set => SetProperty(ref _searchText, value); set => SetProperty(ref _searchText, value);
} }
public int CurrentLoadedLines
{
get => _currentLoadedLines;
set => SetProperty(ref _currentLoadedLines, value);
}
public int TotalLines
{
get => _totalLines;
set
{
if (SetProperty(ref _totalLines, value))
{
UpdateStatusText();
LoadMoreCommand.RaiseCanExecuteChanged();
}
}
}
public int PageSize
{
get => _pageSize;
set => SetProperty(ref _pageSize, value);
}
public string StatusText
{
get => _statusText;
set => SetProperty(ref _statusText, value);
}
public bool CanLoadMore => CurrentLoadedLines < TotalLines;
private void UpdateStatusText()
{
if (TotalLines == 0)
{
StatusText = "暂无日志";
}
else if (CurrentLoadedLines >= TotalLines)
{
StatusText = $"已加载全部 {TotalLines} 行日志";
}
else
{
StatusText = $"已加载 {CurrentLoadedLines}/{TotalLines} 行 (点击\"加载更多\"继续)";
}
}
private async Task LoadLogsAsync() private async Task LoadLogsAsync()
{ {
if (string.IsNullOrEmpty(SelectedLogFile)) if (string.IsNullOrEmpty(SelectedLogFile))
@@ -67,9 +122,15 @@ public class LogViewerViewModel : ViewModelBase
IsBusy = true; IsBusy = true;
try try
{ {
var logs = await _logReader.ReadLogsAsync(SelectedLogFile); // 清空现有日志并重新加载第一页
Logs.Clear(); Logs.Clear();
CurrentLoadedLines = 0;
var (logs, totalLines) = await _logReader.ReadLogsPagedAsync(SelectedLogFile, skip: 0, take: PageSize);
TotalLines = totalLines;
CurrentLoadedLines = logs.Count;
foreach (var log in logs) foreach (var log in logs)
{ {
if (string.IsNullOrEmpty(SearchText) || if (string.IsNullOrEmpty(SearchText) ||
@@ -78,6 +139,40 @@ public class LogViewerViewModel : ViewModelBase
Logs.Add(log); Logs.Add(log);
} }
} }
UpdateStatusText();
}
finally
{
IsBusy = false;
}
}
private async Task LoadMoreAsync()
{
if (string.IsNullOrEmpty(SelectedLogFile) || !CanLoadMore)
return;
IsBusy = true;
try
{
var (logs, _) = await _logReader.ReadLogsPagedAsync(
SelectedLogFile,
skip: CurrentLoadedLines,
take: PageSize);
CurrentLoadedLines += logs.Count;
foreach (var log in logs)
{
if (string.IsNullOrEmpty(SearchText) ||
log.Message.Contains(SearchText, StringComparison.OrdinalIgnoreCase))
{
Logs.Add(log);
}
}
UpdateStatusText();
} }
finally finally
{ {
@@ -9,6 +9,7 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 工具栏 --> <!-- 工具栏 -->
@@ -46,8 +47,20 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<!-- 加载更多按钮和状态栏 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="5">
<TextBlock Text="{Binding StatusText}"
VerticalAlignment="Center"
Margin="0,0,10,0"
FontWeight="Bold"/>
<Button Content="📥 加载更多 (100行)"
Command="{Binding LoadMoreCommand}"
Width="150" Height="32"
IsEnabled="{Binding CanLoadMore}"/>
</StackPanel>
<!-- 详情 --> <!-- 详情 -->
<GroupBox Grid.Row="2" Header="详细信息" Height="150" Margin="5"> <GroupBox Grid.Row="3" Header="详细信息" Height="150" Margin="5">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding SelectedLog.FullContent, Mode=OneWay}" <TextBox Text="{Binding SelectedLog.FullContent, Mode=OneWay}"
TextWrapping="Wrap" TextWrapping="Wrap"
@@ -60,7 +73,7 @@
</GroupBox> </GroupBox>
<!-- 底部 --> <!-- 底部 -->
<Button Grid.Row="3" Content="关闭" <Button Grid.Row="4" Content="关闭"
Width="100" Height="32" Width="100" Height="32"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="5" Margin="5"