diff --git a/.gitignore b/.gitignore index d990a44..e502344 100644 --- a/.gitignore +++ b/.gitignore @@ -405,3 +405,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.obsidian/ + +.zcode/ diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..a089756 --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,75 @@ +## 问答题 + +### (Q1.1) + +在给出的代码框架 `Parser` 中: + ++ 哪条语句或哪几条语句将日志按逗号进行分割?代码中,我们是如何指定每一行的第几个字段代表何种意义的? + +```c +// 按逗号进行分割 +using var csv = new CsvReader(logFile, config); +csv.Context.RegisterClassMap(); +foreach (var logRecord in csv.GetRecords()) +``` + +通过 LogRecordMap 指定意义; + ++ 在对日志中 JSON 格式的 `message` 字段进行读取时,我们是在哪个方法内用哪几条语句判断这一行日志的种类(Call / Request / Internal)的? + +```c +// 通过 LineRarse 的 ParseLine 方法 + +if (root.TryGetProperty("event", out var eventElement)) +{ + return eventElement.GetString() switch + { + "call" => LineParser.CreateCall(logRecord), + "request" => LineParser.CreateRequest(logRecord), + "internal" => LineParser.CreateInternal(logRecord), + _ => throw new FormatException($"Unknown event type: {eventElement.GetString()} in log message: {logRecord.Message}") + }; +} +else +{ + throw new FormatException($"Log message does not contain 'event' property: {logRecord.Message}"); +} + +``` + ++ 在确定了日志种类后,我们是调用了哪个库方法对 JSON 进行解析的? + +用 System.Text.Json 的 JsonSerializer.Deserialize(logRecord.Message, options) + + + 进一步,我们的框架代码是如何防止日志中有字段缺失的?(例如所给的 Call 日志的 `message` 中缺失 `request_id` 字段) + + 使用 [property: JsonRequired] + + + 更进一步,日志中的 JSON 的键是 `abc-def` 命名法(称为烤串命名法),而我们的解析结果却是放在 `AbcDef` 命名法(称为大驼峰命名法)的属性里,我们的框架代码中是如何告诉 JSON 解析器完成这一命名法转换的? + +```c +// 使用 JsonSerializerOptions +private static JsonSerializerOptions options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, +}; +``` + +### (Q1.2) + +以一个 Call 事件的解析结果为例,当调用 `KeyValueVisitor` 的 `Dump` 方法后,都有哪些方法被调用?请补充完整如下的方法调用链(.NET 内置库无需写出): + ++ `Dictionary KeyValueVisitor.Dump(LogEntry entry)` ++ `TResult CallLogEntry.Accept(ILogEntryVisitor visitor)` + ++ `Dictionary KeyValueVisitor.Visit(CallLogEntry entry)` + +### (Q1.3) + +#### (Q1.3.b) + +如果使用了 AI,你给予 AI 的提示词是什么?你认为 AI 给出的解答、你完全凭借传统搜索引擎以及自己的能力能够写出的解答之间,AI 的解答比你好在哪?AI 又有哪些解答是存在问题的,或者至少是不如你自己的解答的?给出你的理由。 + +- 使用的是 copilot 补全代码,以及借助 codex 理解代码; + +- 我让 codex 理解一下 01-basic 代码部分已有的工作;然后自己阅读代码的时候借助 copilot 补全注释的能力理解一些看不明白的语法以及补全补全一些重复的代码;但是 copilot 的补全能力不像 codex 之类的整个仓库一起阅读好像不太能理解代码上下文和所文件情况比如对于对于 Call, Request 之类不同 Json 字段他的补充方式就是完全照搬原本写的,所以还是得靠自己 review 完动手调() \ No newline at end of file diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..c47f053 --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,45 @@ +## 问答题 + +问答题的提交方式是在 `docs/02-multithreading` 中的 `report.md` 文件中进行你对问题的解答。 + +### (Q2.1) + +本问题考察关于临界区的理解。 + +我们把访问临界资源的程序片段称作临界区。在我们的多线程程序当中,临界资源即为不同线程的共享变量。请问: + ++ `WorkQueue` 类中的共享变量有哪些?是通过什么保护其免于数据竞争(data race)呢? + +_isCompleted and _items,通过 lock + ++ `LogFileAnalyzer` 类中的共享变量有哪些?是通过什么保护其免于数据竞争呢? + +_currentDirectory、_isAnalyzing、_logFiles、_analysisResults,通过 lock + ++ 如果条件变量的判断条件使用了 `if` 判断而非 `while` 判断,当出现了虚假唤醒现象时(在类 UNIX 系统中,由于 UNIX 信号等机制,即使没有人调用过 `signal` 或 `broadcast`,处于 `wait` 当中的条件变量也可能被唤醒),会出现什么后果?结合无限仓库容量的生产者消费者问题简单叙述一下。 + +可能会对空队列 Dequeue 抛出异常 + +### (Q2.2) + +在给出的代码框架 `LogFileAnalyzer` 中: + ++ 那一段代码扫描了给定的目录中的全部 `.log` 后缀的日志文件? + +ChangeDirectory 方法 + ++ 假使给定的需求是不但要扫描给定目录中的日志文件,还要递归地获取给定的目录的全部子目录、子子目录……内的日志文件,应当如何做(简要回答即可)? + +可以扫描的时候多判断一次该目录下的每一个文件、对于日志文件解析,对于目录文件同样调用扫描函数递归进入该子目录扫描 + +### (Q2.3) + +#### (Q2.3.b) + +如果使用了 AI,你给予 AI 的提示词是什么?你对 AI 的使用是询问 AI 一些接口的用法或是在某处的写法,还是让 AI 帮你写一部分作业代码,又或是让 AI 给你讲解代码框架?AI 的解答是否出现过错误(如果有,是哪些)?你认为本节的难度是偏低、适中,还是偏高? + +仍然是让他分析代码,对于不熟悉的命名法,讲解每个文件和类是做什么的,另外补全一些简单代码; + +出现过错误,比如实现简单 CLI 时补全输出的显示代码的时候由于跨文件所以补写出一些不存在的属性比如 LogEntry 中出现不存在的 Message 属性 + +有些难度,感觉仍然不熟悉 C# 语法(x \ No newline at end of file diff --git a/docs/03-async-grpc/assets/Pasted image 20260830144545.png b/docs/03-async-grpc/assets/Pasted image 20260830144545.png new file mode 100644 index 0000000..ea48bbe Binary files /dev/null and b/docs/03-async-grpc/assets/Pasted image 20260830144545.png differ diff --git a/docs/03-async-grpc/assets/Pasted image 20260830144609.png b/docs/03-async-grpc/assets/Pasted image 20260830144609.png new file mode 100644 index 0000000..213512e Binary files /dev/null and b/docs/03-async-grpc/assets/Pasted image 20260830144609.png differ diff --git a/docs/03-async-grpc/report.md b/docs/03-async-grpc/report.md new file mode 100644 index 0000000..31185d3 --- /dev/null +++ b/docs/03-async-grpc/report.md @@ -0,0 +1,22 @@ +![[Pasted image 20260830144545.png]]![[Pasted image 20260830144609.png]] + +## 问答题 + +### (Q3.1) + +你认为,你在开发网络应用程序,与你在以往开发非网络应用程序的区别在哪里?网络应用程序的开发存在哪些额外的难点?存在哪些额外的复杂之处? + +LocalCli 是全在一个进程里。这次调用 client.AnalyzeFilesAsync(),服务端在另一个进程,中间隔着网络。 + +我感觉那些要知道数据那些自动生成的方法和属性很麻烦,得去翻 proto 文件或 obj 里生成的代码(`FileNames` 还是 `LogFiles`、oneof 生成 `PayloadCase`) + +### (Q3.2) + +本次作业中,你是否使用了 AI?根据你的使用情况,在以下 (Q3.2.a) (Q3.2.b) 两个问题中选择一题作答: + +#### (Q3.2.b) + +帮我解释一些接口,帮我讲解那些 proto 自动生成的那些数据类型的属性和方法,也讲解一些框架,感觉仍然不是很明白,有一种被 ai 牵着走写代码的感觉(悲),仍然不太记得住和理解那些 C# 中的复杂的属性方法之类的名称和用法(晕),只能一边写一边求助 ai( + +深刻的感觉到了我拖累了 ai 的水平( + diff --git a/docs/04-avalonia/assets/report-01-xxx.png b/docs/04-avalonia/assets/report-01-xxx.png new file mode 100644 index 0000000..99791e6 Binary files /dev/null and b/docs/04-avalonia/assets/report-01-xxx.png differ diff --git a/docs/04-avalonia/assets/report-02-xxx.png b/docs/04-avalonia/assets/report-02-xxx.png new file mode 100644 index 0000000..6dda94a Binary files /dev/null and b/docs/04-avalonia/assets/report-02-xxx.png differ diff --git a/docs/04-avalonia/assets/report-03-xxx.png b/docs/04-avalonia/assets/report-03-xxx.png new file mode 100644 index 0000000..4e99257 Binary files /dev/null and b/docs/04-avalonia/assets/report-03-xxx.png differ diff --git a/docs/04-avalonia/report.md b/docs/04-avalonia/report.md new file mode 100644 index 0000000..b607113 --- /dev/null +++ b/docs/04-avalonia/report.md @@ -0,0 +1,38 @@ +## 功能实现 + + +图形界面客户端 LogAnalyzerClient 的如下功能(T4.1): + +1. 刷新文件列表(Refresh):调用 `GetLogFiles` RPC 获取服务器当前目录的日志文件,清空并重建 `LogFiles` 集合,界面列表自动刷新。菜 + +2. 分析选中的文件(Selected):Ctrl 多选文件后点击 Selected,调用 `AnalyzeFiles` RPC,请求中带上并行度和选中的文件名列表;并行度会先做整数校验。 + +3. 分析全部文件(All):在 Analyze 栏新增 All 按钮(axaml 加第五列),绑定新写的`AnalyzeAllCommand`,调用 `AnalyzeAll` RPC。 + +4. 分析右键选中的文件(Analyze File):右键菜单触发 `AnalyzeRightClickedFileCommand`,分析当前选中的单个文件。 + +5. 查看分析结果(View Analysis Results):右键菜单触发,使用流式 RPC `GetAnalysisResult` + +## 测试截图 + +![[report-01-xxx.png]] + +![[report-02-xxx.png]] +![[report-03-xxx.png]] +## 问答题 + +### (Q4.1) + +你认为,你在开发 GUI 应用程序,与你在以往开控制台应用程序的区别在哪里?GUI 应用程序的开发存在哪些额外的难点?存在哪些额外的复杂之处?你是否有通过编写 GUI 应用程序对异步 `async` 和 `await` 有了更进一步的理解?异步编程是否又给你带来的额外的困扰?说说你的看法。 + +它需要一些 UI 设计的品味,如何布局等,需要确认耦合关系; + +但是我对于异步编程以及其他模块中自动生成编译的方法属性仍然很不熟悉,不太清楚它的使用方法和原理,导致在调试时遇到了一些问题。包括命名、对应关系、如何在程序中定位某个方法和属性并 F12 定位后完之后如何看懂它都遇到了困难 + +### (Q4.2) + +#### (Q4.2.b) + +如果使用了 AI,你给予 AI 的提示词是什么?你对 AI 的使用是询问 AI 一些接口的用法、gRPC 的使用,或是在某处的写法,还是让 AI 帮你写一部分作业代码,又或是让 AI 给你讲解代码框架?AI 的解答是否出现过错误(如果有,是哪些)?你从 AI 那里是否得知了一些关于异步,或是 gRPC 等原本你不知道或是难以理解的知识? + +仍然是让 AI 讲解代码框架,询问一些参数的使用方法,帮我确认代码问题和 Debug,处理一些不明白的报错,对于一些不太清楚的接口写法进行确认。 \ No newline at end of file diff --git a/docs/appendix/appendix-a-glossary.md b/docs/appendix/appendix-a-glossary.md index f6c5b64..758745d 100644 --- a/docs/appendix/appendix-a-glossary.md +++ b/docs/appendix/appendix-a-glossary.md @@ -40,27 +40,27 @@ ## 云服务与日志 -| 简体中文(大陆) | 繁體中文(台灣) | English (United States) | -| --- | --- | --- | -| 云 | 雲端 | cloud | -| 云服务 | 雲端服務 | cloud service | -| 微服务 | 微服務 | microservice | -| 架构 | 架構 | architecture | -| 部署 | 部署 | deployment | -| 前端 | 前端 | frontend | -| 后端 | 後端 | backend | -| 日志 | 記錄 | log | -| 日志文件 | 記錄檔 | log file | -| 结构化日志 | 結構化記錄 | structured log | -| 半结构化日志 | 半結構化記錄 | semi-structured log | -| 非结构化日志 | 非結構化記錄 | unstructured log | -| 日志等级 | 記錄層級 | log level | -| 字段 | 欄位 | field | -| 故障诊断 | 故障診斷 | fault diagnosis | -| 异常检测 | 異常偵測 | anomaly detection | -| 大语言模型 | 大型語言模型 | large language model (LLM) | -| 可观测性 | 可觀測性 | observability | -| 云服务拓扑 | 雲端服務拓撲 | cloud service topology | +| 简体中文(大陆) | 繁體中文(台灣) | English (United States) | +| -------- | -------- | -------------------------- | +| 云 | 雲端 | cloud | +| 云服务 | 雲端服務 | cloud service | +| 微服务 | 微服務 | microservice | +| 架构 | 架構 | architecture | +| 部署 | 部署 | deployment | +| 前端 | 前端 | frontend | +| 后端 | 後端 | backend | +| 日志 | 記錄 | log | +| 日志文件 | 記錄檔 | log file | +| 结构化日志 | 結構化記錄 | structured log | +| 半结构化日志 | 半結構化記錄 | semi-structured log | +| 非结构化日志 | 非結構化記錄 | unstructured log | +| 日志等级 | 記錄層級 | log level | +| 字段 | 欄位 | field | +| 故障诊断 | 故障診斷 | fault diagnosis | +| 异常检测 | 異常偵測 | anomaly detection | +| 大语言模型 | 大型語言模型 | large language model (LLM) | +| 可观测性 | 可觀測性 | observability | +| 云服务拓扑 | 雲端服務拓撲 | cloud service topology | ## C\#、面向对象程序设计与设计模式 diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..79cae37 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -112,22 +112,125 @@ 6. Exit. private static void ShowLogFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + var logFiles = analyzer.GetLogFiles(); + if (logFiles.Count == 0) + { + Console.WriteLine("No log files found."); + return; + } + Console.WriteLine("Log files:"); + foreach (var file in logFiles) + { + Console.WriteLine($"- {file}"); + } } private static void AnalyzeFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Enter the degree of parallelism:"); + int degreeOfParallelism = 0; + Console.Write(">>> "); + var degreeStr = Console.ReadLine(); + if (degreeStr is null) + { + Console.WriteLine("No degree of parallelism entered."); + return; + } + try + { + degreeOfParallelism = int.Parse(degreeStr); + } + catch (Exception) + { + Console.WriteLine("Invalid input, please try again."); + return; + } + Console.WriteLine("please input log file names (comma separated):"); + Console.Write(">>> "); + var fileNamesStr = Console.ReadLine(); + if (fileNamesStr is null) + { + Console.WriteLine("No file names entered."); + return; + } + var fileNames = fileNamesStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + try + { + analyzer.AnalyzeFiles(degreeOfParallelism, fileNames); + Console.WriteLine("Analysis completed."); + } + catch (Exception ex) + { + Console.WriteLine($"Analysis failed: {ex.Message}"); + } } private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Enter the degree of parallelism:"); + int degreeOfParallelism = 0; + Console.Write(">>> "); + var degreeStr = Console.ReadLine(); + if (degreeStr is null) + { + Console.WriteLine("No degree of parallelism entered."); + return; + } + try + { + degreeOfParallelism = int.Parse(degreeStr); + } + catch (Exception) + { + Console.WriteLine("Invalid input, please try again."); + return; + } + try + { + analyzer.AnalyzeAll(degreeOfParallelism); + Console.WriteLine("Analysis completed."); + } + catch (Exception ex) + { + Console.WriteLine($"Analysis failed: {ex.Message}"); + } } - private static void GetAnalysisResult(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("please input log file name:"); + Console.Write(">>> "); + var fileName = Console.ReadLine(); + if (fileName is null) + { + Console.WriteLine("No file name entered."); + return; + } + try + { + if (!analyzer.TryGetAnalysisResult(fileName, out var result) || result is null) + { + Console.WriteLine($"File '{fileName}' not found in the current directory."); + return; + } + Console.WriteLine($"Analysis result for {fileName}:"); + Console.WriteLine($"- State: {result.State}"); + if (result.State == AnalysisState.Failed) + { + Console.WriteLine($"- Error message: {result.ErrorMessage}"); + } + else + { + Console.WriteLine($"- Number of entries: {result.Entries.Count}"); + foreach (var entry in result.Entries) + { + Console.WriteLine($" - {entry.Timestamp}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error getting analysis result: {ex.Message}"); + } } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..6728808 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -137,11 +137,7 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) } } fileList = fileNameList.Select(fileName => _logFiles[fileName]).ToList(); - - /* - * Set _isAnalyzing - */ - // TODO: T2.2 + _isAnalyzing = true; } try @@ -150,11 +146,10 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) } finally { - /* - * Unset _isAnalyzing - * Remember to lock _syncRoot to prevent data race - */ - // TODO: T2.2 + lock(_syncRoot) + { + _isAnalyzing = false; + } } } @@ -165,11 +160,14 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList 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.TryGetValue(file.Name, out var isExist)) + { + throw new InvalidOperationException($"Unknown file '{file.FullName}'."); + } + if (isExist.State != AnalysisState.Succeeded) + { + logFilesToParse.Add(file); + } } } @@ -180,27 +178,21 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis var queue = new WorkQueue(); - /* - * 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 queue) @@ -212,20 +204,33 @@ private void WorkerMain(int workerId, WorkQueue 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(), + ErrorMessage: ex.Message, + 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; + } } } } diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..6cdd6a9 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -20,17 +20,39 @@ public bool IsCompleted public void Enqueue(T item) { - throw new NotImplementedException("TODO: T2.1"); + lock(_items) + { + if(_isCompleted) + { + throw new InvalidOperationException("Cannot enqueue to a completed queue."); + } + _items.Enqueue(item); + Monitor.Pulse(_items); + } } public bool TryDequeue([NotNullWhen(true)] out T? item) { - throw new NotImplementedException("TODO: T2.1"); + lock(_items) + { + while(_items.Count == 0 && _isCompleted == false) Monitor.Wait(_items); + if(_items.Count == 0 && _isCompleted) + { + item = default; + return false; + } + item = _items.Dequeue(); + return true; + } } public void CompleteAdding() { - throw new NotImplementedException("TODO: T2.1"); + lock(_items) + { + _isCompleted = true; + Monitor.PulseAll(_items); + } } } } diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..b3addb0 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -79,22 +79,115 @@ public Task GetLogFiles(Empty empty, CancellationToken canc public Task ChangeDirectory(ChangeDirectoryRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new ChangeDirectoryResponse(); + try + { + bool success = _analyzer.ChangeDirectory(request.DirectoryPath); + response.Status = success + ? CreateNoErrorOperationStatus() + : new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.DirectoryNotFound, + Message = $"Invalid directory path: {request.DirectoryPath}.", + }; + if (success) + { + response.CurrentDirectory = _analyzer.CurrentDirectory ?? ""; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + } + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while changing directory."); + } + return Task.FromResult(response); } public Task AnalyzeAll(AnalyzeAllRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeAllResponse(); + try + { + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing all log files."); + } + return Task.FromResult(response); } public Task AnalyzeFiles(AnalyzeFilesRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeFilesResponse(); + try + { + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex){ + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing specified log files."); + } + return Task.FromResult(response); } public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var responses = new List(); + try + { + bool is_found = _analyzer.TryGetAnalysisResult(request.FileName, out var result); + if (!is_found || result is null) + { + responses.Add(new GetAnalysisResultResponse() + { + Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"Analysis result for file '{request.FileName}' not found.", + }, + }); + return responses; + } + responses.Add(new GetAnalysisResultResponse() + { + Header = new AnalysisResultHeaderMessage() + { + FileName = result.FileName, + FullName = result.FullName, + State = GrpcTypeConverter.ConvertToGrpc(result.State), + WorkerId = result.WorkerId, + ErrorMessage = result.ErrorMessage ?? "", + }, + Status = CreateNoErrorOperationStatus(), + }); + if (result.State == AnalysisState.Succeeded) + { + foreach (var logEntry in result.Entries) + { + responses.Add(new GetAnalysisResultResponse() + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(logEntry), + Status = CreateNoErrorOperationStatus(), + }); + } + } + } + catch (Exception ex) + { + responses.Add(new GetAnalysisResultResponse() + { + Status = CreateInternalErrorOperationStatus(ex), + }); + _logger.LogError(ex, "An error occurred while retrieving analysis result for file '{FileName}'.", request.FileName); + } + return responses; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..d8a8bea 100644 --- a/src/LogAnalyzerAgent/Services/AgentService.cs +++ b/src/LogAnalyzerAgent/Services/AgentService.cs @@ -29,27 +29,31 @@ public override Task GetAgentStatus(Empty empty, ServerC public override Task ChangeDirectory(ChangeDirectoryRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.ChangeDirectory(request, context.CancellationToken); } public override Task GetLogFiles(Empty empty, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.GetLogFiles(empty, context.CancellationToken); } public override Task AnalyzeAll(AnalyzeAllRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.AnalyzeAll(request, context.CancellationToken); } public override Task AnalyzeFiles(AnalyzeFilesRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.AnalyzeFiles(request, context.CancellationToken); } public override async Task GetAnalysisResult(GetAnalysisResultRequest request, IServerStreamWriter responseStream, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var results = _session.GetAnalysisResult(request, context.CancellationToken); + foreach (var item in results) + { + await responseStream.WriteAsync(item); + } } } } diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs b/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs index 2ff1b64..b2e3631 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Models/RemoteModels.cs @@ -11,7 +11,11 @@ public sealed record LogFileItem(string FileName) public sealed record LogFields(int Index, IReadOnlyList Fields, string? ErrorMessage) { - public string Summary => "TODO: T4.1"; + public string Summary => ErrorMessage is not null + ? $"Analyze Failed, Error: {ErrorMessage}" + : Fields.Count == 0 + ? "No fields Found" + : $"#{Index} " + string.Join(", ", Fields.Select(f => $"{f.Key}: {f.Value}")); } public sealed record LogFieldItem(string Key, string Value); diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs b/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs index 91c05a8..30a84b5 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs +++ b/src/LogAnalyzerClient/LogAnalyzerClient/ViewModels/MainViewModel.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -132,31 +133,161 @@ private async Task RefreshAsync() { await WithClientNotNull(async () => { - throw new NotImplementedException("TODO: T4.1"); + var response = await _client!.GetLogFilesAsync(new Empty()); + if (!response.Status.Success) { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + return; + } + LogFiles.Clear(); + foreach (var filename in response.FileNames) { + LogFiles.Add(new LogFileItem(filename)); + } }); } [RelayCommand] private async Task AnalyzeSelectedFilesAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + if (!int.TryParse(DegreeOfParallelismText, out var dop) || dop < 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "The DegreeOfParallelism is invalid!"); + return; + } + if (SelectedFiles.Count == 0) { + await DialogHelper.ShowMessageDialogAsync("Error", "No files selected."); + return; + } + var response = await _client!.AnalyzeFilesAsync(new AnalyzeFilesRequest + { + DegreeOfParallelism = dop, + FileNames = { SelectedFiles, }, + }); + if (!response.Status.Success) + { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + return; + } + }); + } + + [RelayCommand] + private async Task AnalyzeAllAsync() + { + await WithClientNotNull(async () => + { + if (!int.TryParse(DegreeOfParallelismText, out var dop) || dop < 0) { + await DialogHelper.ShowMessageDialogAsync("Error", "The DegreeOfParallelism is invalid!"); + return; + } + var response = await _client!.AnalyzeAllAsync(new AnalyzeAllRequest { DegreeOfParallelism = dop, }); + if (!response.Status.Success) { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + return; + } + }); } - /* - * TODO: T4.1 - * Add AnalyzeAllAsync ReplayCommand - */ [RelayCommand] private async Task AnalyzeRightClickedFileAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + var file = SelectedLogFile?.FileName; + if (file is null) { + await DialogHelper.ShowMessageDialogAsync("Error", "No file selected."); + return; + } + if (!int.TryParse(DegreeOfParallelismText, out var dop) || dop < 0) + { + await DialogHelper.ShowMessageDialogAsync("Error", "The DegreeOfParallelism is invalid!"); + return; + } + var response = await _client!.AnalyzeFilesAsync(new AnalyzeFilesRequest + { + DegreeOfParallelism = dop, + FileNames = { file, }, + }); + if (!response.Status.Success) { + await DialogHelper.ShowMessageDialogAsync("Error", $"{response.Status.Code}: {response.Status.Message}"); + } + }); } [RelayCommand] private async Task GetAnalysisResultAsync() { - throw new NotImplementedException("TODO: T4.1"); + await WithClientNotNull(async () => + { + var file = SelectedLogFile?.FileName; + if (file is null) { + await DialogHelper.ShowMessageDialogAsync("Error", "No file selected."); + return; + } + ResultEntries.Clear(); + using var call = _client!.GetAnalysisResult(new GetAnalysisResultRequest { FileName = file, }); + while (await call.ResponseStream.MoveNext()) + { + var entry = call.ResponseStream.Current; + switch (entry.PayloadCase) + { + case GetAnalysisResultResponse.PayloadOneofCase.Header: + switch (entry.Header.State) + { + case AnalysisStateEnum.NotAnalyzed: + ResultEntries.Add(new LogFields(0, new List(), null)); + break; + case AnalysisStateEnum.Failed: + var error = entry.Header.HasErrorMessage ? entry.Header.ErrorMessage : "unknown error"; + ResultEntries.Add(new LogFields(0, new List(), error)); + break; + case AnalysisStateEnum.Succeeded: + break; + default: break; + } + break; + case GetAnalysisResultResponse.PayloadOneofCase.LogEntry: + var fields = new List(); + int lineNo; + switch (entry.LogEntry.EntryCase) + { + case LogEntryMessage.EntryOneofCase.CallLogEntry: + lineNo = entry.LogEntry.CallLogEntry.LineNo; + fields.Add(new LogFieldItem("severity", entry.LogEntry.CallLogEntry.Severity.ToString())); + fields.Add(new LogFieldItem("pod", entry.LogEntry.CallLogEntry.PodName)); + fields.Add(new LogFieldItem("request_id", entry.LogEntry.CallLogEntry.RequestId)); + fields.Add(new LogFieldItem("target_service", entry.LogEntry.CallLogEntry.TargetService)); + fields.Add(new LogFieldItem("duration_ms", entry.LogEntry.CallLogEntry.DurationMs.ToString())); + break; + case LogEntryMessage.EntryOneofCase.RequestLogEntry: + lineNo = entry.LogEntry.RequestLogEntry.LineNo; + fields.Add(new LogFieldItem("severity", entry.LogEntry.RequestLogEntry.Severity.ToString())); + fields.Add(new LogFieldItem("pod", entry.LogEntry.RequestLogEntry.PodName)); + fields.Add(new LogFieldItem("request_id", entry.LogEntry.RequestLogEntry.RequestId)); + fields.Add(new LogFieldItem("method", entry.LogEntry.RequestLogEntry.Method)); + fields.Add(new LogFieldItem("path", entry.LogEntry.RequestLogEntry.Path)); + fields.Add(new LogFieldItem("status_code", entry.LogEntry.RequestLogEntry.StatusCode.ToString())); + break; + case LogEntryMessage.EntryOneofCase.InternalLogEntry: + lineNo = entry.LogEntry.InternalLogEntry.LineNo; + fields.Add(new LogFieldItem("severity", entry.LogEntry.InternalLogEntry.Severity.ToString())); + fields.Add(new LogFieldItem("pod", entry.LogEntry.InternalLogEntry.PodName)); + fields.Add(new LogFieldItem("exception", entry.LogEntry.InternalLogEntry.ExceptionName)); + fields.Add(new LogFieldItem("message", entry.LogEntry.InternalLogEntry.ExceptionMessage)); + break; + default: + return; + } + ResultEntries.Add(new LogFields(lineNo, fields, null)); + break; + default: + break; + } + + } + }); } [RelayCommand] diff --git a/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml b/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml index fffef7e..e9cef32 100644 --- a/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml +++ b/src/LogAnalyzerClient/LogAnalyzerClient/Views/MainView.axaml @@ -79,7 +79,7 @@ to set the actual DataContext for runtime, set the DataContext property in code - +