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
29 changes: 29 additions & 0 deletions docs/01-basic/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Task 1 问答作业报告

## (Q1.1)

1. **按逗号分割与字段指定**
- **分割**:在 `LineParser.cs` 中,通过 `line.Record.Split(',', 4)` 将日志按逗号分割为 4 个部分。
- **意义**:代码直接通过数组索引(`parts[0]` 代表行号 LineNo,`parts[1]` 代表时间戳 Timestamp,`parts[2]` 代表 Pod 名称,`parts[3]` 代表 JSON 消息 Body)来定位字段意义。

2. **判断日志种类**
- **位置与语句**:在 `LineParser.ParseLine` 方法中,先通过 `JsonDocument.Parse(jsonString)` 解析 JSON 字符串,再通过 `root.GetProperty("event").GetString()` 获取 `event` 字段的值(如 `call`、`request`、`internal`),并用 `switch` 分支判断日志种类。

3. **JSON 解析与格式转换**
- **调用的库方法**:使用了 .NET 标准库 `System.Text.Json` 中的 `JsonSerializer.Deserialize<T>(...)`。
- **防止字段缺失**:在消息接收模型的属性上添加了 `[JsonRequired]` 特性,如果 JSON 中缺失对应的必填字段,反序列化时将自动抛出异常。
- **命名法转换(kebab-case 转换为 PascalCase)**:在属性上添加 `[JsonPropertyName("abc-def")]` 特性显式指定 JSON 键名,或在 `JsonSerializerOptions` 中设置 `PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower`。

## (Q1.2)

调用 `KeyValueVisitor` 的 `Dump` 方法时,对于 `Call` 事件的方法调用链如下:

+ `Dictionary<string, string> KeyValueVisitor.Dump(LogEntry entry)`
+ `TResult LogEntry.Accept<TResult>(ILogEntryVisitor<TResult> visitor)`(实际运行时动态派发调用 `CallLogEntry.Accept`)
+ `Dictionary<string, string> KeyValueVisitor.Visit(CallLogEntry entry)`

## (Q1.3.b)

+ **给予 AI 的提示词**:报错信息,请ai分析问题
+ **AI 的优势**:AI 能快速排查并指出 `JsonRequired` 大小写拼写错误等语法细节
+ **AI 的问题**:AI 偶尔会写出语法缺失的代码,会缺少部分语句,需要我自己检查。
Binary file added docs/02-multithreading/images/error_run.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/02-multithreading/images/normal_run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions docs/02-multithreading/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 实验报告

## 1. 功能演示

### 1.1 功能实现介绍
启动 `LocalCli` 后,选择日志所在目录,选择需要解析的日志文件,点击“开始解析”按钮,系统将自动并发解析日志文件并显示结果。

![正常解析流程](images/normal_run.png)

* **功能说明**:主线程将目录下的 `.log` 文件投入 `WorkQueue<FileInfo>`,由 4 个 Worker 线程并发拉取并解析,结果安全写入共享字典。

### 1.2 鲁棒性展示
当用户输入不存在的日志路径或文件名时,系统发现错误,给出提示,要求重新输入:

![异常捕获展示](images/error_run.png)

* **功能说明**:系统在 `LogFileAnalyzer` 中校验路径有效性,并在 Worker 线程遇到异常时将其状态标记为 `AnalysisState.Failed`,记录错误信息而不会导致主程序崩溃。
## 2. 问题解答

### Q2.1

#### 1.
* **共享变量**:
* `Queue<T> _items`(存储工作项的队列)
* `bool _isCompleted`(标记队列是否已完成添加的标志位)
* **保护机制**:
* 使用 **`lock (_items)`**(即 `Monitor.Enter` / `Monitor.Exit` )将对 `_items` 队列的所有读写操作(`Enqueue`、`TryDequeue`、`CompleteAdding`、`IsCompleted`)以及线程间的等待与唤醒(`Monitor.Wait`、`Monitor.Pulse`、`Monitor.PulseAll`)统一放在以 `_items` 作为锁对象的临界区中,确保互斥。

#### 2.
* **共享变量**:
* `_currentDirectory`(当前加载的日志目录路径)
* `_isAnalyzing`(是否处于正在分析状态的标志)
* `_logFiles`(存储文件名与对应 `FileInfo` 的字典)
* `_analysisResults`(存储文件名与解析结果 `AnalysisResult` 的字典)
* **保护机制**:
* 专门声明了一个私有只读的互斥锁对象:`private readonly object _syncRoot = new();`。
* 在状态读写(`IsAnalyzing`)、目录切换(`ChangeDirectory`)、获取文件列表、查询结果以及 Worker 线程更新解析结果(`WorkerMain` 中的字典赋值)等所有涉及共享状态读取或修改的代码块中,均使用了 **`lock (_syncRoot)`** 进行临界区保护。

