Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/04-avalonia/images/analysis-results.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/04-avalonia/images/error-invalid-path.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/04-avalonia/images/error-unconnected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions docs/04-avalonia/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 04-avalonia

## 一、 功能实现说明

实现了一个图形化客户端,并通过和后端进行异步通信来实现功能。

1. **查看是否连接**
- 通过 `ConnectCommand` 调用 `DesktopDialogHelper` 弹窗引导用户输入 Agent 地址与服务器连接。
- 使用异步方法 `_client.PingAsync()` 校验连接有效性,实时更新底部状态栏(如 `Connected.`、`Connecting...`、`Not connected.`)。

2. **选定分析的目录**
- **切换目录 (`ChangeDirectoryCommand`)**:发送 `ChangeDirectoryRequest` 并异步等待响应,更新服务端当前工作路径。
- **刷新文件 (`RefreshCommand`)**:请求服务端日志列表并清空填充至 `ObservableCollection<LogFileItem>`,实现 UI 列表自动同步。

3. **多个文件同时分析**
- 在 `MainView.axaml.cs` 中启动 `SelectionChanged` 事件,将选中列表绑定回 ViewModel 的 `SelectedFiles`。
- `AnalyzeSelectedFilesCommand`:读取界面输入的并行度(DoP)并校验合法性,向服务端异步发起并发分析任务。
- `AnalyzeAllCommand`:针对目录内所有日志文件发起分析操作。

4. **右键**
- 右键快捷操作
- `Analyze File`:对单选右键文件发起分析。
- `View Analysis Results`:发起 `GetAnalysisResult` 服务端流式 gRPC 调用,通过 `await foreach` 逐行异步读取日志条目并显示至右侧 `ResultEntries` 列表展现。

5. **鲁棒性**
- 封存 `WithClientNotNull` 辅助函数,防止未连接状态下的非法调用。
- 所有 gRPC 远程调用及输入解析均使用 `try-catch` 捕获异常,并使用统一消息弹窗(`ShowMessageDialogAsync`)告知错误信息,确保 GUI 客户端与服务端后台均不会崩溃。

---

## 二、 功能演示与鲁棒性测试

### 1. 正常功能演示

![结果](images/analysis-results.png)

### 2. 鲁棒性与异常处理测试

- **测试 1:未连接服务端时发起操作**
![未连接拦截](images/error-unconnected.png)

- **测试 2:服务端返回错误路径处理**
![非法路径提示](images/error-invalid-path.png)

---

## 三、 问答题解答

### Q4.1
区别在于GUI需要前后端的协同,为每一个元素提供其内置的反馈思路,并且对于协同的要求更为重要,需要仔细组织代码以避免屎山。额外难点:需要设计ui界面,同时阻塞带来的崩溃问题更为严重。异步理解:最大价值在于非阻塞,可以通过grpc流式接收再接收数据的同时操作界面。困难:异步下需要妥善处理异常。

### Q4.2.b

提示词:报错信息,并且令ai完成部分代码的复用。
Binary file added docs/05-advanced/images/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions docs/05-advanced/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 项目功能介绍
* 写在最前:我选择的功能性题目是a.b和a.c,美观性题目是b.a,自由发挥选择a.d。
## 1.项目简介
本项目是一个日志解析客户端,在验证用户身份之后,可以对日志文件进行解析并显示。最终由一个常驻的agent(gRPC)服务端和一个客户端交互组成。
### 1.1前后端架构
* `LogAnalyzerClient`是前端,负责用户交互,并且向后端发送请求。
* `LogAnalyzerAgent`是云服务客户端,和前端通过gRPC异步通信,解析日志内容,并生成token验证身份。
### 1.2编译运行步骤
* `LogAnalyzerAgent`和`LogAnalyzerClient`发出请求,调用`LogAnalyzerRpc`的接口,然后在调用`LogAnalyze`,进一步用最底层的`LogParser`进行日志解析。
## 2.功能
![功能](./images/1.png)
### 2.1基础功能——日志解析
* 日志解析:留置读取文件,进行逐行解析(简单工厂和访问者)
* 并行日志分析:多线程解析
* gRPC:传输数据到服务端,在流式返回,尤其注意捕获异常输入
* 图形客户端:特别注意异步,避免阻塞
### 2.2进阶功能
* Token验证:管理员、普通权限不同
* 日志查询:可以按照类型、等级等清晰观看
* 美观显示:表格显示
* 拓补图展示
## 3.AI
* 相较于前几讲,本节ai借助较多,在分析报错的基础上,利用AI帮助设计了代码框架,自己再进行填充修改,同时,哎帮助分析出了一些由于包版本不同导致运行错误的问题,这些问题我似乎自己难以发现原因。
## 4.总结和经验
* 本节使得我对前后端、本地和云端的交互有了更深的理解,之前在硬设比赛大作业中接触过简单的图形化编程,那时候了解到前后端的协调,每一个图形化的按钮都要有i那个管逻辑支撑,本次作业让我理解更深入,同时,关于异步的理解加深了。
4 changes: 3 additions & 1 deletion src/ConsoleTest/ConsoleTest.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -9,10 +9,12 @@

