Skip to content
Closed
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
42 changes: 42 additions & 0 deletions docs/01-basic/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 第三讲:基础日志解析作业报告

姓名:谭翔之
班级:无57班
学号:不填写

## 已实现功能

- 完成 `request` 和 `internal` 两类日志的 JSON 解析,并保留错误日志的异常名称和异常信息。
- 为 `CallLogEntry`、`RequestLogEntry`、`InternalLogEntry` 实现访问者分派。
- 完成 `KeyValueVisitor.Dump` 以及三种日志对应的 `Visit` 方法。

## Q1.1

1. `LogFileParser.Parse` 使用 `ReadLine` 逐行读取日志,再把每行交给 CSV 解析逻辑;解析后的记录按 `lineno`、`timestamp`、`pod-name`、`message` 的顺序保存到 `LogRecord`,因此 `LineParser` 通过记录的各属性而不是模糊的字符串位置来使用字段。
2. `LineParser.ParseLine` 先解析 `message` 的 JSON,再读取 `event` 字段,根据 `event == "call"`、`event == "request"` 或 `event == "internal"` 选择对应的日志类型。
3. JSON 使用 `System.Text.Json.JsonSerializer.Deserialize<T>` 解析。各事件的专用字段通过可空属性和显式检查处理;字段缺失时不会悄悄生成一个有效结果,而是抛出格式错误,交由上层报告该行无效。
4. 烤串命名法到 C# 属性名的转换由 `JsonPropertyName` 特性完成,例如把 `request-id` 映射到 `RequestId`,把 `status-code` 映射到 `StatusCode`。

## Q1.2

以 Call 日志为例,主要调用链为:

1. `Dictionary<string, string> KeyValueVisitor.Dump(LogEntry entry)`;
2. `entry.Accept(this)`;
3. `Dictionary<string, string> KeyValueVisitor.Visit(CallLogEntry entry)`;
4. `CallLogEntry` 的 `Accept` 实现调用 `visitor.Visit(this)`,由静态类型把分派落到 `Visit(CallLogEntry)`;
5. `Visit` 读取 `LineNo`、`Timestamp`、`PodName`、`Severity`、`EventType`、`RequestId`、`TargetService` 和 `DurationMs`,组装并返回字典。

## Q1.3.b(使用 AI)

我使用 AI 辅助阅读项目框架和测试,提示词要求它解释日志模型、JSON 字段映射、访问者模式,并给出 request/internal 解析和 `KeyValueVisitor` 的实现建议。AI 的优势是能快速整理多个文件之间的接口关系,并提醒检查缺失字段、烤串命名和多态分派;不足是初始建议可能忽略项目已有的异常类型和测试对错误输入的要求。因此我逐项对照 `guidance.md`、测试代码和实际构建结果,修改后再提交,未把未经验证的代码或测试结果当作结论。

## 验证

提交前应在 `src` 目录运行:

```shell
dotnet test test-01-basic/test-01-basic.csproj -c Release
```

并在 PR 中附上实际测试通过的终端或 IDE 截图。
43 changes: 43 additions & 0 deletions docs/02-multithreading/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 第二讲:多线程日志分析作业报告

姓名:谭翔之
班级:无57班
学号:不填写

## 已实现功能

- `WorkQueue<T>` 使用 `lock` 保护队列和完成标志,并使用 `Monitor.Wait`、`Pulse`、`PulseAll` 实现生产者—消费者同步;消费者用 `while` 重新检查条件,避免虚假唤醒。
- `LogFileAnalyzer` 支持扫描目录、按并行度启动工作线程、跳过已经成功或失败的文件、保存成功结果及失败信息,并保护共享状态。
- `LocalCli` 支持输入目录、列出日志、分析指定文件、分析全部文件、查看结果和更换目录;非法目录、文件名、选项和并行分析状态都会给出提示,不会直接崩溃。

## Q2.1

`WorkQueue<T>` 的共享变量是 `_items` 和 `_isCompleted`。所有读写都在对 `_items` 加锁的临界区中进行;等待使用 `Monitor.Wait`,入队使用 `Monitor.Pulse`,完成添加时使用 `Monitor.PulseAll` 唤醒全部消费者。

