diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..b52273c --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,74 @@ +## Q 1.1 +### 1.1.1 +- 以下代码负责按逗号分割: +```c sharp +var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false + }; + using var csv = new CsvReader(logFile, config); + csv.Context.RegisterClassMap(); +``` +- 经过CSV分割后,每一行的文本通过以下代码对应到LineNo、Timestamp、PodName、Message四个字段 +```c sharp +public LogRecordMap() + { + Map(m => m.LineNo).Index(0); + Map(m => m.Timestamp).Index(1); + Map(m => m.PodName).Index(2); + Map(m => m.Message).Index(3); + } +``` +,存为一个LogRecord对象,之后单独`JsonDocument.Parse(logRecord.Message)`提取`event`类型,交由`LineParser.CreateCall(logRecord)`等方法,按照`LogEntries.cs`中的定义映射各自含义 +### 1.1.2 +- 在`LineParser::ParseLine(LogRecord logRecord)`中: +```c sharp +if (root.TryGetProperty("event", out var eventElement)) + { + return eventElement.GetString() switch + { + "call" => LineParser.CreateCall(logRecord), + "request" => LineParser.CreateRequest(logRecord), + "internal" => LineParser.CreateInternal(logRecord), + _ => throw new FormatException($"Unknown event type: {eventElement.GetString()} in log message: {logRecord.Message}") + }; + } + else + { + throw new FormatException($"Log message does not contain 'event' property: {logRecord.Message}"); + } +``` +### 1.1.3 +- 使用json库: +```c sharp +using System.Text.Json.Serialization; +... +JsonSerializer.Deserialize(logRecord.Message, options); +... +``` + +- 使用`[property: JsonRequired]`强制指定,不存在时报错 +- 通过options传入源文本的编码方式Kebab +``` c sharp +private static JsonSerializerOptions options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, +}; +... +var callMessage = JsonSerializer.Deserialize(logRecord.Message, options) +... + +``` +驼峰命名法在LogEntry的子类中分别指定。 + +## Q1.2 +- `Dictionary KeyValueVisitor.Dump(LogEntry entry)` +- `TResult LogEntry.Accept(ILogEntryVisitor visitor)` +- `TResult CallLogEntry.Accept(ILogEntryVisitor visitor)` +- `Dictionary Visit(CallLogEntry entry)` + +## Q1.3 +- 有使用 +### Q1.3.b +- 主要使用了Copilot的自动补全,相比我自己写更省时间,减少了排错成本,经测试核查无误 +- 也让ai解释了一些语句的含义、函数的用法 \ No newline at end of file diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..6e05a5b --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,25 @@ +## Q2.1 +### 2.1.1 +- WorkQueue共享变量有:`_items`、`_isCompleted` + - `_items`通过`lock(_items){...}`防止竞争; + - `_isCompleted`通过每次读取时`lock (_items)`保证读到的值正确,写入时`lock (_items)`保证单一线程访问 +- LogFileAnalyzer的共享变量有:`_analysisResults`、`_isAnalyzing`、`_logFiles`、`_currentDirectory` + - `_analysisResults`、`_logFiles`通过`lock(_syncRoot){...}`保护 + - `_isAnalyzing`通过每次读取时`lock (_syncRoot)`保证读到的值正确 + - `_currentDirectory`写入在 `lock (_syncRoot)` 中,读取未加锁,不过其一般也不会被多个线程写入 +- 不用while的话,wait被唤醒后就继续向下执行了,就像顾客没有等到生产者通知生产完成就尝试去仓库抢东西,可能产生不符合预期的bug +## Q2.2 +- 扫描全部 .log 后缀的日志文件: +```csharp +var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly) + .Select(filePath => Path.GetFileName(filePath)) + .OrderBy(fileName => fileName); + foreach (var fileName in logFiles){...} +``` +- `SearchOption.AllDirectories` +## Q2.3 +- 有使用 +### 2.3.b +- 主要使用Copilot的自动补全,以及让ai解释一些语句的含义、函数的用法,提示词就是“解释xxx的参数含义与用法” +- 由于自动补全是顺着我的思路写的,可能我起头起偏了,ai写的也会出问题,比如`worker.Join()`的时候没有注意到这个函数会自动阻塞,就写了个while,然后copilot就顺着开编了,这时就需要另外让ai解释一下函数用法,检查一下逻辑是否合理,多给几个角度再自行核对 +- 本节难度适中 \ No newline at end of file diff --git a/docs/03-async-grpc/image-1.png b/docs/03-async-grpc/image-1.png new file mode 100644 index 0000000..afeaaec Binary files /dev/null and b/docs/03-async-grpc/image-1.png differ diff --git a/docs/03-async-grpc/image.png b/docs/03-async-grpc/image.png new file mode 100644 index 0000000..1c8281a Binary files /dev/null and b/docs/03-async-grpc/image.png differ diff --git a/docs/03-async-grpc/report.md b/docs/03-async-grpc/report.md new file mode 100644 index 0000000..6695d2e --- /dev/null +++ b/docs/03-async-grpc/report.md @@ -0,0 +1,14 @@ +- 普通测试 +![alt text](image.png) +- 稳健性测试 +![alt text](image-1.png) +## Q3.1 +- 最大的区别是需要考虑网络通信的延迟、丢包、超时等各种未知情况,这些情况都需要服务端和客户端处理。这也使得异步、握手、状态码等机制在网络通信中得到广泛应用 +- 难点与复杂之处主要在于,需要处理网络通信的不稳定性,除此之外还要考虑前后端通信接口的一致性、可维护性,以及服务器在大量访问请求下的稳定性等 +## Q3.2 +有使用 +### 3.2.b +- AI主要负责自动补全,解释一些语句、接口的用法,以及帮忙找bug +- 提示词:解释一下xxx的用法与参数含义 +- 至少在自动补全方面,AI经常捏造出各种各样实际不存在的字段或参数,比如往`var request = new ChangeDirectoryRequest()`的构造函数加了个不知哪来的`Force = true`,这时就只能翻定义 +- 确有了解更多知识,比如CancellationToken,在gRPC中可以让服务端按需取消某些任务,比如客户端断连时触发,防止服务端做无用功 \ No newline at end of file diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..68b171e 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -88,8 +88,14 @@ 6. Exit. switch (choice) { case 1: + actions[choice](analyzer); + break; case 2: + actions[choice](analyzer); + break; case 3: + actions[choice](analyzer); + break; case 4: actions[choice](analyzer); break; @@ -112,22 +118,78 @@ 6. Exit. private static void ShowLogFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + analyzer.GetLogFiles().ToList().ForEach(fileName => Console.WriteLine(fileName)); } private static void AnalyzeFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Please input log file names, separated by space:"); + var input = Console.ReadLine(); + if (input is null) + { + return; + } + var fileNames = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + Console.WriteLine("Please input degree of parallelism:"); + var degreeOfParallelismStr = Console.ReadLine(); + if (degreeOfParallelismStr is null) + { + return; + } + if (!int.TryParse(degreeOfParallelismStr, out var degreeOfParallelism)) + { + Console.WriteLine("Invalid input for degree of parallelism."); + return; + } + analyzer.AnalyzeFiles(degreeOfParallelism, fileNames); + Console.WriteLine("Done."); } private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + Console.WriteLine("Please input degree of parallelism:"); + var degreeOfParallelismStr = Console.ReadLine(); + if (degreeOfParallelismStr is null) + { + return; + } + if (!int.TryParse(degreeOfParallelismStr, out var degreeOfParallelism)) + { + Console.WriteLine("Invalid input for degree of parallelism."); + return; + } + analyzer.AnalyzeAll(degreeOfParallelism); + Console.WriteLine("Done."); } private static void GetAnalysisResult(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + analyzer.GetLogFiles().ToList().ForEach(fileName => + { + if (analyzer.TryGetAnalysisResult(fileName, out var result)) + { + if (result == null) + { + Console.WriteLine($"No analysis result for file: {fileName}"); + } + Console.WriteLine($"File: {fileName}"); + Console.WriteLine($"State: {result.State}"); + if (result.State == AnalysisState.Failed) + { + Console.WriteLine($"Error Message: {result.ErrorMessage}"); + } + else + { + Console.WriteLine($"Log Entries Count: {result.Entries.Count}"); + } + Console.WriteLine(); + } + else + { + Console.WriteLine($"No analysis result for file: {fileName}"); + } + }); } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..0631646 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -141,7 +141,7 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) /* * Set _isAnalyzing */ - // TODO: T2.2 + _isAnalyzing = true; } try @@ -154,7 +154,10 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) * Unset _isAnalyzing * Remember to lock _syncRoot to prevent data race */ - // TODO: T2.2 + lock (_syncRoot) + { + _isAnalyzing = false; + } } } @@ -169,7 +172,15 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis * Filter unparsed files. * If there is an unknown file, throw System.InvalidOperationException. */ - throw new NotImplementedException("TODO: T2.2"); + if (!_analysisResults.ContainsKey(file.Name)) + { + throw new InvalidOperationException($"Unknown file: {file.Name}"); + } + if (_analysisResults[file.Name].State == AnalysisState.NotAnalyzed) + { + logFilesToParse.Add(file); + } + } } @@ -183,7 +194,11 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis /* * Enqueue log files */ - // TODO: T2.2 + foreach (var file in logFilesToParse) + { + queue.Enqueue(file); + } + queue.CompleteAdding(); degreeOfParallelism = Math.Max(Math.Min(degreeOfParallelism, logFilesToParse.Count), 1); var workers = new Thread[degreeOfParallelism]; @@ -194,13 +209,21 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis /* * Create and start threads to run `WorkerMain` */ - // TODO: T2.2 + workers[i] = new Thread(() => WorkerMain(workerId, queue)) + { + Name = threadName, + IsBackground = true + }; + workers[i].Start(); } /* * Wait for (join) all threads to end */ - // TODO: T2.2 + foreach (var worker in workers) + { + worker.Join(); + } } private void WorkerMain(int workerId, WorkQueue queue) @@ -213,19 +236,36 @@ private void WorkerMain(int workerId, WorkQueue queue) try { // Parse file - throw new NotImplementedException("TODO: T2.2"); + result = new AnalysisResult( + file.Name, + file.FullName, + AnalysisState.Succeeded, + parser.Parse(file.OpenText()).ToList(), + null, + workerId + ); } catch (Exception ex) { // Save exception message to result - throw new NotImplementedException("TODO: T2.2"); + result = new AnalysisResult( + file.Name, + file.FullName, + AnalysisState.Failed, + new List(), + ex.Message, + 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..26d9c31 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -20,17 +20,55 @@ public bool IsCompleted public void Enqueue(T item) { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + _isCompleted = false; + if (item == null) + throw new ArgumentNullException(nameof(item)); + _items.Enqueue(item); + } } public bool TryDequeue([NotNullWhen(true)] out T? item) { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + if (_items.Count > 0) + { + item = _items.Dequeue(); + if (item != null) + { + return true; + } + } + else + { + while (!_isCompleted) + { + Monitor.Wait(_items); + } + if (_items.Count > 0) + { + item = _items.Dequeue(); + if (item != null) + { + return true; + } + } + } + item = default; + return false; + } + } public void CompleteAdding() { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + _isCompleted = true; + Monitor.PulseAll(_items); + } } } } diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..2c838ca 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -79,22 +79,121 @@ 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.ChangeDirectory(request.DirectoryPath)) + { + response.Status = CreateNoErrorOperationStatus(); + response.CurrentDirectory = _analyzer.CurrentDirectory ?? ""; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + } + else + { + response.Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.DirectoryNotFound, + Message = $"Directory not found: {request.DirectoryPath}.", + }; + } + } + } + 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 + { + _analyzer.AnalyzeAll(request.DegreeOfParallelism); + response.Status = CreateNoErrorOperationStatus(); + } + 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 + { + _analyzer.AnalyzeFiles(request.DegreeOfParallelism, request.FileNames); + response.Status = CreateNoErrorOperationStatus(); + } + 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 not null) + { + responses.Add(new GetAnalysisResultResponse + { + Header = new AnalysisResultHeaderMessage + { + FileName = result.FileName ?? "Unknown", + FullName = result.FullName ?? "Unknown", + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? string.Empty, + WorkerId = result.WorkerId, + }, + Status = CreateNoErrorOperationStatus(), + }); + + if (result.State == AnalysisState.Succeeded) + { + foreach (var entry in result.Entries) + { + responses.Add(new GetAnalysisResultResponse + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus(), + }); + } + } + } + else + { + responses.Add(new GetAnalysisResultResponse + { + Status = new OperationStatusMessage + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File not found: {request.FileName}", + }, + }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while retrieving analysis results."); + responses.Add(new GetAnalysisResultResponse + { + Status = CreateInternalErrorOperationStatus(ex), + }); + } + return responses; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..1bd81f2 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 results = _session.GetAnalysisResult(request, context.CancellationToken); + foreach (var result in results) + { + await responseStream.WriteAsync(result); + } } } } diff --git a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs index eb69232..3196aac 100644 --- a/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs +++ b/src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs @@ -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..195e230 100644 --- a/src/LogAnalyzerRpc/GrpcTypeConverter.cs +++ b/src/LogAnalyzerRpc/GrpcTypeConverter.cs @@ -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.Internal => LogEventTypeEnum.Internal, + LogEventType.Request => LogEventTypeEnum.Request, + _ => 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.Internal => LogEventType.Internal, + LogEventTypeEnum.Request => LogEventType.Request, + _ => throw new ArgumentOutOfRangeException(nameof(eventType), eventType, null) + }; } public static LogEntry ConvertFromGrpc(LogEntryMessage entryMessage) @@ -63,12 +87,28 @@ 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), + 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..c902f6d 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}") }; } @@ -48,14 +48,40 @@ 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}"); + string exception = internalMessage.Exception; + int index = exception.IndexOf(":"); + string exceptionName = exception.Substring(0, index).Trim(); + string exceptionMessage = exception.Substring(index + 1).Trim(); + 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 +103,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..f70bcc2 100644 --- a/src/LogParser/Visitors/KeyValueVisitor.cs +++ b/src/LogParser/Visitors/KeyValueVisitor.cs @@ -26,12 +26,32 @@ public Dictionary Visit(CallLogEntry entry) public Dictionary Visit(RequestLogEntry entry) { - throw new NotImplementedException("TODO: T1.3"); + return new Dictionary + { + ["LineNo"] = entry.LineNo.ToString(), + ["Timestamp"] = entry.Timestamp.ToString("O"), + ["PodName"] = entry.PodName, + ["Severity"] = entry.Severity.ToString(), + ["EventType"] = entry.EventType.ToString(), + ["RequestId"] = entry.RequestId, + ["Method"] = entry.Method, + ["Path"] = entry.Path, + ["StatusCode"] = entry.StatusCode.ToString(), + }; } public Dictionary Visit(InternalLogEntry entry) { - throw new NotImplementedException("TODO: T1.3"); + return new Dictionary + { + ["LineNo"] = entry.LineNo.ToString(), + ["Timestamp"] = entry.Timestamp.ToString("O"), + ["PodName"] = entry.PodName, + ["Severity"] = entry.Severity.ToString(), + ["EventType"] = entry.EventType.ToString(), + ["ExceptionName"] = entry.ExceptionName, + ["ExceptionMessage"] = entry.ExceptionMessage, + }; } } } diff --git a/src/RemoteCli/Program.cs b/src/RemoteCli/Program.cs index de0ac99..b7702c5 100644 --- a/src/RemoteCli/Program.cs +++ b/src/RemoteCli/Program.cs @@ -30,7 +30,7 @@ private static async Task InputDirectory(LogAnalyzerAgentServiceClient cli while (true) { Console.WriteLine("Please input directory containing log files:"); - var directory = Console.ReadLine(); + var directory = Console.ReadLine()?.Trim(); if (directory is null) { return false; @@ -93,8 +93,14 @@ 6. Exit. switch (choice) { case 1: + await actions[choice](client); + break; case 2: + await actions[choice](client); + break; case 3: + await actions[choice](client); + break; case 4: await actions[choice](client); break; @@ -102,6 +108,7 @@ 6. Exit. var success = await InputDirectory(client); if (!success) { + Console.WriteLine("Failed to change directory."); return; } break; @@ -116,32 +123,107 @@ 6. Exit. private static async Task ShowLogFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var response = await client.GetLogFilesAsync(new Empty()); + Console.WriteLine("Log files:"); + foreach (var fileName in response.FileNames) + { + Console.WriteLine($" {fileName}"); + } } private static int ReadDegreeOfParallelism() { - throw new NotImplementedException("TODO: T3.2"); + int degreeOfParallelism = 0; + while (true) + { + Console.WriteLine("Please input degree of parallelism:"); + var input = Console.ReadLine()?.Trim(); + if (input is null) + { + return 1; + } + try + { + degreeOfParallelism = int.Parse(input); + if (degreeOfParallelism < 1 || degreeOfParallelism > 10) + { + Console.WriteLine("Invalid input, please try again."); + continue; + } + break; + } + catch (Exception) + { + Console.WriteLine("Invalid input, please try again."); + continue; + } + } + return degreeOfParallelism; } private static List ReadFileNames() { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Please input log file names, separated by space:"); + var input = Console.ReadLine()?.Trim(); + if (input is null) + { + return new List(); + } + return input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } private static async Task AnalyzeFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var request = new AnalyzeFilesRequest(); + + request.FileNames.AddRange(ReadFileNames()); + request.DegreeOfParallelism = ReadDegreeOfParallelism(); + Console.WriteLine("Analyzing..."); + var response = await client.AnalyzeFilesAsync(request); + Console.WriteLine($"Analysis result: {response.Status.Success}"); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + } } private static async Task AnalyzeAll(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + var request = new AnalyzeAllRequest() + { + DegreeOfParallelism = ReadDegreeOfParallelism(), + }; + Console.WriteLine("Analyzing..."); + var response = await client.AnalyzeAllAsync(request); + Console.WriteLine($"Analysis result: {response.Status.Success}"); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + } } private static async Task GetAnalysisResult(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + Console.WriteLine("Enter file name:"); + var fileName = Console.ReadLine(); + var request = new GetAnalysisResultRequest { FileName = fileName ?? string.Empty }; + using var call = client.GetAnalysisResult(request, cancellationToken: default); + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + switch (response.PayloadCase) + { + case GetAnalysisResultResponse.PayloadOneofCase.Header: + Console.WriteLine($"File: {response.Header.FileName}, State: {response.Header.State}, ErrorMessage: {response.Header.ErrorMessage ?? "N/A"}"); + break; + case GetAnalysisResultResponse.PayloadOneofCase.LogEntry: + Console.WriteLine($"Log Entry: {response.LogEntry}"); + break; + default: + Console.WriteLine("Unknown response type."); + break; + } + } + } } }