#### 3.
* 1.虚假唤醒:消费者线程在队列仍然为空(_items.Count == 0)时可能被系统信号唤醒。由于使用的是 if,线程被唤醒后不再重新校验队列数量,直接向下执行 _items.Dequeue(),从而抛出 InvalidOperationException(对空队列执行 Dequeue 异常) 导致程序崩溃。

* 2.竞争抢锁失效:当生产者放入 1 个元素并调用 PulseAll 唤醒多个阻塞在 Wait 上的消费者时,所有唤醒的消费者会重新竞争锁。假设线程 A 抢到锁并弹出该元素,锁被释放后线程 B 接着拿到锁。如果使用 if,线程 B 会直接执行 Dequeue() 试图弹出元素,但此时队列已经被线程 A 消费空了,同样导致崩溃。

### Q2.1

#### 1.
var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)
.Select(filePath => Path.GetFileName(filePath))
.OrderBy(fileName => fileName);

#### 2.

将 Directory.EnumerateFiles 中的第三个参数从 SearchOption.TopDirectoryOnly 修改为 SearchOption.AllDirectories 可实现递归扫描。

### Q2.3b
* 给AI的提示词一般是报错的诊断,将报错提供,要求分析问题。AI产生的错误在于不能理解多个代码文件的统一性,它针对报错给出的修改与和其余文件中的代码不匹配,需要我自己查找修改。适中。
Binary file added docs/03-async-grpc/images/result.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/03-async-grpc/images/robustness.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/03-async-grpc/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# gRPC
## 1.功能介绍和截图
### 1.1功能介绍
* 作为常住在服务器的Agent,通过gRPC服务完成日志解析。启动后自动发送Ping请求,确认状态正常,同时工作后支持用户输入工作目录,对指定日志文件进行分析,流式输出结果。
![运行结果](./images/result.png)
![鲁棒性](./images/robustness.png)
## 2.问题回答
### Q3.1
* 区别:网络开发分为服务端和客户端,需要多个项目同时启动,非网络只需要本地内存方法调用,网络则需要跨进程/网络调用。
* 额外难点:存在网络环境延迟,报错时候难以具体区分原因。
* 复杂之处:需要异步处理。
### Q3.2b
* 给予AI的提示词是报错信息和讲解代码框架,并命令AI讲解一些知识点。AI分析报错原因时候常常不能结合多个.cs文件一同考量,AI同时会讲解一下理论知识。
90 changes: 86 additions & 4 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;
}
}
}
}
92 changes: 57 additions & 35 deletions src/LogAnalyzer/LogFileAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using LogParser.Models;
using LogParser.Parser;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace LogAnalyzer
{
Expand Down Expand Up @@ -138,10 +142,8 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable<string> fileNames)
}
fileList = fileNameList.Select(fileName => _logFiles[fileName]).ToList();

/*
* Set _isAnalyzing
*/
// TODO: T2.2
//设置正在分析状态为true
_isAnalyzing = true;
}

try
Expand All @@ -150,11 +152,11 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable<string> fileNames)
}
finally
{
/*
* Unset _isAnalyzing
* Remember to lock _syncRoot to prevent data race
*/
// TODO: T2.2
//分析结束,设置一个锁保护,在其中重置状态
lock (_syncRoot)
{
_isAnalyzing = false;
}
}
}

Expand All @@ -165,11 +167,15 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis
{
foreach (var file in fileList)
{
/*
* Filter unparsed files.
* If there is an unknown file, throw System.InvalidOperationException.
*/
throw new NotImplementedException("TODO: T2.2");
if (!_analysisResults.ContainsKey(file.Name))
{
throw new InvalidOperationException($"File '{file.Name}' is unknown.");
}

if (_analysisResults[file.Name].State != AnalysisState.Succeeded)
{
logFilesToParse.Add(file);
}
}
}

Expand All @@ -180,27 +186,29 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList<FileInfo> fileLis

var queue = new WorkQueue<FileInfo>();

/*
* 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];
for (int i = 0; i < degreeOfParallelism; i++)
{
int workerId = i;
string threadName = $"log-analyzer-worker-{workerId}";
/*
* Create and start threads to run `WorkerMain`
*/
// TODO: T2.2
workers[i] = new Thread(() => WorkerMain(workerId, queue))
{
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 @@ -212,20 +220,34 @@ private void WorkerMain(int workerId, WorkQueue<FileInfo> queue)
AnalysisResult result;
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.ToString(),
WorkerId: workerId
);
}

/*
* Save parse result.
* [!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