<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.83.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LogAnalyzer\LogAnalyzer.csproj" />
<ProjectReference Include="..\LogAnalyzerRpc\LogAnalyzerRpc.csproj" />
<ProjectReference Include="..\LogParser\LogParser.csproj" />
<None Include="..\dataset\**\*.log">
<Link>dataset\%(RecursiveDir)%(Filename)%(Extension)</Link>
Expand Down
159 changes: 130 additions & 29 deletions src/ConsoleTest/Program.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,142 @@
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

using Grpc.Core;
using Grpc.Net.Client;
using Google.Protobuf.WellKnownTypes;
using LogAnalyzerRpc;
using LogAnalyzerRpc.Protos;
using LogParser.Models;
using LogParser.Parser;
using LogParser.Visitors;
using LogAnalyzer;

Console.WriteLine("Hello, ConsoleTest of dotnet-workshop!");
Console.WriteLine();
Console.WriteLine("=== Advanced Chapter 5: query & topology verification ===\n");

var entries = new LogFileParser().Parse(new StreamReader("dataset/basic.log")).ToList();
Console.WriteLine($"Parsed {entries.Count} log entries from dataset/basic.log");

// ---------- 1. 过滤:按事件类型 ----------
var callFilter = new LogFilter { EventType = LogEventTypeEnum.Call };
var calls = LogAnalysisQuery.FilterAndSort(entries, callFilter, null);
Console.WriteLine($"\n[filter event=Call] -> {calls.Count} entries");

var errFilter = new LogFilter { Severity = LogSeverityEnum.Error };
var errors = LogAnalysisQuery.FilterAndSort(entries, errFilter, null);
Console.WriteLine($"[filter severity=Error] -> {errors.Count} entries");

var gwFilter = new LogFilter { ServiceName = "gateway" };
var gateway = LogAnalysisQuery.FilterAndSort(entries, gwFilter, null);
Console.WriteLine($"[filter service=gateway] -> {gateway.Count} entries");

// ---------- 2. 排序 ----------
var sortBySeverity = new LogSortOptions { SortBy = "Severity", IsDescending = true };
var sorted = LogAnalysisQuery.FilterAndSort(entries, null, sortBySeverity);
Console.WriteLine($"\n[sort severity desc] first 5 severities:");
foreach (var e in sorted.Take(5))
{
var parser = new LogFileParser();
var results = parser.Parse(new StreamReader("dataset/basic.log")).ToList();
var kvdumper = new KeyValueVisitor();
var kvresults = results.Select(entry => kvdumper.Dump(entry)).ToList();
Console.WriteLine($" line={e.LineNo,-4} severity={e.Severity}");
}

// ---------- 3. 服务名提取 ----------
Console.WriteLine($"\n[GetServiceName] gateway-0 -> {LogAnalysisQuery.GetServiceName("gateway-0")}");
Console.WriteLine($"[GetServiceName] userservice-12 -> {LogAnalysisQuery.GetServiceName("userservice-12")}");

