diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..21b9dfe --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,45 @@ +# 问答题报告 + +### (Q1.1) + +在给出的代码框架 `Parser` 中: + +* 哪条语句或哪几条语句将日志按逗号进行分割?代码中,我们是如何指定每一行的第几个字段代表何种意义的? +> using var csv = new CsvReader(logFile, config);配合循环使用的csv.GetRecords(); +>在LogRecordMap中一一指定对应 + +* 在对日志中 JSON 格式的 `message` 字段进行读取时,我们是在哪个方法内用哪几条语句判断这一行日志的种类(Call / Request / Internal)的? +> LineParser 类的 ParseLine 方法,if(root.TryGetProperty("event", out var eventElement))语句,再用swith判断 + +* 在确定了日志种类后,我们是调用了哪个库方法对 JSON 进行解析的? +> System.Text.Json 库中的 JsonSerializer.Deserialize(...) 方法 + +* 进一步,我们的框架代码是如何防止日志中有字段缺失的?(例如所给的 Call 日志的 `message` 中缺失 `request_id` 字段) +> 在每个属性前面都强制加上了 [property: JsonRequired] 特性标签 + +* 更进一步,日志中的 JSON 的键是 `abc-def` 命名法(称为烤串命名法),而我们的解析结果却是放在 `AbcDef` 命名法(称为大驼峰命名法)的属性里,我们的框架代码中是如何告诉 JSON 解析器完成这一命名法转换的? +> 框架代码事先创建了一个名为 options 的配置变量,并在其中设置了 JsonNamingPolicy.KebabCaseLower 这一规则。在调用 JsonSerializer.Deserialize 提取数据时,代码将这个 options 作为参数交给了解析工具。 + +--- + +### (Q1.2) + +以一个 Call 事件的解析结果为例,当调用 `KeyValueVisitor` 的 `Dump` 方法后,都有哪些方法被调用?请补充完整如下的方法调用链(.NET 内置库无需写出): + ++ `Dictionary KeyValueVisitor.Dump(LogEntry entry)` ++ > TResult CallLogEntry.Accept(ILogEntryVisitor visitor) ++ > Dictionary KeyValueVisitor.Visit(CallLogEntry entry) + + +--- + +### (Q1.3) + +本次作业中,你是否使用了 AI?根据你的使用情况,在以下 (Q1.3.a) (Q1.3.b) 两个问题中选择一题作答: + + +#### (Q1.3.b) +如果使用了 AI,你给予 AI 的提示词是什么?你认为 AI 给出的解答、你完全凭借传统搜索引擎以及自己的能力能够写出的解答之间,AI 的解答比你好在哪?AI 又有哪些解答是存在问题的,或者至少是不如你自己的解答的?给出你的理由。 ++ > 提示词上,在完成代码部分,我只是把ai当做搜索引擎使用,去解释一些我看不懂的C#内置函数,然后再用自己能力补充代码,不过在q1.1中第1,4,5个问题,我确实完全不了解c#内置的库是什么,让ai先给出了解答再自己借助ai去了解; ++ > 代码中,ai的解答和我几乎相同;在问答题中,ai的解答比我更具有专业性,对整个项目的把握比我更透彻 ++ > 我让只让ai给出了q1.1中1,4,5题超出我能力范围之外的解答,这些解答核实没发现什么大问题,其他的ai解答和我大致相同,我暂时未发现ai明显不如自己解答的情况。 \ No newline at end of file diff --git a/docs/02-multithreading/image-1.png b/docs/02-multithreading/image-1.png new file mode 100644 index 0000000..5382b05 Binary files /dev/null and b/docs/02-multithreading/image-1.png differ diff --git a/docs/02-multithreading/image.png b/docs/02-multithreading/image.png new file mode 100644 index 0000000..091cbb7 Binary files /dev/null and b/docs/02-multithreading/image.png differ diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..cfe4a01 --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,57 @@ +## 任务 2.3:实现前端交互逻辑 (Console UI) + +### 1. 功能介绍 +本任务在 `Program.cs` 中实现了控制台交互界面,主要完成了以下三个功能: + +* **指定文件分析 (AnalyzeFiles)**: + 接收用户输入的以逗号分隔的文件名,自动去除多余空格和空项。通过安全的方式读取用户输入的线程数,如果遇到输入格式错误或者后台任务冲突,会通过循环把用户留在当前步骤要求重新输入,防止程序崩溃。 +* **全目录分析 (AnalyzeAll)**: + 只需读取用户输入的线程数,在校验输入合法后,直接交给后台并发分析整个文件夹里的所有日志。 +* **查询结果 (GetAnalysisResult)**: + 获取用户输入的文件名,调用后台查询。如果文件还没有分析,提示未分析;如果分析失败,打印具体的报错信息;如果分析成功,则调用 `KeyValueVisitor.Dump` 方法,把底层的日志数据转换成键值对字典,并逐行清晰地打印到屏幕上。 + +### 2. 运行效果截图 + +![alt text](image.png) + + +![alt text](image-1.png) + +### 3. 问答题 + +#### (Q2.1) 临界区与数据竞争理解 + +* **`WorkQueue` 类中的共享变量有哪些?是通过什么保护其免于数据竞争(data race)呢?** + > 答:_items和_isCompleted;运用lock()形成互斥锁 + > + > + +* **`LogFileAnalyzer` 类中的共享变量有哪些?是通过什么保护其免于数据竞争呢?** + > 答:_currentDirectory,_isAnalyzing,_logFiles,_analysisResults;定义 _syncRoot作为互斥锁对象来保护 + > + > + > + +* **如果条件变量的判断条件使用了 `if` 判断而非 `while` 判断,当出现了虚假唤醒现象时(在类 UNIX 系统中,由于 UNIX 信号等机制,即使没有人调用过 `signal` 或 `broadcast`,处于 `wait` 当中的条件变量也可能被唤醒),会出现什么后果?结合无限仓库容量的生产者消费者问题简单叙述一下。** + > 答:这样的话会导致消费者在被异常唤醒时跳过if检查直接强行尝试从空仓库中取商品,,进而导致程序发生崩溃。 + > + > + > + +#### (Q2.2) 目录扫描逻辑 + +* **在给出的代码框架 `LogFileAnalyzer` 中,那一段代码扫描了给定的目录中的全部 `.log` 后缀的日志文件?假使给定的需求是不但要扫描给定目录中的日志文件,还要递归地获取给定的目录的全部子目录、子子目录……内的日志文件,应当如何做(简要回答即可)?**[cite: 6] + > 答:ChangeDirectory 方法中Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly); + > SearchOption.TopDirectoryOnly 修改为 SearchOption.AllDirectories + > + > + +#### (Q2.3) AI 使用情况调查 + +*本次作业中,你是否使用了 AI?根据你的使用情况,在以下 (Q2.3.a) (Q2.3.b) 两个问题中选择一题作答即可:* + +* **(Q2.3.b) 如果使用了 AI,你给予 AI 的提示词是什么?你对 AI 的使用是询问 AI 一些接口的用法或是在某处的写法,还是让 AI 帮你写一部分作业代码,又或是让 AI 给你讲解代码框架?AI 的解答是否出现过错误(如果有,是哪些)?你认为本节的难度是偏低、适中,还是偏高?** + > 答:我给予ai提示词更多是搜索性质与提示讲解性质,并没有让ai直接给出过代码。因为本节难度我认为对于之前从未接触过c#的人来说难度比较高,c#的一堆内置函数单凭自己很难看懂,更何况有时候会一下子牵扯到多个代码文件,没有ai的辅助梳理很容易忘记要干什么。 + > + > + > \ No newline at end of file diff --git a/src/LocalCli/LocalCli.csproj b/src/LocalCli/LocalCli.csproj index 0ad60d9..ea7e02f 100644 --- a/src/LocalCli/LocalCli.csproj +++ b/src/LocalCli/LocalCli.csproj @@ -18,4 +18,13 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..03cec08 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -1,4 +1,5 @@ -using LogAnalyzer; +using System.Net; +using LogAnalyzer; using LogParser.Visitors; namespace LocalCli @@ -112,22 +113,149 @@ 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; + } + int index=1; + foreach (var file in files) + { + Console.WriteLine($"{index}.{file}"); + index++; + } } private static void AnalyzeFiles(LogFileAnalyzer analyzer) - { - throw new NotImplementedException("T2.3"); + { while(true){ + Console.WriteLine("Please input log files to analyze (separated by comma):"); + var str=Console.ReadLine(); + if (str is null) + { + return; + } + var selectedFiles = str.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(f => f.Trim()).ToArray(); + if (selectedFiles.Length == 0) + { + Console.WriteLine("No files specified, please try again."); + continue; + } + int degree = 0; + while (true) + { + Console.WriteLine("Please input degree of parallelism:"); + var degreeStr = Console.ReadLine(); + if (degreeStr is null) return; + if (!int.TryParse(degreeStr, out degree)) + { + Console.WriteLine("Invalid number, please try again."); + continue; + } + break; + } + try + { + analyzer.AnalyzeFiles(degree,selectedFiles); + break; + } + catch (InvalidOperationException) + { + Console.WriteLine("Analysis is already running, please wait."); + break; + } + catch(ArgumentException) + { + Console.WriteLine("Invalid files, please try again."); + continue; + } + } + } + private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + while (true) + { + Console.WriteLine("Please input degree of parallelism:"); + var degreeStr = Console.ReadLine(); + if (degreeStr is null) + { + return; + } + if (!int.TryParse(degreeStr, out int degree)) + { + Console.WriteLine("Invalid number, please try again."); + continue; + } + try + { + analyzer.AnalyzeAll(degree); + break; + } + catch (InvalidOperationException) + { + Console.WriteLine("Analysis is already running, please wait."); + break; + } + catch (ArgumentException) + { + Console.WriteLine("Invalid degree of parallelism, please try again."); + continue; + } + } } private static void GetAnalysisResult(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + while (true) + { + Console.WriteLine("Please input log file name:"); + var fileName = Console.ReadLine(); + if (fileName is null) + { + return; + } + fileName = fileName.Trim(); + if (fileName.Length == 0) + { + Console.WriteLine("Invalid file name, please try again."); + continue; + } + if (!analyzer.TryGetAnalysisResult(fileName, out var result)) + { + Console.WriteLine("File not found, please try again."); + continue; + } + if (result is null) + { + Console.WriteLine("File not found, please try again."); + continue; + } + switch (result.State) + { + case AnalysisState.NotAnalyzed: + Console.WriteLine("Not analyzed."); + break; + case AnalysisState.Failed: + Console.WriteLine($"Failed: {result.ErrorMessage}"); + break; + case AnalysisState.Succeeded: + var visitor = new KeyValueVisitor(); + foreach (var entry in result.Entries) + { + var logDict = visitor.Dump(entry); + foreach (var kvp in logDict) + { + Console.WriteLine($"{kvp.Key}: {kvp.Value}"); + } + } + break; + + } + break; + } } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..7daf113 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -142,6 +142,7 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) * Set _isAnalyzing */ // TODO: T2.2 + _isAnalyzing = true; } try @@ -155,6 +156,10 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) * Remember to lock _syncRoot to prevent data race */ // TODO: T2.2 + lock (_syncRoot) + { + _isAnalyzing = false; + } } } @@ -169,7 +174,14 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis * 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($"Unknown file: {file.Name}"); + } + if (_analysisResults[file.Name].State == AnalysisState.NotAnalyzed) + { + logFilesToParse.Add(file); + } } } @@ -184,7 +196,11 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList 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]; for (int i = 0; i < degreeOfParallelism; i++) @@ -195,12 +211,19 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis * Create and start threads to run `WorkerMain` */ // TODO: T2.2 + workers[i] = new Thread(() => WorkerMain(workerId, queue)); + workers[i].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) @@ -213,19 +236,38 @@ private void WorkerMain(int workerId, WorkQueue queue) try { // Parse file - throw new NotImplementedException("TODO: T2.2"); + using var reader = new StreamReader(file.FullName); + var entries = parser.Parse(reader); + result = new AnalysisResult( + FileName: file.Name, + FullName: file.FullName, + State: AnalysisState.Succeeded, + Entries: entries.ToList(), + 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..d1f6112 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) + { + _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) + { + if (_isCompleted == true) + { + item=default; + return false; + } + Monitor.Wait(_items); + } + 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/LogAnalyzerRpc/LogAnalyzerRpc.csproj b/src/LogAnalyzerRpc/LogAnalyzerRpc.csproj index 0d13f38..2aa0d21 100644 --- a/src/LogAnalyzerRpc/LogAnalyzerRpc.csproj +++ b/src/LogAnalyzerRpc/LogAnalyzerRpc.csproj @@ -6,10 +6,14 @@ enable - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/src/LogParser/Models/LogEntries.cs b/src/LogParser/Models/LogEntries.cs index 69edbc0..e4e9bbc 100644 --- a/src/LogParser/Models/LogEntries.cs +++ b/src/LogParser/Models/LogEntries.cs @@ -54,7 +54,7 @@ public sealed record RequestLogEntry( { public override TResult Accept(ILogEntryVisitor visitor) { - throw new NotImplementedException("TODO: T1.2"); + return visitor.Visit(this); } } @@ -69,7 +69,7 @@ public sealed record InternalLogEntry( { public override TResult Accept(ILogEntryVisitor visitor) { - throw new NotImplementedException("TODO: T1.2"); + return visitor.Visit(this); } } diff --git a/src/LogParser/Parser/LineParser.cs b/src/LogParser/Parser/LineParser.cs index 0475f6b..c20338e 100644 --- a/src/LogParser/Parser/LineParser.cs +++ b/src/LogParser/Parser/LineParser.cs @@ -1,4 +1,5 @@ -using LogParser.Models; +using CsvHelper; +using LogParser.Models; using System.Text.Json; using System.Text.Json.Serialization; @@ -16,8 +17,8 @@ public static LogEntry ParseLine(LogRecord logRecord) return eventElement.GetString() switch { "call" => LineParser.CreateCall(logRecord), - "request" => throw new NotImplementedException("TODO: T1.2"), - "internal" => throw new NotImplementedException("TODO: T1.2"), + "request" => LineParser.CreateRequest(logRecord), + "internal" => LineParser.CreateInternal(logRecord), _ => throw new FormatException($"Unknown event type: {eventElement.GetString()} in log message: {logRecord.Message}") }; } @@ -50,12 +51,35 @@ private static LogEntry CreateCall(LogRecord logRecord) private static LogEntry CreateRequest(LogRecord logRecord) { - throw new NotImplementedException("TODO: T1.2"); + var requestMessage = JsonSerializer.Deserialize(logRecord.Message, options) + ?? throw new FormatException($"Failed to deserialize request message: {logRecord.Message}"); + return new RequestLogEntry( + LineNo: logRecord.LineNo, + Timestamp: DateTimeOffset.Parse(logRecord.Timestamp), + PodName: logRecord.PodName, + Severity: ParseSeverity(requestMessage.Severity), + RequestId: requestMessage.RequestId, + Method: requestMessage.Method, + Path:requestMessage.Path, + StatusCode:requestMessage.StatusCode + ); } private static LogEntry CreateInternal(LogRecord logRecord) { - throw new NotImplementedException("TODO: T1.2"); + var internalMessage = JsonSerializer.Deserialize(logRecord.Message, options) + ?? throw new FormatException($"Failed to deserialize internal message: {logRecord.Message}"); + var index=internalMessage.Exception.IndexOf(": "); + var exceptionMessage=internalMessage.Exception.Substring(index+2); + var exceptionName=internalMessage.Exception.Substring(0,index); + return new InternalLogEntry( + LineNo: logRecord.LineNo, + Timestamp: DateTimeOffset.Parse(logRecord.Timestamp), + PodName: logRecord.PodName, + Severity: ParseSeverity(internalMessage.Severity), + ExceptionMessage:exceptionMessage, + ExceptionName:exceptionName + ); } private static LogSeverity ParseSeverity(string severity) @@ -77,11 +101,16 @@ private record CallMessage( ); private record RequestMessage( - // TODO: T1.2 + [property: JsonRequired] string Severity, + [property: JsonRequired] string RequestId, + [property: JsonRequired] string Method, + [property: JsonRequired] string Path, + [property: JsonRequired] int StatusCode ); private record InternalMessage( - // TODO: T1.2 + [property: JsonRequired] string Severity, + [property: JsonRequired] string Exception ); } } diff --git a/src/LogParser/Visitors/KeyValueVisitor.cs b/src/LogParser/Visitors/KeyValueVisitor.cs index e5ceba2..c0ae12e 100644 --- a/src/LogParser/Visitors/KeyValueVisitor.cs +++ b/src/LogParser/Visitors/KeyValueVisitor.cs @@ -26,12 +26,31 @@ public Dictionary Visit(CallLogEntry entry) public Dictionary Visit(RequestLogEntry entry) { - throw new NotImplementedException("TODO: T1.3"); + return new Dictionary + { + ["LineNo"] = entry.LineNo.ToString(), + ["Timestamp"] = entry.Timestamp.ToString("O"), + ["PodName"] = entry.PodName, + ["Severity"] = entry.Severity.ToString(), + ["EventType"] = entry.EventType.ToString(), + ["RequestId"] = entry.RequestId, + ["Method"] = entry.Method, + ["Path"] = entry.Path, + ["StatusCode"] = entry.StatusCode.ToString(), + }; } public Dictionary Visit(InternalLogEntry entry) { - throw new NotImplementedException("TODO: T1.3"); + return new Dictionary{ + ["LineNo"] = entry.LineNo.ToString(), + ["Timestamp"] = entry.Timestamp.ToString("O"), + ["PodName"] = entry.PodName, + ["Severity"] = entry.Severity.ToString(), + ["EventType"] = entry.EventType.ToString(), + ["ExceptionName"] = entry.ExceptionName, + ["ExceptionMessage"] = entry.ExceptionMessage, + }; } } }