`LogFileAnalyzer` 的共享变量包括 `_currentDirectory`、`_isAnalyzing`、`_logFiles` 和 `_analysisResults`。这些变量由 `_syncRoot` 保护;文件解析在工作线程中进行,写回 `_analysisResults` 时重新加锁。

如果把条件判断从 `while` 改成 `if`,线程可能因虚假唤醒而在队列仍为空时继续执行 `Dequeue`,导致异常或错误结果。使用 `while` 可以在线程被唤醒后重新检查“队列非空或已经完成添加”的条件。

## Q2.2

目录扫描在 `ChangeDirectory` 中通过 `Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)` 完成。若需要递归扫描全部子目录,应将搜索选项改为 `SearchOption.AllDirectories`,并对无权限目录等 `IOException`/`UnauthorizedAccessException` 做相应处理。

## Q2.3.b(使用 AI)

本次作业使用了 AI 辅助。提示词的核心内容是:阅读给定的 C# 项目框架和测试,补全线程安全队列、并行日志分析器、控制台交互界面,并解释同步、互斥和虚假唤醒。AI 主要用于梳理接口要求、检查并发边界和生成初始实现,之后结合讲义与官方测试逐项核对。最终实现中保留了人工检查和测试验证,没有把无法验证的输出当作测试结果提交。

本节难度评价:中等。队列本身较清晰,日志分析器的共享状态、重复分析和异常状态处理需要仔细设计。

## 运行验证

已运行官方测试项目:

```text
src/test-02-multithreading/test-02-multithreading.csproj
```

提交前请补充以下真实截图(截图应来自实际终端或程序窗口):

- LocalCli 正常流程:列出文件、分析全部、查看成功结果;
- LocalCli 鲁棒性流程:非法目录、非法选项、不存在文件名、重复分析;
- 官方测试通过的终端输出(若终端没有显示输出,可使用 IDE 的测试结果面板)。
42 changes: 42 additions & 0 deletions docs/03-async-grpc/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 第三讲:异步与 gRPC 作业报告

姓名:谭翔之
班级:无57班
学号:不填写

## 已实现功能

- 完成日志严重级别、事件类型以及三类日志记录与 Protobuf 消息之间的双向转换。
- 完成 Agent 的目录切换、文件查询、指定文件分析、全部文件分析和流式返回分析结果。
- Agent 对无效并行度、目录不存在、文件不存在、未选择目录、重复分析和内部异常返回明确的状态码,不因非法请求退出。
- `AgentService` 将所有 gRPC 请求转发到 `AgentSession`,分析结果按 header → log entry 顺序流式发送。
- `RemoteCli` 的远程调用全部采用异步接口,支持查看文件、设置并行度、分析指定/全部文件以及读取流式结果,并处理输入错误和网络异常。

## Q3.1

网络应用与普通本地程序相比,除了业务逻辑,还必须处理连接建立、序列化、网络延迟、请求失败、服务端状态以及客户端和服务端的版本兼容。本地方法调用通常能立即得到结果,而网络调用需要异步等待,并且成功发送不等于对方一定成功处理。程序还要区分参数错误、资源不存在、当前状态不允许操作和服务器内部错误,并保证单个错误请求不会使常驻服务退出。

本次实现中额外的复杂点主要有:

1. 内部 C# 类型与 Protobuf 类型需要逐字段、双向转换;
2. 大文件结果不能一次返回,需要使用服务器流;
3. RemoteCli 要在等待网络时保持异步调用方式;
4. Agent 是有状态单例,需要正确反映当前目录和分析状态;
5. 网络层异常和业务状态码必须分别处理。

## Q3.2.b(使用 AI)

本次作业使用了 AI 辅助。提示词的核心内容是:阅读 proto、项目框架和官方测试,补全类型转换、AgentSession、AgentService 和异步 RemoteCli,并检查错误状态与流式返回顺序。AI 主要用于生成初始代码、检查 gRPC 接口用法和梳理异常映射。

