From c4657b9c1d4cd9f431ba6c905f6aae284826afc8 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Sat, 1 Aug 2026 15:44:31 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=9001-basic=E6=89=80?= =?UTF-8?q?=E6=9C=89=E4=BB=A3=E7=A0=81=E5=92=8C=E9=97=AE=E7=AD=94=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/01-basic/report.md | 32 ++++++++++++++++ src/LogParser/Models/LogEntries.cs | 4 +- src/LogParser/Parser/LineParser.cs | 46 ++++++++++++++++++++--- src/LogParser/Visitors/KeyValueVisitor.cs | 24 +++++++++++- 4 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 docs/01-basic/report.md diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..c0a28f5 --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,32 @@ +Q1.1: + +1.using var csv = new CsvReader(logFile, config); + +​ csv.GetRecords() + +通过在LogFileParser.cs中定义一个继承自ClassMap的映射类LogRecordMap实现的。通过Map(m => m.LineNo).Index(0);之类,将第0列设置为行号,将第3列设置为Message。 + +2.var root = JsonDocument.Parse(logRecord.Message).RootElement; + +root.TryGetProperty("event", out var eventElement) + +ventElement.GetString() switch { "call" => ..., "request" => ..., "internal" => ... } + +3.调用了 System.Text.Json 库中的 JsonSerializer.Deserialize(string, JsonSerializerOptions) + +给每个属性加上了property: JsonRequired特性,如果缺失特性,会抛出异常 + +定义了JsonSerializerOptions的options静态对象,设置了PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower + +Q1.2: + +Dictionary KeyValueVisitor.Dump(LogEntry entry) + +TResult CallLogEntry.Accept(ILogEntryVisitor visitor) + +Dictionary KeyValueVisitor.Visit(CallLogEntry entry) + +Q1.3: + +提示词:给我整个代码的类的结构图,与程序执行的流程图。AI的解答比我更加迅速,并且能够给我提供相对应的知识点,便于更好更快的理解代码。 + 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..1b57491 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}"); + var exception = internalMessage.Exception; + var idx = exception.IndexOf(": "); + if (idx == -1) + { + throw new FormatException($"Invalid exception format: {exception}"); + } + var exceptionName = exception.Substring(0, idx); + var exceptionMessage = exception.Substring(idx + 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 +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..c4ee145 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 + }; } } } From fb8f35d2561553a957e70c883eb9deb05db06e31 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Mon, 3 Aug 2026 20:25:06 +0800 Subject: [PATCH 2/7] homework2 --- docs/02-multithreading/report.md | 25 ++++++++ src/LocalCli/Program.cs | 92 ++++++++++++++++++++++++++++-- src/LogAnalyzer/LogFileAnalyzer.cs | 90 +++++++++++++++++------------ src/LogAnalyzer/WorkQueue.cs | 39 ++++++++++++- 4 files changed, 202 insertions(+), 44 deletions(-) create mode 100644 docs/02-multithreading/report.md diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..cc2f585 --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,25 @@ +Q2.1: + +*共享变量有_items,和 _isCompleted,保护机制,通过lock( _items)来保护临界区,同时使用Monitor.Wait和Monitor.PulseAll来实现条件等待和唤醒处理。 + +*共享变量有 _ isAnalyzing , _currentDirectory , _logFiles , _analysisResults。保护机制,通过lock( _syncRoot)进行保护。 + +*后果:if条件不会重新检查队列状态,线程会继续执行,如果队列继续为空,就会抛出队列空的报错,造成崩溃。 + +Q2.2: + +**var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly) + .Select(filePath => Path.GetFileName(filePath)) + .OrderBy(fileName => fileName); + +*只需要将 Directory.EnumerateFiles 的第三个参数由SearchOption.TopDirectoryOnly 修改为 SearchOption.AllDirectories 即可 + +Q2.3: + +*提供给我类的关系和接口,提供给我所需函数的实现形式和出现位置,讲解pulseall和wait + +*帮我讲解代码框架 + +*在生成workmain中的 AnalysisResult构造时,最初直接将parser.Parse(reader) 的返回值(IEnumerable)传给了需要 IReadOnlyList的构造函数,导致类型不匹配报错。最终加了.ToList()解决 + +*偏高 \ No newline at end of file diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..191d5e4 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -112,22 +112,106 @@ 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 current directory:"); + 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):"); + Console.Write(">>> "); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine("Input cannot be empty."); + return; + } + var fileNames = input.Split(',') + .Select(f => f.Trim()) + .Where(f => !string.IsNullOrEmpty(f)) + .ToList(); + + if (fileNames.Count == 0) + { + Console.WriteLine("No valid file names provided."); + return; + } + + try + { + analyzer.AnalyzeFiles(0, fileNames); + Console.WriteLine("Analysis completed successfully."); + } + catch (ArgumentException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred during analysis: {ex.Message}"); + } } private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + try + { + analyzer.AnalyzeAll(0); + Console.WriteLine("All log files analyzed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred during analysis: {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()?.Trim(); + if (string.IsNullOrEmpty(fileName)) + { + Console.WriteLine("File name cannot be empty."); + return; + } + + if (!analyzer.TryGetAnalysisResult(fileName, out var result) || result is null) + { + Console.WriteLine($"File '{fileName}' does not exist or is not in the directory."); + return; + } + + switch (result.State) + { + case AnalysisState.NotAnalyzed: + Console.WriteLine($"File '{fileName}' has NOT been analyzed yet. Please run analysis first."); + break; + + case AnalysisState.Succeeded: + Console.WriteLine($"Analysis result for '{fileName}' (Worker #{result.WorkerId}):"); + KeyValueVisitor.Dump(result.Entries); + break; + + case AnalysisState.Failed: + Console.WriteLine($"Analysis for '{fileName}' FAILED!"); + Console.WriteLine($"Error message: {result.ErrorMessage}"); + break; + } } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..f6c8863 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -138,10 +138,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 +147,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 +161,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 result)) + { + throw new InvalidOperationException($"Unknown log file: {file.Name}"); + } + if (result.State == AnalysisState.NotAnalyzed) + { + logFilesToParse.Add(file); + } } } @@ -180,10 +179,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 +191,16 @@ 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(); + } + foreach (var worker in workers) + { + worker.Join(); } - - /* - * Wait for (join) all threads to end - */ - // TODO: T2.2 } private void WorkerMain(int workerId, WorkQueue queue) @@ -212,21 +212,37 @@ private void WorkerMain(int workerId, WorkQueue queue) AnalysisResult result; try { - // Parse file - throw new NotImplementedException("TODO: T2.2"); + + using var streamReader = new StreamReader(file.FullName); + var entries = parser.Parse(streamReader); + + 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; + } } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..22c2dfa 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -1,4 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Threading; + namespace LogAnalyzer { @@ -20,17 +24,46 @@ public bool IsCompleted public void Enqueue(T item) { - throw new NotImplementedException("TODO: T2.1"); + lock(_items) + { + if(_isCompleted) + { + throw new InvalidOperationException("Adding has been completed."); + } + _items.Enqueue(item); + Monitor.PulseAll(_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); + } } } } From b04a82b8074bb499395aa34ca78e8986089558e2 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Tue, 4 Aug 2026 11:22:46 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E4=BD=9C=E4=B8=9A2=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LogAnalyzer/LogFileAnalyzer.cs | 2 +- src/LogAnalyzer/WorkQueue.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index f6c8863..2a346f6 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -179,12 +179,12 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis var queue = new WorkQueue(); + // 1. 先将任务入队 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++) diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 22c2dfa..8b334f2 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; - namespace LogAnalyzer { public class WorkQueue @@ -24,9 +23,9 @@ public bool IsCompleted public void Enqueue(T item) { - lock(_items) + lock (_items) { - if(_isCompleted) + if (_isCompleted) { throw new InvalidOperationException("Adding has been completed."); } @@ -37,17 +36,19 @@ public void Enqueue(T item) public bool TryDequeue([NotNullWhen(true)] out T? item) { - lock (_items) + lock (_items) { - while (_items.Count== 0 &&! _isCompleted) - { + while (_items.Count == 0 && !_isCompleted) + { Monitor.Wait(_items); } - if( _items.Count >0) + + if (_items.Count > 0) { - item=_items.Dequeue(); + item = _items.Dequeue()!; return true; } + item = default; return false; } @@ -66,4 +67,4 @@ public void CompleteAdding() } } } -} +} \ No newline at end of file From 9a4fac2cc500e246bd94b69b604189f27b387c56 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Tue, 4 Aug 2026 11:54:01 +0800 Subject: [PATCH 4/7] homework/02-multithreading --- src/LogAnalyzer/LogFileAnalyzer.cs | 19 +++++++++++-------- src/LogAnalyzer/WorkQueue.cs | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index 2a346f6..56fab08 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -1,7 +1,10 @@ -using LogParser.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using LogParser.Models; using LogParser.Parser; -using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography.X509Certificates; namespace LogAnalyzer { @@ -15,6 +18,7 @@ public class LogFileAnalyzer public string? CurrentDirectory => _currentDirectory; public bool HasDirectory => _currentDirectory is not null; + public bool IsAnalyzing { get @@ -67,7 +71,7 @@ public bool ChangeDirectory(string? directoryPath) .OrderBy(fileName => fileName); foreach (var fileName in logFiles) { - _logFiles.Add(fileName, new FileInfo(Path.Join(_currentDirectory, fileName))); + _logFiles.Add(fileName, new FileInfo(Path.Combine(directoryPath, fileName))); _analysisResults.Add(fileName, new AnalysisResult( FileName: fileName, FullName: _logFiles[fileName].FullName, @@ -179,12 +183,12 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis var queue = new WorkQueue(); - // 1. 先将任务入队 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++) @@ -193,7 +197,8 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis string threadName = $"log-analyzer-worker-{workerId}"; workers[i] = new Thread(() => WorkerMain(workerId, queue)) { - Name = threadName + Name = threadName, + IsBackground = true }; workers[i].Start(); } @@ -212,7 +217,6 @@ private void WorkerMain(int workerId, WorkQueue queue) AnalysisResult result; try { - using var streamReader = new StreamReader(file.FullName); var entries = parser.Parse(streamReader); @@ -237,7 +241,6 @@ private void WorkerMain(int workerId, WorkQueue queue) ); } - lock (_syncRoot) { _analysisResults[file.Name] = result; diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 8b334f2..1164d1a 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -1,6 +1,6 @@ -using System.Diagnostics.CodeAnalysis; -using System; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; namespace LogAnalyzer From be2240484988c3ab71d70a03235142e172df1996 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Tue, 4 Aug 2026 18:41:05 +0800 Subject: [PATCH 5/7] homework/03-async-grpc --- .../Applications/AgentSession.cs | 156 ++++++++++++++- src/LogAnalyzerAgent/Services/AgentService.cs | 19 +- src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs | 35 +++- src/LogAnalyzerRpc/GrpcTypeConverter.cs | 62 +++++- src/RemoteCli/Program.cs | 177 +++++++++++++++++- 5 files changed, 418 insertions(+), 31 deletions(-) diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..887cda2 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -4,6 +4,7 @@ using LogAnalyzerRpc.Protos; using LogAnalyzerRpc; using LogParser.Visitors; +using Microsoft.Extensions.Logging; namespace LogAnalyzerAgent.Applications { @@ -24,7 +25,7 @@ private static OperationStatusMessage CreateInternalErrorOperationStatus(Excepti { Success = false, Code = AgentErrorCode.InternalError, - Message = $"An error occurred while retrieving agent status: {ex.Message}", + Message = $"An error occurred: {ex.Message}", }; } @@ -79,22 +80,165 @@ 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 + { + _analyzer.ChangeDirectory(request.DirectoryPath); + + response.CurrentDirectory = _analyzer.CurrentDirectory ?? ""; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + response.Status = CreateNoErrorOperationStatus(); + } + catch (DirectoryNotFoundException ex) + { + response.Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.DirectoryNotFound, Message = ex.Message }; + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "Failed to change directory."); + } + return Task.FromResult(response); } public Task AnalyzeAll(AnalyzeAllRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var response = new AnalyzeAllResponse(); + try + { + if (request.DegreeOfParallelism <= 0) + { + response.Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.InvalidArgument, Message = "Degree of parallelism must be greater than 0." }; + return Task.FromResult(response); + } + + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + catch (InvalidOperationException ex) + { + response.Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.InvalidOperation, Message = ex.Message }; + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "Failed to analyze 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 (request.DegreeOfParallelism <= 0) + { + response.Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.InvalidArgument, Message = "Degree of parallelism must be greater than 0." }; + return Task.FromResult(response); + } + + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames.ToList()); + response.Status = CreateNoErrorOperationStatus(); + } + catch (FileNotFoundException ex) + { + response.Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.FileNotFound, Message = ex.Message }; + } + catch (InvalidOperationException ex) + { + response.Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.InvalidOperation, Message = ex.Message }; + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "Failed to analyze files."); + } + return Task.FromResult(response); } + // 恢复为返回 IReadOnlyList,匹配测试要求 public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var responses = new List(); + var fileName = request.FileName; + + if (!_analyzer.HasDirectory) + { + responses.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.InvalidOperation, Message = "Directory not set." } + }); + return responses; + } + + var fileNames = _analyzer.GetLogFiles(); + + if (!fileNames.Contains(fileName)) + { + responses.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.FileNotFound, Message = $"File {fileName} not found in the current directory." } + }); + return responses; + } + + try + { + if (!_analyzer.TryGetAnalysisResult(fileName, out var result) || result == null) + { + responses.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage { Success = false, Code = AgentErrorCode.InternalError, Message = "Failed to read analysis result." } + }); + return responses; + } + + var header = new AnalysisResultHeaderMessage + { + FileName = fileName, + FullName = result.FullName ?? "", + State = GrpcTypeConverter.ConvertToGrpc(result.State), + WorkerId = result.WorkerId, + }; + + if (result.ErrorMessage != null) + { + header.ErrorMessage = result.ErrorMessage; + } + + // 2. 无论是否分析成功,先存入 header + responses.Add(new GetAnalysisResultResponse + { + Header = header, + Status = CreateNoErrorOperationStatus() + }); + + // 3. 若分析成功,则存入一系列 LogEntry + if (result.State == AnalysisState.Succeeded && result.Entries != null) + { + foreach (var entry in result.Entries) + { + cancellationToken.ThrowIfCancellationRequested(); + + responses.Add(new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus() + }); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to get analysis result for {fileName}."); + responses.Add(new GetAnalysisResultResponse + { + Status = CreateInternalErrorOperationStatus(ex) + }); + } + + return responses; } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..e906833 100644 --- a/src/LogAnalyzerAgent/Services/AgentService.cs +++ b/src/LogAnalyzerAgent/Services/AgentService.cs @@ -29,27 +29,34 @@ 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"); + // 从 Session 获取结果列表 + var responses = _session.GetAnalysisResult(request, context.CancellationToken); + + // 遍历列表并流式写入到 gRPC 响应流中 + foreach (var response in responses) + { + await responseStream.WriteAsync(response); + } } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs index eb69232..9cb2b42 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; namespace LogAnalyzerRpc { @@ -30,12 +31,40 @@ 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), + // 根据 proto 中的 RequestLogEntryMessage 补充特有字段 + 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), + // 根据 proto 中的 InternalLogEntryMessage 补充特有字段 + ExceptionName = entry.ExceptionName, + ExceptionMessage = entry.ExceptionMessage, + } + }; } } -} +} \ No newline at end of file diff --git a/src/LogAnalyzerRpc/GrpcTypeConverter.cs b/src/LogAnalyzerRpc/GrpcTypeConverter.cs index 029122e..f39298e 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) @@ -63,14 +88,35 @@ public static LogEntry ConvertFromGrpc(LogEntryMessage entryMessage) Timestamp: entryMessage.CallLogEntry.Timestamp.ToDateTimeOffset(), PodName: entryMessage.CallLogEntry.PodName, Severity: ConvertFromGrpc(entryMessage.CallLogEntry.Severity), - RequestId: entryMessage.CallLogEntry.RequestId, + RequestId: entryMessage.CallLogEntry.RequestId, 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), + // 补充 RequestLogEntry 字段 + 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), + // 补充 InternalLogEntry 字段 + ExceptionName: entryMessage.InternalLogEntry.ExceptionName, + ExceptionMessage: entryMessage.InternalLogEntry.ExceptionMessage + ), + _ => throw new ArgumentException($"Unknown entry type: {entryMessage.EntryCase}", nameof(entryMessage)) }; } } -} +} \ No newline at end of file diff --git a/src/RemoteCli/Program.cs b/src/RemoteCli/Program.cs index de0ac99..04b3bdd 100644 --- a/src/RemoteCli/Program.cs +++ b/src/RemoteCli/Program.cs @@ -5,6 +5,11 @@ using LogAnalyzerRpc.Protos; using LogParser.Visitors; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace RemoteCli { @@ -20,7 +25,16 @@ static async Task Main(string[] args) Console.WriteLine($"Connecting to agent at {address}..."); using var channel = GrpcChannel.ForAddress(address); var client = new LogAnalyzerAgentServiceClient(channel); - _ = await client.PingAsync(new Empty()); + + try + { + _ = await client.PingAsync(new Empty()); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to connect to agent: {ex.Message}"); + return; + } await ChooseAction(client); } @@ -116,32 +130,179 @@ 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; + } + + // 按照参考截图输出格式 [file1, file2] + Console.WriteLine($"[{string.Join(", ", response.FileNames)}]"); } private static int ReadDegreeOfParallelism() { - throw new NotImplementedException("TODO: T3.2"); + while (true) + { + Console.WriteLine("Please input degree of parallelism:"); + var input = Console.ReadLine(); + + // 确保输入是数字并且不小于 0(通常 0 代表自动配置,正数代表具体线程数) + if (int.TryParse(input, out int dop) && dop >= 0) + { + return dop; + } + Console.WriteLine("Invalid input, please try again."); + } } private static List ReadFileNames() { - throw new NotImplementedException("TODO: T3.2"); + while (true) + { + Console.WriteLine("Please input log file names (comma separated):"); + var input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine("Invalid input, please try again."); + continue; + } + + // 拆分字符串,去除两端空格,并忽略空元素 + var files = input.Split(',') + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + + if (files.Count == 0) + { + Console.WriteLine("No valid file names found, please try again."); + continue; + } + + return files; + } } private static async Task AnalyzeFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + int dop = ReadDegreeOfParallelism(); + List files = ReadFileNames(); + + var request = new AnalyzeFilesRequest + { + DegreeOfParallelism = dop + }; + request.FileNames.AddRange(files); + + var response = await client.AnalyzeFilesAsync(request); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + } + else + { + Console.WriteLine($"Analysis completed: [{string.Join(", ", files)}]"); + } } private static async Task AnalyzeAll(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + int dop = ReadDegreeOfParallelism(); + var request = new AnalyzeAllRequest + { + DegreeOfParallelism = dop + }; + + var response = await client.AnalyzeAllAsync(request); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {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(); + + if (string.IsNullOrWhiteSpace(fileName)) + { + Console.WriteLine("Invalid input."); + return; + } + + var request = new GetAnalysisResultRequest + { + FileName = fileName + }; + + try + { + // 获取流式调用对象 + using var call = client.GetAnalysisResult(request); + + // 使用 await foreach 迭代读取服务端流 + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + return; + } + + // 处理头部信息 + if (response.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.Header) + { + var header = response.Header; + if (header.State == AnalysisStateEnum.NotAnalyzed) + { + Console.WriteLine($"File {fileName} has not been analyzed yet."); + return; // 还没分析,直接退出 + } + if (header.State == AnalysisStateEnum.Failed) + { + Console.WriteLine($"File {fileName} analysis failed: {header.ErrorMessage}"); + return; + } + } + // 处理日志条目 + else if (response.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.LogEntry) + { + var entry = response.LogEntry; + string output = ""; + + // 根据具体的 oneof 类型输出日志 + switch (entry.EntryCase) + { + case LogEntryMessage.EntryOneofCase.CallLogEntry: + var c = entry.CallLogEntry; + output = $"LineNo: {c.LineNo}, Timestamp: {c.Timestamp.ToDateTimeOffset():O}, PodName: {c.PodName}, Severity: {c.Severity}, EventType: {c.EventType}, RequestId: {c.RequestId}, TargetService: {c.TargetService}, DurationMs: {c.DurationMs}"; + break; + case LogEntryMessage.EntryOneofCase.RequestLogEntry: + var r = entry.RequestLogEntry; + output = $"LineNo: {r.LineNo}, Timestamp: {r.Timestamp.ToDateTimeOffset():O}, PodName: {r.PodName}, Severity: {r.Severity}, EventType: {r.EventType}, RequestId: {r.RequestId}, Method: {r.Method}, Path: {r.Path}, StatusCode: {r.StatusCode}"; + break; + case LogEntryMessage.EntryOneofCase.InternalLogEntry: + var i = entry.InternalLogEntry; + output = $"LineNo: {i.LineNo}, Timestamp: {i.Timestamp.ToDateTimeOffset():O}, PodName: {i.PodName}, Severity: {i.Severity}, EventType: {i.EventType}, ExceptionName: {i.ExceptionName}, ExceptionMessage: {i.ExceptionMessage}"; + break; + } + + Console.WriteLine(output); + } + } + } + catch (RpcException ex) + { + Console.WriteLine($"RPC Error: {ex.Status.Detail}"); + } } } -} +} \ No newline at end of file From 24dac00d5504e67f10ea2057a26ecd9ca1a5c7f0 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Tue, 4 Aug 2026 21:25:05 +0800 Subject: [PATCH 6/7] homework/03-async-grpc --- src/LocalCli/Program.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 191d5e4..b65878d 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -204,7 +204,14 @@ private static void GetAnalysisResult(LogFileAnalyzer analyzer) case AnalysisState.Succeeded: Console.WriteLine($"Analysis result for '{fileName}' (Worker #{result.WorkerId}):"); - KeyValueVisitor.Dump(result.Entries); + // 修改部分:遍历传入 + if (result.Entries != null) + { + foreach (var entry in result.Entries) + { + KeyValueVisitor.Dump(entry); + } + } break; case AnalysisState.Failed: From 754dd2d3148b72957e33f755cfeb0173f4749174 Mon Sep 17 00:00:00 2001 From: pintu545 <282360370@qq.com> Date: Thu, 6 Aug 2026 18:48:44 +0800 Subject: [PATCH 7/7] homework/02-multithreading --- src/LocalCli/Program.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 191d5e4..403fde5 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -204,7 +204,11 @@ private static void GetAnalysisResult(LogFileAnalyzer analyzer) case AnalysisState.Succeeded: Console.WriteLine($"Analysis result for '{fileName}' (Worker #{result.WorkerId}):"); - KeyValueVisitor.Dump(result.Entries); + // Fix: Iterate over the entries collection + foreach (var entry in result.Entries) + { + KeyValueVisitor.Dump(entry); + } break; case AnalysisState.Failed: