A:日志集成
This commit is contained in:
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 读取日志文件
|
||||
/// 读取日志文件(完整读取,不推荐用于大文件)
|
||||
/// </summary>
|
||||
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>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Sharp.Wpf.DevTools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 日志读取服务实现(简化版本)
|
||||
/// 日志读取服务实现(支持 JSON 和文本格式)
|
||||
/// </summary>
|
||||
public class LogReaderService : ILogReaderService
|
||||
{
|
||||
@@ -42,25 +43,72 @@ public class LogReaderService : ILogReaderService
|
||||
|
||||
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;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var match = LogPattern.Match(line);
|
||||
if (match.Success)
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// 尝试解析为 JSON 格式(Serilog JSON)
|
||||
if (line.TrimStart().StartsWith("{"))
|
||||
{
|
||||
logs.Add(new LogEntry
|
||||
try
|
||||
{
|
||||
Timestamp = DateTime.Parse(match.Groups[1].Value),
|
||||
Level = match.Groups[2].Value,
|
||||
Message = match.Groups[3].Value,
|
||||
FullContent = line
|
||||
});
|
||||
var jsonLog = JsonSerializer.Deserialize<JsonLogEntry>(line);
|
||||
if (jsonLog != null)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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 LogEntry? _selectedLog;
|
||||
private string _searchText = string.Empty;
|
||||
private int _currentLoadedLines = 0;
|
||||
private int _totalLines = 0;
|
||||
private int _pageSize = 100;
|
||||
private string _statusText = "请选择日志文件";
|
||||
|
||||
public LogViewerViewModel(ILogReaderService logReader)
|
||||
{
|
||||
@@ -23,6 +27,7 @@ public class LogViewerViewModel : ViewModelBase
|
||||
LoadLogsCommand = new AsyncCommand(LoadLogsAsync);
|
||||
RefreshCommand = new AsyncCommand(RefreshAsync);
|
||||
ClearLogsCommand = new AsyncCommand(ClearLogsAsync);
|
||||
LoadMoreCommand = new AsyncCommand(LoadMoreAsync, () => CanLoadMore);
|
||||
|
||||
LogFiles = new ObservableCollection<string>(_logReader.GetLogFiles());
|
||||
SelectedLogFile = LogFiles.FirstOrDefault();
|
||||
@@ -31,6 +36,7 @@ public class LogViewerViewModel : ViewModelBase
|
||||
public IAsyncCommand LoadLogsCommand { get; }
|
||||
public IAsyncCommand RefreshCommand { get; }
|
||||
public IAsyncCommand ClearLogsCommand { get; }
|
||||
public AsyncCommand LoadMoreCommand { get; }
|
||||
|
||||
public ObservableCollection<string> LogFiles { get; }
|
||||
public ObservableCollection<LogEntry> Logs { get; } = new();
|
||||
@@ -59,6 +65,55 @@ public class LogViewerViewModel : ViewModelBase
|
||||
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()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SelectedLogFile))
|
||||
@@ -67,9 +122,15 @@ public class LogViewerViewModel : ViewModelBase
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var logs = await _logReader.ReadLogsAsync(SelectedLogFile);
|
||||
|
||||
// 清空现有日志并重新加载第一页
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchText) ||
|
||||
@@ -78,6 +139,40 @@ public class LogViewerViewModel : ViewModelBase
|
||||
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
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
@@ -46,8 +47,20 @@
|
||||
</DataGrid.Columns>
|
||||
</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">
|
||||
<TextBox Text="{Binding SelectedLog.FullContent, Mode=OneWay}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -60,7 +73,7 @@
|
||||
</GroupBox>
|
||||
|
||||
<!-- 底部 -->
|
||||
<Button Grid.Row="3" Content="关闭"
|
||||
<Button Grid.Row="4" Content="关闭"
|
||||
Width="100" Height="32"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="5"
|
||||
|
||||
Reference in new issue
Block a user