diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..78820d5 --- /dev/null +++ b/docs/01-basic/report.md @@ -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(...)`。 + - **防止字段缺失**:在消息接收模型的属性上添加了 `[JsonRequired]` 特性,如果 JSON 中缺失对应的必填字段,反序列化时将自动抛出异常。 + - **命名法转换(kebab-case 转换为 PascalCase)**:在属性上添加 `[JsonPropertyName("abc-def")]` 特性显式指定 JSON 键名,或在 `JsonSerializerOptions` 中设置 `PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower`。 + +## (Q1.2) + +调用 `KeyValueVisitor` 的 `Dump` 方法时,对于 `Call` 事件的方法调用链如下: + ++ `Dictionary KeyValueVisitor.Dump(LogEntry entry)` ++ `TResult LogEntry.Accept(ILogEntryVisitor visitor)`(实际运行时动态派发调用 `CallLogEntry.Accept`) ++ `Dictionary KeyValueVisitor.Visit(CallLogEntry entry)` + +## (Q1.3.b) + ++ **给予 AI 的提示词**:报错信息,请ai分析问题 ++ **AI 的优势**:AI 能快速排查并指出 `JsonRequired` 大小写拼写错误等语法细节 ++ **AI 的问题**:AI 偶尔会写出语法缺失的代码,会缺少部分语句,需要我自己检查。 \ No newline at end of file diff --git a/docs/02-multithreading/images/error_run.png b/docs/02-multithreading/images/error_run.png new file mode 100644 index 0000000..c3a15eb Binary files /dev/null and b/docs/02-multithreading/images/error_run.png differ diff --git a/docs/02-multithreading/images/normal_run.png b/docs/02-multithreading/images/normal_run.png new file mode 100644 index 0000000..c89a7ba Binary files /dev/null and b/docs/02-multithreading/images/normal_run.png differ diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..4789137 --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,56 @@ +# 实验报告 + +## 1. 功能演示 + +### 1.1 功能实现介绍 +启动 `LocalCli` 后,选择日志所在目录,选择需要解析的日志文件,点击“开始解析”按钮,系统将自动并发解析日志文件并显示结果。 + +![正常解析流程](images/normal_run.png) + +* **功能说明**:主线程将目录下的 `.log` 文件投入 `WorkQueue`,由 4 个 Worker 线程并发拉取并解析,结果安全写入共享字典。 + +### 1.2 鲁棒性展示 +当用户输入不存在的日志路径或文件名时,系统发现错误,给出提示,要求重新输入: + +![异常捕获展示](images/error_run.png) + +* **功能说明**:系统在 `LogFileAnalyzer` 中校验路径有效性,并在 Worker 线程遇到异常时将其状态标记为 `AnalysisState.Failed`,记录错误信息而不会导致主程序崩溃。 +## 2. 问题解答 + +### Q2.1 + +#### 1. +* **共享变量**: + * `Queue _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产生的错误在于不能理解多个代码文件的统一性,它针对报错给出的修改与和其余文件中的代码不匹配,需要我自己查找修改。适中。 \ No newline at end of file diff --git a/docs/03-async-grpc/images/result.png b/docs/03-async-grpc/images/result.png new file mode 100644 index 0000000..f61149b Binary files /dev/null and b/docs/03-async-grpc/images/result.png differ diff --git a/docs/03-async-grpc/images/robustness.png b/docs/03-async-grpc/images/robustness.png new file mode 100644 index 0000000..0635c97 Binary files /dev/null and b/docs/03-async-grpc/images/robustness.png differ diff --git a/docs/03-async-grpc/report.md b/docs/03-async-grpc/report.md new file mode 100644 index 0000000..edb142a --- /dev/null +++ b/docs/03-async-grpc/report.md @@ -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同时会讲解一下理论知识。 diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..a557cc1 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -1,5 +1,6 @@ using LogAnalyzer; using LogParser.Visitors; +using System; namespace LocalCli { @@ -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; + } } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..e33719b 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -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 { @@ -138,10 +142,8 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) } fileList = fileNameList.Select(fileName => _logFiles[fileName]).ToList(); - /* - * Set _isAnalyzing - */ - // TODO: T2.2 + //设置正在分析状态为true + _isAnalyzing = true; } try @@ -150,11 +152,11 @@ 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 +167,15 @@ 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.ContainsKey(file.Name)) + { + throw new InvalidOperationException($"File '{file.Name}' is unknown."); + } + + if (_analysisResults[file.Name].State != AnalysisState.Succeeded) + { + logFilesToParse.Add(file); + } } } @@ -180,10 +186,11 @@ 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]; @@ -191,16 +198,17 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis { 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 +220,34 @@ 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.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; + } } } } diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..994c554 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace LogAnalyzer { @@ -20,17 +21,45 @@ 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 work 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) + { + Monitor.Wait(_items); + } + + if (_items.Count > 0) + { + item = _items.Dequeue()!; + return true; + } + + item = default; + return false; + } } public void CompleteAdding() { - throw new NotImplementedException("TODO: T2.1"); + lock(_items) + { + if (_isCompleted) return; + _isCompleted = true; + Monitor.PulseAll(_items); + } } } } diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..92c5991 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -1,9 +1,13 @@ using Google.Protobuf.WellKnownTypes; using Grpc.Core; using LogAnalyzer; -using LogAnalyzerRpc.Protos; using LogAnalyzerRpc; +using LogAnalyzerRpc.Protos; using LogParser.Visitors; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; namespace LogAnalyzerAgent.Applications { @@ -79,22 +83,219 @@ 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 + { + if (string.IsNullOrWhiteSpace(request.DirectoryPath)) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "Directory path cannot be empty." + }; + return Task.FromResult(response); + } + + if (!_analyzer.ChangeDirectory(request.DirectoryPath)) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.DirectoryNotFound, + Message = $"Directory '{request.DirectoryPath}' does not exist." + }; + return Task.FromResult(response); + } + + response.CurrentDirectory = _analyzer.CurrentDirectory ?? request.DirectoryPath; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + response.Status = CreateNoErrorOperationStatus(); + } + catch (ArgumentException ex) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = ex.Message + }; + } + 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 + { + if (!_analyzer.HasDirectory) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Directory not set." + }; + return Task.FromResult(response); + } + + if (_analyzer.IsAnalyzing) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Agent is currently analyzing logs." + }; + return Task.FromResult(response); + } + + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing all files."); + } + return Task.FromResult(response); } public Task AnalyzeFiles(AnalyzeFilesRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeFilesResponse(); + try + { + if (!_analyzer.HasDirectory) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Directory not set." + }; + return Task.FromResult(response); + } + + if (_analyzer.IsAnalyzing) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = "Agent is currently analyzing logs." + }; + return Task.FromResult(response); + } + + if (request.FileNames == null || request.FileNames.Count == 0) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "No files specified for analysis." + }; + return Task.FromResult(response); + } + + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames); + response.Status = CreateNoErrorOperationStatus(); + } + catch (FileNotFoundException ex) + { + response.Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = ex.Message + }; + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing specified files."); + } + return Task.FromResult(response); } public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var resultsList = new List(); + try + { + if (string.IsNullOrWhiteSpace(request.FileName)) + { + resultsList.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = "File name cannot be empty." + } + }); + return resultsList; + } + + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result) || result is null) + { + resultsList.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File '{request.FileName}' was not found or has no analysis result." + } + }); + return resultsList; + } + + var headerMessage = new AnalysisResultHeaderMessage + { + FileName = request.FileName, + FullName = result.FullName ?? "", + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? "", + WorkerId = result.WorkerId + }; + + resultsList.Add(new GetAnalysisResultResponse + { + Header = headerMessage, + Status = CreateNoErrorOperationStatus() + }); + + if (result.State == AnalysisState.Succeeded && result.Entries != null) + { + foreach (var entry in result.Entries) + { + resultsList.Add(new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus() + }); + } + } + } + catch (Exception ex) + { + resultsList.Clear(); + resultsList.Add(new GetAnalysisResultResponse + { + Status = CreateInternalErrorOperationStatus(ex) + }); + _logger.LogError(ex, "An error occurred while getting analysis result."); + } + + return resultsList; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..3c929f9 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 responses = _session.GetAnalysisResult(request, context.CancellationToken); + foreach (var response in responses) // 替换掉原来的 await foreach + { + await responseStream.WriteAsync(response, context.CancellationToken); + } } } } diff --git a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs index eb69232..4217cc7 100644 --- a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs +++ b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs @@ -1,6 +1,7 @@ using Google.Protobuf.WellKnownTypes; using LogAnalyzerRpc.Protos; using LogParser.Models; +using System.IO; namespace LogAnalyzerRpc { @@ -30,12 +31,38 @@ public LogEntryMessage Visit(CallLogEntry entry) public LogEntryMessage Visit(RequestLogEntry entry) { - throw new NotImplementedException("TODO: T3.1"); + return new LogEntryMessage() + { + RequestLogEntry = new RequestLogEntryMessage + { + LineNo = entry.LineNo, + Timestamp = Timestamp.FromDateTimeOffset(entry.Timestamp), + PodName = entry.PodName, + Severity = GrpcTypeConverter.ConvertToGrpc(entry.Severity), + EventType = GrpcTypeConverter.ConvertToGrpc(entry.EventType), + RequestId = entry.RequestId, + Method = entry.Method, + Path = entry.Path, + StatusCode = entry.StatusCode, + } + }; } public LogEntryMessage Visit(InternalLogEntry entry) { - throw new NotImplementedException("TODO: T3.1"); + return new LogEntryMessage() + { + InternalLogEntry = new InternalLogEntryMessage + { + LineNo = entry.LineNo, + Timestamp = Timestamp.FromDateTimeOffset(entry.Timestamp), + PodName = entry.PodName, + Severity = GrpcTypeConverter.ConvertToGrpc(entry.Severity), + EventType = GrpcTypeConverter.ConvertToGrpc(entry.EventType), + ExceptionName = entry.ExceptionName, + ExceptionMessage = entry.ExceptionMessage, + } + }; } } } diff --git a/src/LogAnalyzerRpc/GrpcTypeConverter.cs b/src/LogAnalyzerRpc/GrpcTypeConverter.cs index 029122e..2dd67e8 100644 --- a/src/LogAnalyzerRpc/GrpcTypeConverter.cs +++ b/src/LogAnalyzerRpc/GrpcTypeConverter.cs @@ -2,6 +2,7 @@ using LogAnalyzer; using LogAnalyzerRpc.Protos; using LogParser.Models; +using System; namespace LogAnalyzerRpc { @@ -20,12 +21,24 @@ public static AnalysisStateEnum ConvertToGrpc(AnalysisState state) public static LogSeverityEnum ConvertToGrpc(LogSeverity severity) { - throw new NotImplementedException("TODO: T3.1"); + return severity switch + { + LogSeverity.Info => LogSeverityEnum.Info, + LogSeverity.Warning => LogSeverityEnum.Warning, + LogSeverity.Error => LogSeverityEnum.Error, + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) + }; } public static LogEventTypeEnum ConvertToGrpc(LogEventType eventType) { - throw new NotImplementedException("TODO: T3.1"); + return eventType switch + { + LogEventType.Call => LogEventTypeEnum.Call, + LogEventType.Request => LogEventTypeEnum.Request, + LogEventType.Internal => LogEventTypeEnum.Internal, + _ => throw new ArgumentOutOfRangeException(nameof(eventType), eventType, null) + }; } public static LogEntryMessage ConvertToGrpc(LogEntry entry) @@ -46,12 +59,24 @@ public static AnalysisState ConvertFromGrpc(AnalysisStateEnum state) public static LogSeverity ConvertFromGrpc(LogSeverityEnum severity) { - throw new NotImplementedException("TODO: T3.1"); + return severity switch + { + LogSeverityEnum.Info => LogSeverity.Info, + LogSeverityEnum.Warning => LogSeverity.Warning, + LogSeverityEnum.Error => LogSeverity.Error, + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) + }; } public static LogEventType ConvertFromGrpc(LogEventTypeEnum eventType) { - throw new NotImplementedException("TODO: T3.1"); + return eventType switch + { + LogEventTypeEnum.Call => LogEventType.Call, + LogEventTypeEnum.Request => LogEventType.Request, + LogEventTypeEnum.Internal => LogEventType.Internal, + _ => throw new ArgumentOutOfRangeException(nameof(eventType), eventType, null) + }; } public static LogEntry ConvertFromGrpc(LogEntryMessage entryMessage) @@ -67,8 +92,24 @@ public static LogEntry ConvertFromGrpc(LogEntryMessage entryMessage) TargetService: entryMessage.CallLogEntry.TargetService, DurationMs: entryMessage.CallLogEntry.DurationMs ), - LogEntryMessage.EntryOneofCase.RequestLogEntry => throw new NotImplementedException("TODO: T3.1"), - LogEntryMessage.EntryOneofCase.InternalLogEntry => throw new NotImplementedException("TODO: T3.1"), + LogEntryMessage.EntryOneofCase.RequestLogEntry => new RequestLogEntry( + LineNo: entryMessage.RequestLogEntry.LineNo, + Timestamp: entryMessage.RequestLogEntry.Timestamp.ToDateTimeOffset(), + PodName: entryMessage.RequestLogEntry.PodName, + Severity: ConvertFromGrpc(entryMessage.RequestLogEntry.Severity), + RequestId: entryMessage.RequestLogEntry.RequestId, + Method: entryMessage.RequestLogEntry.Method, + Path: entryMessage.RequestLogEntry.Path, + StatusCode: entryMessage.RequestLogEntry.StatusCode + ), + LogEntryMessage.EntryOneofCase.InternalLogEntry => new InternalLogEntry( + LineNo: entryMessage.InternalLogEntry.LineNo, + Timestamp: entryMessage.InternalLogEntry.Timestamp.ToDateTimeOffset(), + PodName: entryMessage.InternalLogEntry.PodName, + Severity: ConvertFromGrpc(entryMessage.InternalLogEntry.Severity), + ExceptionName: entryMessage.InternalLogEntry.ExceptionName, + ExceptionMessage: entryMessage.InternalLogEntry.ExceptionMessage + ), _ => throw new ArgumentException($"Unknown entry type: {entryMessage.EntryCase}", nameof(entryMessage)) }; } 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..f067b26 100644 --- a/src/LogParser/Parser/LineParser.cs +++ b/src/LogParser/Parser/LineParser.cs @@ -1,4 +1,5 @@ using LogParser.Models; +using System; 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,43 @@ 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}"); + + int colonIndex = internalMessage.Exception.IndexOf(": "); + if (colonIndex == -1) + { + throw new FormatException($"Invalid exception format: {internalMessage.Exception}"); + } + + string exceptionName = internalMessage.Exception.Substring(0, colonIndex); + string exceptionMessage = internalMessage.Exception.Substring(colonIndex + 2); + + return new InternalLogEntry( + LineNo: logRecord.LineNo, + Timestamp: DateTimeOffset.Parse(logRecord.Timestamp), + PodName: logRecord.PodName, + Severity: ParseSeverity(internalMessage.Severity), + ExceptionName: exceptionName, + ExceptionMessage: exceptionMessage + ); } private static LogSeverity ParseSeverity(string severity) @@ -77,11 +109,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..3a23fdb 100644 --- a/src/LogParser/Visitors/KeyValueVisitor.cs +++ b/src/LogParser/Visitors/KeyValueVisitor.cs @@ -1,4 +1,5 @@ using LogParser.Models; +using System.Collections.Generic; namespace LogParser.Visitors { @@ -26,12 +27,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, + }; } } } diff --git a/src/RemoteCli/Program.cs b/src/RemoteCli/Program.cs index de0ac99..e54586d 100644 --- a/src/RemoteCli/Program.cs +++ b/src/RemoteCli/Program.cs @@ -5,6 +5,8 @@ using LogAnalyzerRpc.Protos; using LogParser.Visitors; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; namespace RemoteCli { @@ -116,32 +118,151 @@ 6. Exit. private static async Task ShowLogFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var response = await client.GetLogFilesAsync(new Empty()); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + return; + } + + if (response.FileNames.Count == 0) + { + Console.WriteLine("No log files found in the current directory."); + return; + } + + Console.WriteLine("Log files in directory:"); + foreach (var file in response.FileNames) + { + Console.WriteLine($"- {file}"); + } } private static int ReadDegreeOfParallelism() { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input max degree of parallelism (0 for default):"); + var input = Console.ReadLine(); + if (int.TryParse(input, out int result) && result >= 0) + { + return result; + } + return 0; } private static List ReadFileNames() { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input log file names separated by comma:"); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + return new List(); + } + + return input.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(f => f.Trim()) + .Where(f => !string.IsNullOrEmpty(f)) + .ToList(); } private static async Task AnalyzeFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var fileNames = ReadFileNames(); + if (fileNames.Count == 0) + { + Console.WriteLine("Input cannot be empty."); + return; + } + + var parallelism = ReadDegreeOfParallelism(); + + Console.WriteLine("Analyzing specified files..."); + var request = new AnalyzeFilesRequest + { + DegreeOfParallelism = parallelism + }; + request.FileNames.AddRange(fileNames); + + var response = await client.AnalyzeFilesAsync(request); + if (!response.Status.Success) + { + Console.WriteLine($"Error analyzing files: {response.Status.Code}: {response.Status.Message}"); + } + else + { + Console.WriteLine("Analysis completed."); + } } private static async Task AnalyzeAll(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var parallelism = ReadDegreeOfParallelism(); + + Console.WriteLine("Analyzing all log files..."); + var request = new AnalyzeAllRequest + { + DegreeOfParallelism = parallelism + }; + + var response = await client.AnalyzeAllAsync(request); + if (!response.Status.Success) + { + Console.WriteLine($"Error analyzing files: {response.Status.Code}: {response.Status.Message}"); + } + else + { + Console.WriteLine("Analysis completed."); + } } private static async Task GetAnalysisResult(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input log file name:"); + var fileName = Console.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(fileName)) + { + Console.WriteLine("Invalid file name."); + return; + } + + var request = new GetAnalysisResultRequest + { + FileName = fileName + }; + + using var call = client.GetAnalysisResult(request); + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + return; + } + + switch (response.PayloadCase) + { + case GetAnalysisResultResponse.PayloadOneofCase.Header: + var header = response.Header; + switch (header.State) + { + case AnalysisStateEnum.NotAnalyzed: + Console.WriteLine($"File '{fileName}' has not been analyzed yet."); + break; + case AnalysisStateEnum.Failed: + Console.WriteLine($"Analysis failed for '{fileName}':"); + Console.WriteLine(header.ErrorMessage); + break; + case AnalysisStateEnum.Succeeded: + Console.WriteLine($"Analysis result for '{fileName}':"); + break; + } + break; + + case GetAnalysisResultResponse.PayloadOneofCase.LogEntry: + var entry = GrpcTypeConverter.ConvertFromGrpc(response.LogEntry); + Console.WriteLine(entry); + break; + } + } } } }