Console.WriteLine(string.Join("\n", kvresults.Select(kvresult => string.Join(", ", kvresult.Select(kv => $"{kv.Key}: {kv.Value}")))));
Console.WriteLine();
// ---------- 4. 拓扑推断 ----------
var topology = TopologyBuilder.Build(entries);
Console.WriteLine($"\n[topology] {topology.Nodes.Count} nodes:");
Console.WriteLine(" " + string.Join(", ", topology.Nodes));
Console.WriteLine($"[topology] {topology.Edges.Count} edges:");
foreach (var edge in topology.Edges)
{
Console.WriteLine($" {edge.SourceService} -> {edge.TargetService} ({edge.CallCount} calls)");
}

// ---------- 5. 拓扑边 -> 日志(request_ids 过滤) ----------
var firstEdge = topology.Edges.FirstOrDefault();
if (firstEdge is not null)
{
var analyzer = new LogFileAnalyzer("dataset");
Console.WriteLine(analyzer.CurrentDirectory);
Console.WriteLine(string.Join(", ", analyzer.GetLogFiles()));
analyzer.AnalyzeAll(4);
analyzer.TryGetAnalysisResult("basic.log", out var analysisResults);
var kvdumper = new KeyValueVisitor();
var kvresults = analysisResults!.Entries.Select(entry => kvdumper.Dump(entry)).ToList();
Console.WriteLine(string.Join("\n", kvresults.Select(kvresult => string.Join(", ", kvresult.Select(kv => $"{kv.Key}: {kv.Value}")))));
Console.WriteLine();
var edgeFilter = new LogFilter();
edgeFilter.RequestIds.AddRange(firstEdge.RequestIds);
var edgeLogs = LogAnalysisQuery.FilterAndSort(entries, edgeFilter, null);
Console.WriteLine($"\n[edge {firstEdge.SourceService}->{firstEdge.TargetService}] " +
$"request_ids={firstEdge.RequestIds.Count}, matched logs={edgeLogs.Count}");
}

// ---------- 6. gRPC 端到端流程测试(需要先启动 Agent 并传入 admin token) ----------
if (args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]))
{
Console.WriteLine("\n=== gRPC end-to-end flow test ===");
var token = args[0];
using var channel = GrpcChannel.ForAddress("http://localhost:5000");
var client = new LogAnalyzerAgentService.LogAnalyzerAgentServiceClient(channel);
var headers = new Metadata { { "x-agent-token", token } };

// 无 token -> 应被拒绝
try
{
await client.PingAsync(new Empty());
Console.WriteLine("[FAIL] ping without token should be rejected");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
Console.WriteLine("[PASS] ping without token -> Unauthenticated");
}

// 带 token -> Ping
await client.PingAsync(new Empty(), headers);
Console.WriteLine("[PASS] ping with token -> OK");

// ChangeDirectory
var dir = @"D:\THUsummerlearning\THU\src\dataset";
var cdResp = await client.ChangeDirectoryAsync(new ChangeDirectoryRequest { DirectoryPath = dir }, headers);
Console.WriteLine(cdResp.Status.Success
? $"[PASS] ChangeDirectory -> {cdResp.CurrentDirectory} ({cdResp.FileNames.Count} files)"
: $"[FAIL] ChangeDirectory: {cdResp.Status.Message}");

// GetLogFiles
var filesResp = await client.GetLogFilesAsync(new Empty(), headers);
Console.WriteLine($"[PASS] GetLogFiles -> {filesResp.FileNames.Count} files: {string.Join(", ", filesResp.FileNames)}");

// AnalyzeAll
var analyzeResp = await client.AnalyzeAllAsync(new AnalyzeAllRequest { DegreeOfParallelism = 4 }, headers);
Console.WriteLine(analyzeResp.Status.Success ? "[PASS] AnalyzeAll -> OK" : $"[FAIL] AnalyzeAll: {analyzeResp.Status.Message}");

// GetAnalysisResult
int entryCount = 0;
using (var call = client.GetAnalysisResult(new GetAnalysisResultRequest { FileName = "basic.log" }, headers))
{
await foreach (var r in call.ResponseStream.ReadAllAsync())
{
if (r.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.LogEntry) entryCount++;
}
}
Console.WriteLine($"[PASS] GetAnalysisResult basic.log -> {entryCount} entries");

// Query(按服务名过滤)
int qCount = 0;
using (var qCall = client.QueryAnalysisResult(new QueryAnalysisResultRequest
{
FileName = "basic.log",
Filter = new LogFilter { ServiceName = "gateway" }
}, headers))
{
await foreach (var r in qCall.ResponseStream.ReadAllAsync())
{
if (r.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.LogEntry) qCount++;
}
}
Console.WriteLine($"[PASS] Query service=gateway -> {qCount} entries");

// Topology
var topoResp = await client.GetTopologyAsync(new GetTopologyRequest { FileName = "basic.log" }, headers);
Console.WriteLine(topoResp.Status.Success
? $"[PASS] Topology -> {topoResp.Nodes.Count} nodes, {topoResp.Edges.Count} edges"
: $"[FAIL] Topology: {topoResp.Status.Message}");

// ListTokens(管理员接口)
var list = await client.ListTokensAsync(new Empty(), headers);
Console.WriteLine($"[PASS] ListTokens -> {list.Tokens.Count} token(s): " +
string.Join(", ", list.Tokens.Select(t => $"{t.Role}")));
}
else
{
var strs = (IEnumerable<string>)new List<string>() { "a", "b", "c" };
Console.WriteLine(strs.FirstOrDefault());
Console.WriteLine(strs.ToList().Count);
Console.WriteLine("\n=== gRPC end-to-end flow test skipped (pass admin token as arg) ===");
}

