diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..ea2a8de --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,28 @@ + # 01-basic 问答题报告 + + ## Q1.1 + + **(1)** CsvReader 自动按逗号拆解 CSV,LogRecordMap 里的 Index(0)-Index(3) + 指定了第 0 列到第 3 列分别对应 LineNo、Timestamp、PodName、Message。 + + **(2)** 在 LineParser.ParseLine 方法中,先用 JsonDocument.Parse 解析 JSON, + 然后通过 root.TryGetProperty("event", ...) 取出 event 字段, + 再用 switch 表达式匹配 "call"、"request"、"internal" 来判断日志种类。 + + **(3)** 使用 JsonSerializer.Deserialize() 解析 JSON。 + 通过 [JsonRequired] 特性防止字段缺失,缺字段时自动抛 JsonException。 + 通过 JsonNamingPolicy.KebabCaseLower 将烤串命名自动转换为大驼峰命名。 + ## Q1.2 + 第一步: KeyValueVisitor.Dump(LogEntry entry) 被调用,里面只有一行: + + return entry.Accept(this); // this 是 KeyValueVisitor 自己 + + 第二步: entry 实际是 CallLogEntry,所以走 CallLogEntry.Accept: + + return visitor.Visit(this); // this 是 CallLogEntry,visitor 是 + KeyValueVisitor + + 第三步: this 是 CallLogEntry,C# 自动匹配到 + KeyValueVisitor.Visit(CallLogEntry entry),在里面构建字典并返回。 + ## Q1.3 + 使用了GLM5.2帮助我理解JSON与C#的转换。 \ No newline at end of file diff --git a/docs/02-multithreading/assets/ScreenShot_2026-08-21_154541_552.png b/docs/02-multithreading/assets/ScreenShot_2026-08-21_154541_552.png new file mode 100644 index 0000000..b981007 Binary files /dev/null and b/docs/02-multithreading/assets/ScreenShot_2026-08-21_154541_552.png differ diff --git a/docs/02-multithreading/assets/ScreenShot_2026-08-21_154638_202.png b/docs/02-multithreading/assets/ScreenShot_2026-08-21_154638_202.png new file mode 100644 index 0000000..3902a97 Binary files /dev/null and b/docs/02-multithreading/assets/ScreenShot_2026-08-21_154638_202.png differ diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..e7d973c --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,37 @@ +# 多线程实验报告 + +## 1. 功能介绍 +本项目实现了一个多线程日志解析器和简易交互界面: +- **InputDirectory**: 输入目录路径,加载日志文件。 +- **ShowLogFiles**: 查看当前目录下所有 .log 文件。 +- **AnalyzeFiles**: 使用多线程并行分析指定的日志文件。 +- **AnalyzeAll**: 一键分析目录下的所有文件。 +- **GetAnalysisResult**: 查看特定文件的分析结果,支持状态分类显示。 +- **异常处理**: 程序实现了完善的 try-catch 机制,即使输入非法路径或文件名,程序依然稳定运行。 + +## 2. 运行截图 + +![功能截图](assets/ScreenShot_2026-08-21_154541_552.png) +![鲁棒性测试](assets/ScreenShot_2026-08-21_154638_202.png) + +## 3. 问答题 + +### Q2.1 临界区理解 +**① 共享变量及保护:** +- WorkQueue: _items (Queue) 和 _isCompleted (bool),通过 `lock(_items)` 保护。 +- LogFileAnalyzer: _currentDirectory, _isAnalyzing, _logFiles, _analysisResults,通过 `lock(_syncRoot)` 保护。 + +**② 为什么用 while 而不是 if:** +因为“虚假唤醒”和“多线程竞争”。被唤醒后,可能货物已经被其他线程抢走。使用 while 循环可以确保线程在醒来后再次检查仓库是否真的有货,从而避免从空仓库取货引发崩溃。 + +### Q2.2 代码框架 +**① 如何扫描文件:** +在 `ChangeDirectory` 方法中使用 `Directory.EnumerateFiles` 扫描。 + +**② 如何递归扫描:** +将 `SearchOption.TopDirectoryOnly` 修改为 `SearchOption.AllDirectories` 即可。 + +### Q2.3 AI 使用情况 +我使用了 AI 辅助编程。AI 主要帮助我理解了多线程中 `Monitor.Wait` 和 `Pulse` 的协同工作逻辑,并协助我修复了 NuGet 包还原和 C# 语法兼容性问题。 + +--- \ No newline at end of file diff --git a/docs/03-async-grpc/report.md b/docs/03-async-grpc/report.md new file mode 100644 index 0000000..20c342c --- /dev/null +++ b/docs/03-async-grpc/report.md @@ -0,0 +1,283 @@ +# 异步与 gRPC 实验报告 + +## 1. 功能介绍 + +本项目实现了一个基于 gRPC 的远程日志分析系统,包含: +- **LogAnalyzerAgent**:常驻运行的服务端,对外提供 gRPC 服务(Ping、GetAgentStatus、ChangeDirectory、GetLogFiles、AnalyzeAll、AnalyzeFiles、GetAnalysisResult 流式返回) +- **RemoteCli**:远程控制台客户端,全部使用 `async/await` 异步调用 gRPC 接口 +- **GrpcTypeConverter / GrpcLogEntryVisitor**:内部 C# 类型与 Protobuf 消息类型的双向转换,访问者模式 + 单例模式 + +## 2. 运行截图 + +### 完整功能 + + +Dell@DESKTOP-0GJFVEM MINGW64 ~ +$ cd /d/dotnet-workshop && dotnet run --project src/RemoteCli +Connecting to agent at http://localhost:5000... + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 5 +Please input directory containing log files: +D:\dotnet-workshop\src\dataset + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 1 +Log files: + - basic-fail.log + - basic-multiple.log + - basic.log + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 1 +Log files: + - basic-fail.log + - basic-multiple.log + - basic.log + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 1 +Log files: + - basic-fail.log + - basic-multiple.log + - basic.log + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 1 +Log files: + - basic-fail.log + - basic-multiple.log + - basic.log + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> D:\dotnet-workshop\src\dataset +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 1 +Log files: + - basic-fail.log + - basic-multiple.log + - basic.log + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 2 +Please input degree of parallelism (0 for processor count): +4 +Please input file names separated by comma (e.g. log1.log,log2.log): +basic.log +Analysis finished. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 4 +Please input file name: +basic.log +File: basic.log + State: Succeeded + LineNo: 0 + Timestamp: 2026-06-05T16:00:29.0450000+00:00 + PodName: userservice-0 + Severity: Info + EventType: Call + RequestId: 3a013a08-6853-49fc-8f06-50daeb5c1e51 + TargetService: authservice + DurationMs: 18 + + LineNo: 1 + Timestamp: 2026-06-05T16:00:31.0860000+00:00 + PodName: userservice-1 + Severity: Info + EventType: Request + RequestId: 1177c344-115e-4f85-b8ec-c9164d132b79 + Method: GET + Path: /api/user/john + StatusCode: 404 + + LineNo: 2 + Timestamp: 2026-06-05T16:05:45.3220000+00:00 + PodName: gateway-0 + Severity: Error + EventType: Internal + ExceptionName: System.InvalidOperationException + ExceptionMessage: Failed to load gateway routing configuration. + + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 3 +Please input degree of parallelism (0 for processor count): + 4 +All files analysis finished. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 4 +Please input file name: +basic-fail.log +File: basic-fail.log + State: Failed + Analysis failed: JSON deserialization for type 'LogParser.Parser.LineParser+RequestMessage' was missing required properties including: 'method'. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> + + +### 鲁棒性测试 + +>>> abc +Invalid input, please try again. + +Please choose: +1. Show log files. +2. Analyze specified log files. +3. Analyze all log files. +4. Get log file analysis result. +5. Change directory. +6. Exit. +>>> 5 +Please input directory containing log files: +C:\nope +Error: DirectoryNotFound: Directory not found: C:\nope, please try again: +Please input directory containing log files: + + + +## 3. 问答题 + +### Q3.1 网络应用开发与非网络应用的区别 + +网络应用开发需要注意用户端的请求,可以关注到,和以往的并行开发应用不同,这一部分需要使用到异步开发来解放CPU的性能,充分发挥所有功能的 + +### Q3.2 AI 使用情况 + + diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..bbeb099 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -112,22 +112,83 @@ 6. Exit. private static void ShowLogFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + var files = analyzer.GetLogFiles(); + Console.WriteLine("Log files:"); + foreach (var file in files) + { + Console.WriteLine($" - {file}"); + } } private static void AnalyzeFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Please input file names separated by comma (e.g. log1.log,log2.log):"); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) return; + + var fileNames = input.Split(',').Select(s => s.Trim()); + try + { + // 调用分析方法 + analyzer.AnalyzeFiles(0, fileNames); + Console.WriteLine("Analysis finished."); + } + catch (Exception ex) + { + // 鲁棒性:捕获异常并提示用户,而不是让程序崩溃 + Console.WriteLine($"Analysis failed: {ex.Message}"); + } } private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + try + { + analyzer.AnalyzeAll(0); + Console.WriteLine("All files analysis finished."); + } + catch (Exception ex) + { + Console.WriteLine($"Analysis failed: {ex.Message}"); + } } private static void GetAnalysisResult(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Please input file name:"); + var fileName = Console.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(fileName)) return; + + if (analyzer.TryGetAnalysisResult(fileName, out var result)) + { + // 根据状态分流处理 + if (result!.State == AnalysisState.NotAnalyzed) + { + Console.WriteLine("File has not been analyzed."); + } + else if (result.State == AnalysisState.Failed) + { + Console.WriteLine($"Analysis failed: {result.ErrorMessage}"); + } + else + { + // 使用 Visitor 输出 + var visitor = new KeyValueVisitor(); + foreach (var entry in result.Entries) + { + var dict = visitor.Dump(entry); + foreach (var kv in dict) + { + Console.WriteLine($" {kv.Key}: {kv.Value}"); + } + Console.WriteLine(); // 每条日志间空一行 + } + } + } + else + { + Console.WriteLine("File not found."); + } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..fe7ecca 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -1,13 +1,13 @@ using LogParser.Models; using LogParser.Parser; using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography.X509Certificates; +using System.Threading; namespace LogAnalyzer { public class LogFileAnalyzer { - private readonly object _syncRoot = new(); + private readonly object _syncRoot = new(); // 互斥量 private string? _currentDirectory = null; private bool _isAnalyzing = false; private readonly Dictionary _logFiles = new(); @@ -19,13 +19,11 @@ public bool IsAnalyzing { get { - lock (_syncRoot) - { - return _isAnalyzing; - } + lock (_syncRoot) return _isAnalyzing; } } + // ... (省略构造函数和 ChangeDirectory,保持你原有的不变) ... public LogFileAnalyzer(string? directoryPath = null) { var cdResult = ChangeDirectory(directoryPath); @@ -107,7 +105,6 @@ public void AnalyzeAll(int degreeOfParallelism) } AnalyzeFiles(degreeOfParallelism, fileNames); } - public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) { if (degreeOfParallelism < 0) @@ -124,24 +121,17 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) List fileList; lock (_syncRoot) { - if (_isAnalyzing) - { - throw new InvalidOperationException("Analysis is already in progress."); - } + if (_isAnalyzing) throw new InvalidOperationException("Analysis already in progress."); foreach (var fileName in fileNameList) { if (!_logFiles.ContainsKey(fileName)) - { - throw new ArgumentException($"File '{fileName}' is not in the current directory or does not exist."); - } + throw new ArgumentException($"File '{fileName}' does not exist."); } fileList = fileNameList.Select(fileName => _logFiles[fileName]).ToList(); - /* - * Set _isAnalyzing - */ - // TODO: T2.2 + // 【T2.2: 设置正在分析标记】 + _isAnalyzing = true; } try @@ -150,11 +140,11 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) } finally { - /* - * Unset _isAnalyzing - * Remember to lock _syncRoot to prevent data race - */ - // TODO: T2.2 + // 【T2.2: 无论成功失败,最后都要解除标记】 + lock (_syncRoot) + { + _isAnalyzing = false; + } } } @@ -165,42 +155,51 @@ 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"); + // 【T2.2: 过滤未解析或需重解析的文件】 + if (!_analysisResults.TryGetValue(file.Name, out var currentResult)) + { + throw new InvalidOperationException($"File {file.Name} is unknown."); + } + + // 只有未分析或失败的文件需要重新解析(已成功的跳过) + if (currentResult.State != AnalysisState.Succeeded) + { + logFilesToParse.Add(file); + } } } - if (logFilesToParse.Count == 0) - { - return; - } + if (logFilesToParse.Count == 0) return; + // 创建之前完成的阻塞队列 var queue = new WorkQueue(); - /* - * Enqueue log files - */ - // TODO: T2.2 + // 【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 + int workerId = i; // 闭包变量,必须在循环内声明 + // 【T2.2: 创建并启动工人线程】 + workers[i] = new Thread(() => WorkerMain(workerId, queue)) + { + Name = $"log-analyzer-worker-{workerId}" + }; + workers[i].Start(); } - /* - * Wait for (join) all threads to end - */ - // TODO: T2.2 + // 【T2.2: 等待所有工人收工】 + foreach (var worker in workers) + { + worker.Join(); + } } private void WorkerMain(int workerId, WorkQueue queue) @@ -212,21 +211,43 @@ private void WorkerMain(int workerId, WorkQueue queue) AnalysisResult result; try { - // Parse file - throw new NotImplementedException("TODO: T2.2"); + // 【T2.2: 调用解析器解析文件】 + // 注意:Parse 是惰性求值,真正的读文件/解析发生在 ToArray() 时, + // 所以必须把 ToArray() 放在锁外,否则解析会被锁串行化、失去并行性 + using var reader = new StreamReader(file.FullName); + var entries = parser.Parse(reader).ToArray(); + + lock (_syncRoot) + { + result = _analysisResults[file.Name] with + { + State = AnalysisState.Succeeded, + Entries = entries, + WorkerId = workerId, + ErrorMessage = null + }; + } } catch (Exception ex) { - // Save exception message to result - throw new NotImplementedException("TODO: T2.2"); + // 【T2.2: 异常处理】 + lock (_syncRoot) + { + result = _analysisResults[file.Name] with + { + State = AnalysisState.Failed, + ErrorMessage = ex.Message, + WorkerId = workerId + }; + } } - /* - * Save parse result. - * [!Important] Remember to lock _syncRoot to prevent data race. - */ - throw new NotImplementedException("TODO: T2.2"); + // 【T2.2: 保存结果,必须加锁防止数据竞争】 + lock (_syncRoot) + { + _analysisResults[file.Name] = result; + } } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..5cad8b1 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -20,17 +20,57 @@ public bool IsCompleted public void Enqueue(T item) { - throw new NotImplementedException("TODO: T2.1"); + // 1. 加锁,确保对 _items 和 _isCompleted 的操作是互斥的 + lock (_items) + { + // 如果已经标记为结束,则不再允许放入(可选,根据健壮性要求添加) + if (_isCompleted) return; + + // 2. 放入商品 + _items.Enqueue(item); + + // 3. 唤醒一个正在等待的消费者(如有) + Monitor.Pulse(_items); + } } public bool TryDequeue([NotNullWhen(true)] out T? item) { - throw new NotImplementedException("TODO: T2.1"); + // 1. 加锁 + lock (_items) + { + // 2. 核心:如果仓库为空且还没下班,就原地等待 + // 使用 while 防止“虚假唤醒”或“被截胡” + while (_items.Count == 0 && !_isCompleted) + { + // 释放锁并进入等待状态,直到被 Pulse 或 PulseAll 唤醒 + Monitor.Wait(_items); + } + + // 3. 检查是否有商品可以取出 + if (_items.Count > 0) + { + item = _items.Dequeue()!; + return true; + } + + // 4. 执行到这里说明:仓库为空 且 _isCompleted 为 true + item = default; + return false; + } } public void CompleteAdding() { - throw new NotImplementedException("TODO: T2.1"); + // 1. 加锁 + lock (_items) + { + // 2. 标记生产结束 + _isCompleted = true; + + // 3. 核心:广播给所有正在等待的消费者,告诉他们下班了,不用再等了 + Monitor.PulseAll(_items); + } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..707fbe3 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -1,4 +1,4 @@ -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; using LogAnalyzer; using LogAnalyzerRpc.Protos; @@ -38,6 +38,16 @@ private static OperationStatusMessage CreateNoErrorOperationStatus() }; } + private static OperationStatusMessage CreateErrorOperationStatus(AgentErrorCode code, string message) + { + return new OperationStatusMessage() + { + Success = false, + Code = code, + Message = message, + }; + } + public Task Ping(Empty empty, CancellationToken cancellationToken) { return Task.FromResult(new Empty()); @@ -79,22 +89,161 @@ 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 (_analyzer.IsAnalyzing) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidOperation, + "Cannot change directory while analysis is in progress."); + return Task.FromResult(response); + } + + if (!_analyzer.ChangeDirectory(request.DirectoryPath)) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.DirectoryNotFound, + $"Directory not found: {request.DirectoryPath}"); + return Task.FromResult(response); + } + + response.Status = CreateNoErrorOperationStatus(); + 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 + { + if (!_analyzer.HasDirectory) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidOperation, + "No log directory has been set."); + return Task.FromResult(response); + } + + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + catch (ArgumentOutOfRangeException) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidArgument, + "Degree of parallelism must be non-negative."); + } + catch (InvalidOperationException ex) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidOperation, ex.Message); + } + 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 + { + if (!_analyzer.HasDirectory) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidOperation, + "No log directory has been set."); + return Task.FromResult(response); + } + + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames); + response.Status = CreateNoErrorOperationStatus(); + } + catch (ArgumentOutOfRangeException) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidArgument, + "Degree of parallelism must be non-negative."); + } + catch (ArgumentException ex) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.FileNotFound, ex.Message); + } + catch (InvalidOperationException ex) + { + response.Status = CreateErrorOperationStatus(AgentErrorCode.InvalidOperation, ex.Message); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing log files."); + } + return Task.FromResult(response); } public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var responses = new List(); + try + { + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result) || result is null) + { + responses.Add(new GetAnalysisResultResponse + { + Status = CreateErrorOperationStatus(AgentErrorCode.FileNotFound, + $"File not found: {request.FileName}"), + }); + return responses; + } + + var noError = CreateNoErrorOperationStatus(); + + var header = new AnalysisResultHeaderMessage + { + FileName = result.FileName, + FullName = result.FullName, + State = GrpcTypeConverter.ConvertToGrpc(result.State), + WorkerId = result.WorkerId, + }; + if (result.ErrorMessage is not null) + { + header.ErrorMessage = result.ErrorMessage; + } + + responses.Add(new GetAnalysisResultResponse + { + Header = header, + Status = noError, + }); + + // 仅当分析成功时,流式返回每一条日志 + if (result.State == AnalysisState.Succeeded) + { + foreach (var entry in result.Entries) + { + responses.Add(new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = noError, + }); + } + } + } + catch (Exception ex) + { + responses.Clear(); + responses.Add(new GetAnalysisResultResponse + { + Status = CreateInternalErrorOperationStatus(ex), + }); + _logger.LogError(ex, "An error occurred while retrieving analysis result."); + } + return responses; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..f440818 100644 --- a/src/LogAnalyzerAgent/Services/AgentService.cs +++ b/src/LogAnalyzerAgent/Services/AgentService.cs @@ -1,4 +1,4 @@ -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; using LogAnalyzer; using LogAnalyzerRpc.Protos; @@ -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 responseStream.WriteAsync(response); + } } } } diff --git a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs index eb69232..e54be18 100644 --- a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs +++ b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs @@ -1,4 +1,4 @@ -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; using LogAnalyzerRpc.Protos; using LogParser.Models; @@ -30,12 +30,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..edf4709 100644 --- a/src/LogAnalyzerRpc/GrpcTypeConverter.cs +++ b/src/LogAnalyzerRpc/GrpcTypeConverter.cs @@ -1,4 +1,4 @@ -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; using LogAnalyzer; using LogAnalyzerRpc.Protos; using LogParser.Models; @@ -20,12 +20,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 +58,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 +91,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..a97c4e2 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,41 @@ 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}"); + + // exception 值的格式:"ExceptionName: exception message" + // 按 ": " 拆成两部分 + var parts = internalMessage.Exception.Split(": ", 2, StringSplitOptions.None); + if (parts.Length != 2) + throw new FormatException( + $"Invalid exception format: '{internalMessage.Exception}'. " + + "Expected format: 'ExceptionName: exception message'"); + + return new InternalLogEntry( + LineNo: logRecord.LineNo, + Timestamp: DateTimeOffset.Parse(logRecord.Timestamp), + PodName: logRecord.PodName, + Severity: ParseSeverity(internalMessage.Severity), + ExceptionName: parts[0], + ExceptionMessage: parts[1] + ); } private static LogSeverity ParseSeverity(string severity) @@ -77,11 +106,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..ede727a 100644 --- a/src/LogParser/Visitors/KeyValueVisitor.cs +++ b/src/LogParser/Visitors/KeyValueVisitor.cs @@ -26,12 +26,33 @@ 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..8347e89 100644 --- a/src/RemoteCli/Program.cs +++ b/src/RemoteCli/Program.cs @@ -1,4 +1,4 @@ -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; using LogAnalyzerRpc; @@ -116,32 +116,162 @@ 6. Exit. private static async Task ShowLogFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + try + { + var response = await client.GetLogFilesAsync(new Empty()); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + return; + } + Console.WriteLine("Log files:"); + foreach (var file in response.FileNames) + { + Console.WriteLine($" - {file}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to get log files: {ex.Message}"); + } } private static int ReadDegreeOfParallelism() { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input degree of parallelism (0 for processor count):"); + while (true) + { + var input = Console.ReadLine(); + if (int.TryParse(input, out int degree)) + { + return degree; + } + Console.WriteLine("Invalid input, please try again:"); + } } private static List ReadFileNames() { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input file names separated by comma (e.g. log1.log,log2.log):"); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + return new List(); + } + return input.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToList(); } private static async Task AnalyzeFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var degree = ReadDegreeOfParallelism(); + var fileNames = ReadFileNames(); + if (fileNames.Count == 0) + { + Console.WriteLine("No file names provided."); + return; + } + var request = new AnalyzeFilesRequest() + { + DegreeOfParallelism = degree, + }; + request.FileNames.AddRange(fileNames); + try + { + var response = await client.AnalyzeFilesAsync(request); + if (response.Status.Success) + { + Console.WriteLine("Analysis finished."); + } + else + { + Console.WriteLine($"Analysis failed: {response.Status.Code}: {response.Status.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Analysis failed: {ex.Message}"); + } } private static async Task AnalyzeAll(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var degree = ReadDegreeOfParallelism(); + var request = new AnalyzeAllRequest() + { + DegreeOfParallelism = degree, + }; + try + { + var response = await client.AnalyzeAllAsync(request); + if (response.Status.Success) + { + Console.WriteLine("All files analysis finished."); + } + else + { + Console.WriteLine($"Analysis failed: {response.Status.Code}: {response.Status.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Analysis failed: {ex.Message}"); + } } private static async Task GetAnalysisResult(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input file name:"); + var fileName = Console.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(fileName)) + { + return; + } + var request = new GetAnalysisResultRequest() + { + FileName = fileName, + }; + try + { + 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; + Console.WriteLine($"File: {header.FileName}"); + Console.WriteLine($" State: {header.State}"); + if (header.State == AnalysisStateEnum.NotAnalyzed) + { + Console.WriteLine(" File has not been analyzed."); + } + else if (header.State == AnalysisStateEnum.Failed) + { + Console.WriteLine($" Analysis failed: {header.ErrorMessage}"); + } + break; + case GetAnalysisResultResponse.PayloadOneofCase.LogEntry: + var entry = GrpcTypeConverter.ConvertFromGrpc(response.LogEntry); + var visitor = new KeyValueVisitor(); + var dict = visitor.Dump(entry); + foreach (var kv in dict) + { + Console.WriteLine($" {kv.Key}: {kv.Value}"); + } + Console.WriteLine(); + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to get analysis result: {ex.Message}"); + } } } }