验证过程中曾发现一个错误:Protobuf 的可选字符串属性不能直接赋值为 `null`,否则会使成功结果在转换 header 时抛出异常。根据失败测试定位后,改为只在确有错误信息时设置该字段。这个过程加深了我对 Protobuf 生成代码和流式响应调试的理解。

## 验证结果

- `test-03-async-grpc`:3/3 通过;
- `RemoteCli` Release 构建:成功,0 警告、0 错误。

提交前请补充真实运行截图:

- 同时运行 Agent 与 RemoteCli,展示正常目录切换、文件列表、分析和结果查询;
- 展示非法目录、负数并行度、不存在文件、Agent 未运行时的错误提示;
- 展示官方测试通过结果。
33 changes: 33 additions & 0 deletions docs/04-avalonia/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 第四讲:Avalonia 图形界面作业报告

姓名:谭翔之
班级:无57班
学号:不填写

## 已实现功能

- `LogFields.Summary` 将每条远程日志的字段整理为可读文本,并能显示错误/未分析提示。
- Refresh 异步获取 Agent 文件列表并刷新左侧列表。
- Selected 按钮支持多选文件分析;新增 All 按钮分析全部文件;右键 Analyze File 支持分析单个文件。
- 右键 View Analysis Results 通过 gRPC 流式读取结果,并在右侧列表展示成功、失败和未分析状态。
- 所有 gRPC 操作都通过异步 API 调用,并对未连接、非法并行度、空选择、网络异常和 Agent 错误状态弹出提示。

## Q4.1

控制台程序按顺序读取输入并打印文本,界面程序则由事件驱动:用户操作触发命令,ViewModel 更新属性后由绑定系统刷新界面。GUI 还需要考虑布局、控件状态、多选同步、右键菜单、线程亲和性和异常提示等额外问题。若在 UI 线程上同步等待网络请求,窗口会失去响应,因此这里所有 gRPC 调用都使用异步 API 并 `await`,让 UI 线程在等待期间保持可用。

## Q4.2.b(使用 AI)

本次作业使用了 AI 辅助。提示词的核心内容是:阅读 Avalonia 的 XAML、ViewModel、对话框和 gRPC 客户端工厂,补全刷新、选中文件分析、全部分析、右键分析和结果显示,并保持 UI 线程不阻塞。AI 主要用于梳理 MVVM 绑定关系和异步流读取;编译时发现 C# 14 将 `field` 作为属性访问器关键字,随后将 lambda 变量改名并重新构建通过。

## 验证结果

- `LogAnalyzerClient` Release 构建成功:0 警告、0 错误;
- `LogAnalyzerClient.Desktop` Release 构建成功:0 警告、0 错误。

提交前请补充真实截图:

- 连接 Agent、切换目录和刷新文件列表;
- 多选分析、全部分析、右键单文件分析;
- 成功、失败、未分析三种结果显示;
- 未连接、非法并行度和不存在文件等错误提示。
86 changes: 82 additions & 4 deletions src/LocalCli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,100 @@ 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.");
return;
}

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

private static void AnalyzeFiles(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
Console.WriteLine("Please input comma-separated log file names:");
var input = Console.ReadLine();
if (input is null)
{
return;
}

var fileNames = input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (fileNames.Length == 0)
{
Console.WriteLine("No file name was provided.");
return;
}

try
{
analyzer.AnalyzeFiles(0, fileNames);
Console.WriteLine("Analysis completed.");
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
{
Console.WriteLine($"Unable to analyze files: {ex.Message}");
}
}

private static void AnalyzeAll(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
try
{
analyzer.AnalyzeAll(0);
Console.WriteLine("Analysis completed.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Unable to analyze files: {ex.Message}");
}
}

private static void GetAnalysisResult(LogFileAnalyzer analyzer)
{
throw new NotImplementedException("T2.3");
Console.WriteLine("Please input a log file name:");
var fileName = Console.ReadLine();
if (string.IsNullOrWhiteSpace(fileName))
{
Console.WriteLine("File name cannot be empty.");
return;
}

try
{
if (!analyzer.TryGetAnalysisResult(fileName.Trim(), out var result) || result is null)
{
Console.WriteLine("The specified file does not exist in the current directory.");
return;
}

switch (result.State)
{
case AnalysisState.NotAnalyzed:
Console.WriteLine("This file has not been analyzed yet.");
break;
case AnalysisState.Failed:
Console.WriteLine($"Analysis failed: {result.ErrorMessage}");
break;
case AnalysisState.Succeeded:
var visitor = new KeyValueVisitor();
foreach (var entry in result.Entries)
{
var values = visitor.Dump(entry);
Console.WriteLine(string.Join(", ", values.Select(pair => $"{pair.Key}={pair.Value}")));
}
break;
}
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
{
Console.WriteLine($"Unable to get analysis result: {ex.Message}");
}
}
}
}
63 changes: 52 additions & 11 deletions src/LogAnalyzer/LogFileAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using LogParser.Models;
using LogParser.Parser;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography.X509Certificates;

namespace LogAnalyzer
{
Expand Down Expand Up @@ -141,7 +140,7 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable<string> fileNames)
/*
* Set _isAnalyzing
*/
// TODO: T2.2
_isAnalyzing = true;
}

try
Expand All @@ -154,7 +153,10 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable<string> fileNames)
* Unset _isAnalyzing
* Remember to lock _syncRoot to prevent data race
*/
// TODO: T2.2
lock (_syncRoot)
{
_isAnalyzing = false;
}
}
}

Expand All @@ -169,7 +171,15 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis
* Filter unparsed files.
* If there is an unknown file, throw System.InvalidOperationException.
*/
throw new NotImplementedException("TODO: T2.2");
if (!_analysisResults.TryGetValue(file.Name, out var result))
{
throw new InvalidOperationException($"Unknown log file: {file.Name}.");
}

if (result.State == AnalysisState.NotAnalyzed)
{
logFilesToParse.Add(file);
}
}
}

Expand All @@ -183,7 +193,11 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis
/*
* Enqueue log files
*/
// TODO: T2.2
foreach (var file in logFilesToParse)
{
queue.Enqueue(file);
}
queue.CompleteAdding();

degreeOfParallelism = Math.Max(Math.Min(degreeOfParallelism, logFilesToParse.Count), 1);
var workers = new Thread[degreeOfParallelism];
Expand All @@ -194,13 +208,21 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis
/*
* Create and start threads to run `WorkerMain`
*/
// TODO: T2.2
workers[i] = new Thread(() => WorkerMain(workerId, queue))
{
IsBackground = true,
Name = threadName,
};
workers[i].Start();
}

/*
* Wait for (join) all threads to end
*/
// TODO: T2.2
foreach (var worker in workers)
{
worker.Join();
}
}

private void WorkerMain(int workerId, WorkQueue<FileInfo> queue)
Expand All @@ -213,19 +235,38 @@ private void WorkerMain(int workerId, WorkQueue<FileInfo> queue)
try
{
// Parse file
throw new NotImplementedException("TODO: T2.2");
using var reader = new StreamReader(file.FullName);
var entries = parser.Parse(reader).ToList();
result = new AnalysisResult(
FileName: file.Name,
FullName: file.FullName,
State: AnalysisState.Succeeded,
Entries: entries,
ErrorMessage: null,
WorkerId: workerId
);
}
catch (Exception ex)
{
// Save exception message to result
throw new NotImplementedException("TODO: T2.2");
result = new AnalysisResult(
FileName: file.Name,
FullName: file.FullName,
State: AnalysisState.Failed,
Entries: Array.Empty<LogEntry>(),
ErrorMessage: ex.Message,
WorkerId: workerId
);
}

/*
* Save parse result.
* [!Important] Remember to lock _syncRoot to prevent data race.
* [!Important] Remember to lock _syncRoot to prevent data race.
*/
throw new NotImplementedException("TODO: T2.2");
lock (_syncRoot)
{
_analysisResults[file.Name] = result;
}
}
}
}
Expand Down
Loading
Loading