Console.WriteLine("\n=== Done ===");
1 change: 0 additions & 1 deletion src/LocalCli/LocalCli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<ProjectReference Include="..\LogAnalyzerRpc\LogAnalyzerRpc.csproj" />
<ProjectReference Include="..\LogAnalyzer\LogAnalyzer.csproj" />
<ProjectReference Include="..\LogParser\LogParser.csproj" />
<ProjectReference Include="..\TestUtils\TestUtils.csproj" />
<None Include="..\dataset\**\*.log">
<Link>dataset\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
92 changes: 87 additions & 5 deletions src/LocalCli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using LogAnalyzer;
using LogParser.Visitors;
using System;

namespace LocalCli
{
Expand Down Expand Up @@ -112,22 +113,103 @@ 6. Exit.

private static void ShowLogFiles(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
var files = analyzer.GetLogFiles();
if (files.Count == 0)
{
Console.WriteLine("No log files found in the current directory.");
return;
}

Console.WriteLine("Log files in directory:");
foreach (var file in files)
{
Console.WriteLine($"- {file}");
}
}

private static void AnalyzeFiles(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
Console.WriteLine("Please input log file names separated by comma:");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("Input cannot be empty.");
return;
}

var fileNames = input.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.Where(f => !string.IsNullOrEmpty(f))
.ToList();

if (fileNames.Count == 0)
{
Console.WriteLine("No valid file names provided.");
return;
}

try
{
Console.WriteLine("Analyzing specified files...");
analyzer.AnalyzeFiles(0, fileNames);
Console.WriteLine("Analysis completed.");
}
catch (Exception ex)
{
Console.WriteLine($"Error analyzing files: {ex.Message}");
}
}

private static void AnalyzeAll(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
try
{
Console.WriteLine("Analyzing all log files...");
analyzer.AnalyzeAll(0);
Console.WriteLine("Analysis completed.");
}
catch (Exception ex)
{
Console.WriteLine($"Error analyzing files: {ex.Message}");
}
}

private static void GetAnalysisResult(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
Console.WriteLine("Please input log file name:");
var fileName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(fileName))
{
Console.WriteLine("Invalid file name.");
return;
}

if (!analyzer.TryGetAnalysisResult(fileName, out var result) || result is null)
{
Console.WriteLine($"File '{fileName}' was not found.");
return;
}

switch (result.State)
{
case AnalysisState.NotAnalyzed:
Console.WriteLine($"File '{fileName}' has not been analyzed yet.");
break;

case AnalysisState.Failed:
Console.WriteLine($"Analysis failed for '{fileName}':");
Console.WriteLine(result.ErrorMessage);
break;

case AnalysisState.Succeeded:
Console.WriteLine($"Analysis result for '{fileName}':");
var visitor = new KeyValueVisitor();
foreach (var entry in result.Entries)
{
Console.WriteLine(entry);
}
break;
}
}
}
}
}
Loading
Loading