diff --git a/.gitignore b/.gitignore index d990a44..276cc3a 100644 --- a/.gitignore +++ b/.gitignore @@ -405,3 +405,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.obsidian/ 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/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/LogParser/Models/LogEntries.cs b/src/LogParser/Models/LogEntries.cs index 69edbc0..eec6f0c 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,8 +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..64d8ec6 100644 --- a/src/LogParser/Parser/LineParser.cs +++ b/src/LogParser/Parser/LineParser.cs @@ -16,8 +16,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 +50,37 @@ 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 exceptionspilt = internalMessage.Exception.IndexOf(": "); + if (exceptionspilt == -1) + { + throw new FormatException($"Invalid exception format: {internalMessage.Exception}"); + } + return new InternalLogEntry( + LineNo: logRecord.LineNo, + Timestamp: DateTimeOffset.Parse(logRecord.Timestamp), + PodName: logRecord.PodName, + Severity: ParseSeverity(internalMessage.Severity), + ExceptionName: internalMessage.Exception.Substring(0, exceptionspilt), + ExceptionMessage: internalMessage.Exception.Substring(exceptionspilt + 2) + ); } private static LogSeverity ParseSeverity(string severity) @@ -77,11 +102,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 ); } -} +} \ No newline at end of file diff --git a/src/LogParser/Visitors/KeyValueVisitor.cs b/src/LogParser/Visitors/KeyValueVisitor.cs index e5ceba2..f70bcc2 100644 --- a/src/LogParser/Visitors/KeyValueVisitor.cs +++ b/src/LogParser/Visitors/KeyValueVisitor.cs @@ -26,12 +26,32 @@ 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, + }; } } }