From ae21907638a1dde5d165cb6effcce1d9dea5db07 Mon Sep 17 00:00:00 2001 From: sek27 Date: Tue, 1 Sep 2026 10:54:41 +0800 Subject: [PATCH] feat(async-grpc): gRPC Agent service and RemoteCli client --- docs/01-basic/report.md | 95 ++++++ docs/02-multithreading/report.md | 59 ++++ docs/03-async-grpc/demo_log.txt | 287 ++++++++++++++++++ docs/03-async-grpc/report.md | 56 ++++ src/LocalCli/Program.cs | 75 ++++- src/LogAnalyzer/LogFileAnalyzer.cs | 58 +++- src/LogAnalyzer/WorkQueue.cs | 32 +- .../Applications/AgentSession.cs | 122 +++++++- src/LogAnalyzerAgent/Services/AgentService.cs | 14 +- src/LogAnalyzerRpc/GrpcLogEntryVisitor.cs | 30 +- src/LogAnalyzerRpc/GrpcTypeConverter.cs | 52 +++- src/LogParser/Models/LogEntries.cs | 4 +- src/LogParser/Parser/LineParser.cs | 41 ++- src/LogParser/Visitors/KeyValueVisitor.cs | 24 +- src/RemoteCli/Program.cs | 146 ++++++++- 15 files changed, 1053 insertions(+), 42 deletions(-) create mode 100644 docs/01-basic/report.md create mode 100644 docs/02-multithreading/report.md create mode 100644 docs/03-async-grpc/demo_log.txt create mode 100644 docs/03-async-grpc/report.md diff --git a/docs/01-basic/report.md b/docs/01-basic/report.md new file mode 100644 index 0000000..cb6362e --- /dev/null +++ b/docs/01-basic/report.md @@ -0,0 +1,95 @@ +# 01-basic 问答题报告 + +## (Q1.1) 代码框架分析 + +### 1. 哪条语句将日志按逗号分割?如何指定第几个字段代表何种意义? + +`LogFileParser.cs` 中的这一句把每行日志按逗号分割: + +```csharp +var fields = line.Split(','); +``` + +分割后按**位置约定**对应字段意义:`fields[0]` 是 `lineno`、`fields[1]` 是 `timestamp`、`fields[2]` 是 `pod-name`、`fields[3]` 是 `message`,随后用它们构造 `LogRecord`: + +```csharp +var logRecord = new LogRecord( + LineNo: int.Parse(fields[0]), + Timestamp: fields[1], + PodName: fields[2], + Message: Unescape(fields[3]) // message 内的 CSV 引号转义还原 +); +``` + +也就是说,字段含义不是写在数据里的,而是框架代码里"位置 → 属性名"的固定映射。 + +### 2. 在哪个方法内判断日志种类?用哪几条语句? + +在 `LineParser.ParseLine` 方法内。先用 `JsonDocument.Parse` 把 `message` 解析成只读 DOM,再用 `TryGetProperty("event", ...)` 取出 `event` 字段,然后用 `switch` 表达式按其值分派到 `CreateCall` / `CreateRequest` / `CreateInternal`: + +```csharp +using (var doc = JsonDocument.Parse(logRecord.Message)) +{ + var root = doc.RootElement; + 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(...) + }; + } +} +``` + +### 3. 确定日志种类后,调用了哪个库方法解析 JSON? + +调用 `System.Text.Json.JsonSerializer.Deserialize(json, options)`,把 message 反序列化为强类型的 `CallMessage` / `RequestMessage` / `InternalMessage` record。 + +**如何防止字段缺失?** 三个层面: + +1. record 的属性标注了 `[property: JsonRequired]`——缺失该键时 `Deserialize` 会抛 `JsonException`(测试 T1.2.5/T1.2.6 正是验证这一点); +2. record 属性是非可空类型(如 `int DurationMs`),值为 `null` 时同样失败; +3. 反序列化结果用 `?? throw new FormatException(...)` 兜底,防止整个 message 不是合法 JSON 对象时返回 `null`。 + +**命名法转换(kebab-case → 大驼峰)** 是通过共享的序列化选项完成的: + +```csharp +private static JsonSerializerOptions options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, +}; +``` + +`PropertyNamingPolicy = KebabCaseLower` 告诉序列化器:CLR 属性 `RequestId` 对应 JSON 键 `request-id`、`TargetService` 对应 `target-service`,反序列化时即完成双向映射。 + +## (Q1.2) Dump 的方法调用链 + +以 Call 事件为例,调用 `KeyValueVisitor.Dump(entry)` 后: + ++ `Dictionary KeyValueVisitor.Dump(LogEntry entry)` ++ `TResult LogEntry.Accept(ILogEntryVisitor visitor)` —— 实际派发到 `CallLogEntry.Accept` ++ `TResult CallLogEntry.Accept(ILogEntryVisitor visitor)` ++ `Dictionary KeyValueVisitor.Visit(CallLogEntry entry)` + +关键点:`Dump` 内只有一句 `entry.Accept(this)`。静态类型是 `LogEntry`,但 `Accept` 是虚方法(record 的 override),实际执行的是**运行时具体类型** `CallLogEntry.Accept`,它再回调 `visitor.Visit(this)`,此时 `this` 的静态类型已经是 `CallLogEntry`,于是重载决议选中 `Visit(CallLogEntry)` 重载。这就是访问者模式的"**双重分派**":第一次分派由 `Accept` 的虚机制完成(选具体 entry 类型),第二次由 `Visit` 的重载决议完成(选具体访问逻辑),数据结构与操作就此解耦。 + +## (Q1.3.b) AI 使用情况 + +本次作业我使用了 AI 辅助。 + +**提示词要点**:我向 AI 提供了仓库中 `LineParser.cs`、`LogEntries.cs`、`KeyValueVisitor.cs` 的完整源码与任务文档节选,要求"仿照 Call 类型的既有实现,补全 Request / Internal 两种日志的解析、Accept 与 Visit 实现,保持与 Call 完全一致的代码风格(JsonRequired record + switch 分派 + 同样的异常消息格式),不要改动无关代码"。 + +**AI 解答比我强的地方**: + +1. **速度**。三种类型的解析在结构上是同构的,AI 数秒内就给出了与参考实现风格一致的完整代码;若我自己查 `System.Text.Json` 文档里的 `JsonNamingPolicy` 枚举、`JsonRequired` 特性的确切用法,至少多花半小时。 +2. **API 记忆的准确性**。`JsonNamingPolicy.KebabCaseLower`、`[property: JsonRequired]` 这类 API 名字手写容易拼错或用成旧 API(如 ` camelCase` 策略),AI 一次给对。 + +**AI 不如我的地方**: + +1. **契约细节**。AI 初版对 Internal 日志的 `ExceptionName: message` 切分用了 `Split(':')`,没有意识到异常信息本身可能含冒号——正确做法是 `IndexOf(": ")` 只切第一个分隔符。这个 bug 是我对照测试样例 `InternalLogExampleFailed`(缺失冒号+空格的样例必须抛异常)时发现并改正的。 +2. **上下文一致性**。AI 生成的字典键(如 `"DurationMs"`)需要人工逐一对照讲义里的键表核对,它并不会主动去校验"文档约定了哪些键"这类跨文件契约。 + +结论:AI 适合干"同构复制 + API 检索"的活,而"跨文档契约核对 + 边界情况(分隔符歧义、失败样例)"仍必须靠人。 diff --git a/docs/02-multithreading/report.md b/docs/02-multithreading/report.md new file mode 100644 index 0000000..b28ef77 --- /dev/null +++ b/docs/02-multithreading/report.md @@ -0,0 +1,59 @@ +# 02-multithreading 报告 + +## 功能实现说明 + +### T2.1 WorkQueue(线程安全阻塞队列) + +用 `lock (_items)` + `Monitor` 条件变量实现生产者-消费者队列: + +- `Enqueue`:持锁入队后 `Monitor.Pulse` 唤醒一个等待的消费者;`CompleteAdding` 之后再入队抛 `InvalidOperationException`; +- `TryDequeue`:持锁后 **while** 循环检查(而非 if,防虚假唤醒):队列空且未完成 → `Monitor.Wait` 挂起等待;队列空且已完成 → 返回 false;否则出队返回; +- `CompleteAdding`:置 `_isCompleted = true` 后 `Monitor.PulseAll` 唤醒**所有**等待者,让它们重新检查条件并退出。 + +T2.1.3 用 8 线程 × 50000 条数据验证了 FIFO 语义与线程安全(每个生产者的序列在消费端仍严格递增)。 + +### T2.2 LogFileAnalyzer(多线程并行分析) + +- `AnalyzeFiles`:进入临界区校验文件名合法后置 `_isAnalyzing = true`(`finally` 中持锁复位,防异常泄漏状态); +- `RunWorkers`:持锁过滤出 `NotAnalyzed` 状态的文件(未知文件抛 `InvalidOperationException`)→ 全部入队后 `CompleteAdding` → 创建 `min(并行度, 文件数)` 个后台线程跑 `WorkerMain` → `Join` 等待全部结束; +- `WorkerMain`:循环 `TryDequeue` 领任务,`parser.Parse(reader).ToList()` 解析成功 → `Succeeded` 结果;任何异常被捕获 → `Failed` 结果(`ex.Message` 存入 `ErrorMessage`,不使 worker 崩溃);写回 `_analysisResults` 前持 `_syncRoot` 锁。 + +T2.2.5 验证了多线程对多文件分析的加速比。 + +### T2.3 LocalCli 控制台界面 + +实现了菜单 1-6 全部功能:列文件 / 指定文件分析 / 全部分析 / 查询结果(NotAnalyzed 提示未分析、Succeeded 用 `KeyValueVisitor.Dump` 逐条输出键值对、Failed 输出错误消息)/ 换目录 / 退出。所有用户输入路径都包了 try-catch(`ArgumentException`/`InvalidOperationException`),非法输入只提示不崩溃。 + +运行截图与鲁棒性测试截图见 `screenshots/` 目录(后续运行 LocalCli 后补充)。 + +## 问答题 + +### (Q2.1) 临界区与共享变量 + +**WorkQueue 的共享变量**:`_items`(`Queue`,队列本体)和 `_isCompleted`(完成标志)。二者都只在与 `_items` 互斥的临界区内读写——`Enqueue`/`TryDequeue`/`CompleteAdding`/`IsCompleted` 全部 `lock (_items)`。锁本身还兼任条件变量:消费者用 `Monitor.Wait(_items)` 挂起,生产者用 `Pulse`/`PulseAll` 唤醒,等待时自动放锁、被唤醒后重新竞争锁,因此不会死锁互斥量。 + +**LogFileAnalyzer 的共享变量**:`_currentDirectory`、`_isAnalyzing`、`_logFiles`、`_analysisResults` 四个字段,全部以 `_syncRoot`(私有 object)作互斥量保护。`AnalyzeAll` 先持锁拷贝文件名列表再放锁执行,`WorkerMain` 写结果时再短暂持锁,减小临界区粒度。 + +**if 改 while 的后果(虚假唤醒)**:无限容量生产者-消费者中,消费者伪码 `if (queue empty) wait(); dequeue();` 若被虚假唤醒(无人 signal 的情况下 `wait` 返回),消费者会**跳过重新检查**直接执行 `dequeue()`——此时队列可能仍为空,导致从空队列取元素(返回错误数据或抛异常);若用 `while (queue empty) wait();`,虚假唤醒后条件重新判定仍为真,再次进入等待,逻辑不受影响。这就是条件变量必须配合循环使用的原因。 + +### (Q2.2) 目录扫描代码 + +扫描全部 `.log` 文件的是 `ChangeDirectory` 中的: + +```csharp +var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly) + .Select(filePath => Path.GetFileName(filePath)) + .OrderBy(fileName => fileName); +``` + +若要递归扫描所有子目录,把 `SearchOption.TopDirectoryOnly` 改为 `SearchOption.AllDirectories` 即可(`.NET 8+` 也可用 `EnumerationOptions` 控制忽略权限错误等细节)。 + +### (Q2.3.b) AI 使用情况 + +**提示词**:向 AI 提供了 WorkQueue/LogFileAnalyzer 的完整框架源码,要求"实现生产者-消费者语义的阻塞队列,TryDequeue 必须用 while 循环防虚假唤醒;RunWorkers 按 TODO 注释的语义补全,保持锁约定(_syncRoot 保护结果字典)"。 + +**使用方式**:介于"讲解框架"与"写部分代码"之间——锁与条件变量的语义是我先理解的,AI 主要负责把语义翻译成符合框架风格的 C# 代码。 + +**AI 的错误**:一次生成中 `TryDequeue` 用了 `if`,被我按 Q2.1 同样的虚假唤醒理由要求改成 `while`;另一次在 `RunWorkers` 过滤文件时漏了 `CompleteAdding()`(消费者会在空队列上永久 Wait),对照 T2.2 测试挂起超时的现象定位后补上。 + +**难度评价**:适中偏上。C# 的 `lock`/`Monitor` 语义与操作系统课的互斥量/条件变量一一对应,理论不新;难在并发 bug 不可复现,必须靠测试压力验证(T2.2.5 跑了 54 秒)。 diff --git a/docs/03-async-grpc/demo_log.txt b/docs/03-async-grpc/demo_log.txt new file mode 100644 index 0000000..3af09a2 --- /dev/null +++ b/docs/03-async-grpc/demo_log.txt @@ -0,0 +1,287 @@ +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. +>>> Please input directory containing log files: + +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. +>>> Log files in agent's current directory: + 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. +>>> Please input degree of parallelism (0 = auto): +Please input file names separated by commas: +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. +>>> Please input the file name: +--- basic.log (C:\Users\32797\shupei_homework\dotnet-workshop\src\dataset\basic.log) --- +State: Succeeded, Worker: 0 +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. +>>> Please input degree of parallelism (0 = auto): +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. +>>> Please input the file name: +--- basic-multiple.log (C:\Users\32797\shupei_homework\dotnet-workshop\src\dataset\basic-multiple.log) --- +State: Succeeded, Worker: 1 +LineNo=0, Timestamp=2026-06-05T16:00:01.8930000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=df965964-f6cc-436b-8d36-fd801165b7b5, TargetService=authservice, DurationMs=252 +LineNo=1, Timestamp=2026-06-05T16:00:02.0690000+00:00, PodName=authservice-2, Severity=Info, EventType=Request, RequestId=56736c9e-74cd-4f2e-9390-e10cd02d650c, Method=PUT, Path=/api/content/notice, StatusCode=204 +LineNo=2, Timestamp=2026-06-05T16:00:03.2360000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=c4f22ad2-09ec-438b-87ba-a7cb4132bcd0, Method=POST, Path=/api/content/notice, StatusCode=204 +LineNo=3, Timestamp=2026-06-05T16:00:05.0460000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=368627ab-8332-4516-b257-4143336c13ea, TargetService=authservice, DurationMs=163 +LineNo=4, Timestamp=2026-06-05T16:00:05.4980000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=cee75d0a-2b96-4dbe-9e47-3d7df256ef50, TargetService=authservice, DurationMs=200 +LineNo=5, Timestamp=2026-06-05T16:00:06.7640000+00:00, PodName=emailservice-0, Severity=Info, EventType=Request, RequestId=f9774944-04d4-47f5-a6cd-e0c9cbc7e794, Method=GET, Path=/api/auth/login, StatusCode=400 +LineNo=6, Timestamp=2026-06-05T16:00:08.4310000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=d27c9a01-8256-411d-b03d-6ae23f96c01d, TargetService=emailservice, DurationMs=138 +LineNo=7, Timestamp=2026-06-05T16:00:09.7540000+00:00, PodName=contentservice-0, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=8, Timestamp=2026-06-05T16:00:11.2060000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=7fd16c15-ab3e-432b-abe8-b02f8484aa63, Method=DELETE, Path=/api/content/notice, StatusCode=204 +LineNo=9, Timestamp=2026-06-05T16:00:11.7580000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=d7fdcc2b-dd7a-4368-bf18-74c9abeb04f2, Method=POST, Path=/api/content/notice, StatusCode=403 +LineNo=10, Timestamp=2026-06-05T16:00:12.8070000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=f55eb692-8fa1-4f03-9920-411b8529f43a, Method=DELETE, Path=/api/auth/login, StatusCode=400 +LineNo=11, Timestamp=2026-06-05T16:00:13.0640000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=19ec1dec-bdc6-4e60-b411-936fb3e8e585, Method=POST, Path=/api/user/alice, StatusCode=200 +LineNo=12, Timestamp=2026-06-05T16:00:13.3420000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=9a11693d-7d46-4998-a82f-83fadbaa794e, TargetService=authservice, DurationMs=141 +LineNo=13, Timestamp=2026-06-05T16:00:14.7970000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=1e0ec342-c512-4289-aafd-77fdd6c96434, TargetService=emailservice, DurationMs=22 +LineNo=14, Timestamp=2026-06-05T16:00:16.3940000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=2f6eb8a8-ef42-4293-be33-cc60b96832ab, TargetService=userservice, DurationMs=252 +LineNo=15, Timestamp=2026-06-05T16:00:17.7180000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=5e7c1d8d-5fae-4d33-b648-aa144f775ebf, Method=GET, Path=/api/auth/login, StatusCode=201 +LineNo=16, Timestamp=2026-06-05T16:00:17.9700000+00:00, PodName=emailservice-1, Severity=Info, EventType=Request, RequestId=a7c3478e-db47-4ef0-bc0a-f95522041176, Method=POST, Path=/api/content/notice, StatusCode=401 +LineNo=17, Timestamp=2026-06-05T16:00:18.7800000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=5b8d62cb-13ac-414f-87cf-f63279f1b5e9, TargetService=emailservice, DurationMs=118 +LineNo=18, Timestamp=2026-06-05T16:00:20.0920000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=dd4349da-fe64-4552-8c21-dcc0b0ddfe11, TargetService=emailservice, DurationMs=61 +LineNo=19, Timestamp=2026-06-05T16:00:21.8320000+00:00, PodName=emailservice-0, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=20, Timestamp=2026-06-05T16:00:22.8720000+00:00, PodName=userservice-1, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=21, Timestamp=2026-06-05T16:00:24.8230000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=cac79c41-b0d3-44e2-9417-a1a759fbdf93, Method=POST, Path=/api/content/article, StatusCode=401 +LineNo=22, Timestamp=2026-06-05T16:00:24.8430000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=74c39bbc-86cb-4606-a42a-3f326cb53402, TargetService=userservice, DurationMs=46 +LineNo=23, Timestamp=2026-06-05T16:00:26.0890000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=c1ee803d-93b4-4649-b70d-a866cae8e3a8, Method=POST, Path=/api/user/john, StatusCode=200 +LineNo=24, Timestamp=2026-06-05T16:00:27.2810000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=de0b2277-89c7-41e3-a0db-428420948d1b, TargetService=authservice, DurationMs=284 +LineNo=25, Timestamp=2026-06-05T16:00:27.7250000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=bf6785aa-a18a-46ea-85d1-87c5924fda2b, Method=GET, Path=/api/content/notice, StatusCode=400 +LineNo=26, Timestamp=2026-06-05T16:00:28.7440000+00:00, PodName=emailservice-2, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=27, Timestamp=2026-06-05T16:00:30.2010000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=0d72a4bd-db16-4037-ae6d-691ba473daac, Method=PUT, Path=/api/content/notice, StatusCode=201 +LineNo=28, Timestamp=2026-06-05T16:00:30.4960000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=a232f6a4-5304-4c7d-9ebb-bd299c28b02b, TargetService=emailservice, DurationMs=277 +LineNo=29, Timestamp=2026-06-05T16:00:30.9900000+00:00, PodName=gateway-1, Severity=Info, EventType=Request, RequestId=830e7c2f-b01e-420d-b9ba-9203d78c8d28, Method=PUT, Path=/api/user/john, StatusCode=200 +LineNo=30, Timestamp=2026-06-05T16:00:32.9540000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=345118ff-f5a2-4d26-ba79-8759a65361a8, Method=GET, Path=/api/content/notice, StatusCode=403 +LineNo=31, Timestamp=2026-06-05T16:00:33.4140000+00:00, PodName=userservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.IO.IOException, ExceptionMessage=Temporary file cache is unavailable. +LineNo=32, Timestamp=2026-06-05T16:00:34.5480000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=87abfd62-fd7f-430f-bb50-b25a36d0b3ae, TargetService=contentservice, DurationMs=6 +LineNo=33, Timestamp=2026-06-05T16:00:36.3900000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=ea0474db-ac3f-497a-ad21-ca7a6a56bf1f, TargetService=emailservice, DurationMs=122 +LineNo=34, Timestamp=2026-06-05T16:00:38.2710000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=0e985429-3be6-4254-b274-c07f2c92bfd2, Method=DELETE, Path=/api/auth/logout, StatusCode=200 +LineNo=35, Timestamp=2026-06-05T16:00:39.9710000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=5ca5b109-a9fd-4d53-970e-276cfb2201cd, TargetService=emailservice, DurationMs=157 +LineNo=36, Timestamp=2026-06-05T16:00:40.3010000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=0287909d-ced4-497a-850b-a57c3702d28b, Method=DELETE, Path=/api/auth/logout, StatusCode=204 +LineNo=37, Timestamp=2026-06-05T16:00:40.8160000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=238499f2-6234-402a-a0b1-e37120ab3830, Method=GET, Path=/api/content/notice, StatusCode=404 +LineNo=38, Timestamp=2026-06-05T16:00:41.8290000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=b447b6a1-5996-48f1-8131-0348e9b4e74d, Method=DELETE, Path=/api/auth/login, StatusCode=200 +LineNo=39, Timestamp=2026-06-05T16:00:43.1240000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=a59f0440-81ec-4944-8bdd-f88bcd4a71b1, TargetService=emailservice, DurationMs=290 +LineNo=40, Timestamp=2026-06-05T16:00:44.5830000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=e3d7fd39-3763-4777-a2bb-4036d78d72d4, TargetService=emailservice, DurationMs=2 +LineNo=41, Timestamp=2026-06-05T16:00:45.3770000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=9c1f0cb1-7080-4158-8350-643250d29714, Method=DELETE, Path=/api/content/notice, StatusCode=204 +LineNo=42, Timestamp=2026-06-05T16:00:45.6870000+00:00, PodName=userservice-1, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=43, Timestamp=2026-06-05T16:00:45.7850000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=cc0585b2-2846-41ed-8ed2-322afb9530e5, TargetService=emailservice, DurationMs=33 +LineNo=44, Timestamp=2026-06-05T16:00:46.5140000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=1a0753e8-0757-4156-b762-46b8b238580e, Method=DELETE, Path=/api/user/john, StatusCode=201 +LineNo=45, Timestamp=2026-06-05T16:00:48.3400000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=2dda046c-4a5e-4d4d-81d2-32d6304d1f48, Method=POST, Path=/api/auth/login, StatusCode=201 +LineNo=46, Timestamp=2026-06-05T16:00:49.5210000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=0bc54fc8-dbf8-4124-83cf-8165e268fe5e, TargetService=authservice, DurationMs=195 +LineNo=47, Timestamp=2026-06-05T16:00:51.4350000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=74724759-7f6e-4e11-ae43-d9a3edf93030, TargetService=emailservice, DurationMs=112 +LineNo=48, Timestamp=2026-06-05T16:00:52.6370000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=57e05d14-cbee-4a3c-82c7-02232af68372, TargetService=userservice, DurationMs=238 +LineNo=49, Timestamp=2026-06-05T16:00:52.7590000+00:00, PodName=gateway-1, Severity=Warning, EventType=Internal, ExceptionName=System.IO.IOException, ExceptionMessage=Temporary file cache is unavailable. +LineNo=50, Timestamp=2026-06-05T16:00:54.4090000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=97d3ab71-1be5-4eae-9287-4918aadcf605, TargetService=emailservice, DurationMs=173 +LineNo=51, Timestamp=2026-06-05T16:00:55.8370000+00:00, PodName=gateway-1, Severity=Error, EventType=Internal, ExceptionName=System.NullReferenceException, ExceptionMessage=Object reference not set to an instance of an object. +LineNo=52, Timestamp=2026-06-05T16:00:55.8750000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=80b89989-2b12-4909-a41b-e297daeb1ee8, Method=PUT, Path=/api/auth/login, StatusCode=201 +LineNo=53, Timestamp=2026-06-05T16:00:56.3600000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=ee1eda2d-3fba-4054-a949-f3ad72346ef9, TargetService=emailservice, DurationMs=264 +LineNo=54, Timestamp=2026-06-05T16:00:58.1920000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=be48fdc2-9204-4d3e-a338-cd47cdba1f47, TargetService=emailservice, DurationMs=17 +LineNo=55, Timestamp=2026-06-05T16:00:59.9750000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=f8dd7a34-1d8c-4766-af49-17693e59f0fc, Method=GET, Path=/api/auth/login, StatusCode=204 +LineNo=56, Timestamp=2026-06-05T16:01:01.1920000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=292de4e5-ccc7-4df6-8957-831bd38c2209, TargetService=emailservice, DurationMs=172 +LineNo=57, Timestamp=2026-06-05T16:01:02.4830000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=393fce6b-f1ac-4647-9781-10b90fa4fd0a, TargetService=emailservice, DurationMs=76 +LineNo=58, Timestamp=2026-06-05T16:01:03.4260000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=dc922960-ff8b-40df-989e-ec3257875bc3, TargetService=contentservice, DurationMs=41 +LineNo=59, Timestamp=2026-06-05T16:01:04.2390000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=43e3b5de-d934-41fd-b744-89da5b5798d0, TargetService=emailservice, DurationMs=210 +LineNo=60, Timestamp=2026-06-05T16:01:04.2780000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=a4c49d95-40d4-4814-94dd-99b01ed4ffab, Method=DELETE, Path=/api/content/article, StatusCode=403 +LineNo=61, Timestamp=2026-06-05T16:01:04.9700000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=37cd559e-57c8-4603-873f-1ffc9c1fd266, TargetService=emailservice, DurationMs=133 +LineNo=62, Timestamp=2026-06-05T16:01:06.8160000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=347bc482-a9a1-4f50-ade3-988ebeed8be8, TargetService=authservice, DurationMs=92 +LineNo=63, Timestamp=2026-06-05T16:01:07.7610000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=2b5f2ccc-b6cf-44c6-a2ba-1985e762c5d4, Method=PUT, Path=/api/auth/login, StatusCode=200 +LineNo=64, Timestamp=2026-06-05T16:01:07.8780000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=e131edb3-3704-4c50-9ac7-609211ba011b, Method=PUT, Path=/api/user/alice, StatusCode=200 +LineNo=65, Timestamp=2026-06-05T16:01:09.2440000+00:00, PodName=emailservice-2, Severity=Warning, EventType=Internal, ExceptionName=System.Net.Http.HttpRequestException, ExceptionMessage=Remote service responded slowly. +LineNo=66, Timestamp=2026-06-05T16:01:10.8280000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=0e403347-d5e2-4377-881b-e2e21c963708, TargetService=authservice, DurationMs=219 +LineNo=67, Timestamp=2026-06-05T16:01:12.6980000+00:00, PodName=authservice-0, Severity=Warning, EventType=Internal, ExceptionName=System.TimeoutException, ExceptionMessage=Request processing is slower than expected. +LineNo=68, Timestamp=2026-06-05T16:01:13.4070000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=05e4c006-314a-40b6-8002-42c1431ac3f9, Method=POST, Path=/api/auth/logout, StatusCode=200 +LineNo=69, Timestamp=2026-06-05T16:01:13.7880000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=55a4778d-9a47-4de9-8b1d-0d1737244d43, Method=GET, Path=/api/user/john, StatusCode=204 +LineNo=70, Timestamp=2026-06-05T16:01:13.9740000+00:00, PodName=authservice-2, Severity=Info, EventType=Request, RequestId=fbde4da7-4597-454a-b92d-d5cabb916567, Method=PUT, Path=/api/content/article, StatusCode=404 +LineNo=71, Timestamp=2026-06-05T16:01:15.3290000+00:00, PodName=contentservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.IO.IOException, ExceptionMessage=Temporary file cache is unavailable. +LineNo=72, Timestamp=2026-06-05T16:01:15.5560000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=76f802df-07a0-4976-a15a-1bed691c2d6b, TargetService=contentservice, DurationMs=136 +LineNo=73, Timestamp=2026-06-05T16:01:17.5100000+00:00, PodName=authservice-0, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=74, Timestamp=2026-06-05T16:01:18.3640000+00:00, PodName=contentservice-0, Severity=Warning, EventType=Internal, ExceptionName=System.IO.IOException, ExceptionMessage=Temporary file cache is unavailable. +LineNo=75, Timestamp=2026-06-05T16:01:20.2690000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=ead1ab6c-89d1-4362-b1d4-2dab6ec3ea4d, Method=GET, Path=/api/auth/logout, StatusCode=401 +LineNo=76, Timestamp=2026-06-05T16:01:22.1350000+00:00, PodName=userservice-1, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=77, Timestamp=2026-06-05T16:01:23.4050000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=41cb66ad-a5ad-4037-a3ff-925e75e477b3, TargetService=userservice, DurationMs=104 +LineNo=78, Timestamp=2026-06-05T16:01:23.6000000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=bc1d1774-cc98-4c30-b7c0-f236aab761c7, Method=POST, Path=/api/user/john, StatusCode=401 +LineNo=79, Timestamp=2026-06-05T16:01:23.6730000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=2d6345ef-e614-48ba-8a6e-4ab697150ef7, TargetService=authservice, DurationMs=179 +LineNo=80, Timestamp=2026-06-05T16:01:24.8120000+00:00, PodName=userservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.TimeoutException, ExceptionMessage=Request processing is slower than expected. +LineNo=81, Timestamp=2026-06-05T16:01:26.0060000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=ce7dc6ce-9174-4e2e-9fd5-886bff0938e9, Method=DELETE, Path=/api/user/john, StatusCode=201 +LineNo=82, Timestamp=2026-06-05T16:01:27.9510000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=82d83dc7-488a-4e3c-beb0-02b0b125a31c, TargetService=emailservice, DurationMs=36 +LineNo=83, Timestamp=2026-06-05T16:01:28.3510000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=d9ab3377-04a0-42fe-ad56-b21333e41f0f, Method=DELETE, Path=/api/user/alice, StatusCode=200 +LineNo=84, Timestamp=2026-06-05T16:01:29.4260000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=d0d65640-5dfd-4c6e-99e8-89e4acd105a4, Method=DELETE, Path=/api/auth/login, StatusCode=400 +LineNo=85, Timestamp=2026-06-05T16:01:31.0070000+00:00, PodName=emailservice-1, Severity=Info, EventType=Request, RequestId=fe414756-3031-4e4a-b71a-7adb19e05902, Method=POST, Path=/api/auth/logout, StatusCode=500 +LineNo=86, Timestamp=2026-06-05T16:01:31.5410000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=d816ba87-ec84-4f22-89b9-482b71ba4aed, TargetService=contentservice, DurationMs=22 +LineNo=87, Timestamp=2026-06-05T16:01:32.7730000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=77a76b1d-4018-4848-8c87-bd3f55e0acd4, Method=GET, Path=/api/content/article, StatusCode=400 +LineNo=88, Timestamp=2026-06-05T16:01:32.9820000+00:00, PodName=userservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.IO.IOException, ExceptionMessage=Temporary file cache is unavailable. +LineNo=89, Timestamp=2026-06-05T16:01:34.0030000+00:00, PodName=gateway-0, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=90, Timestamp=2026-06-05T16:01:35.9900000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=bbe1ff34-f95c-4d7c-aba8-3a8fa45cc600, TargetService=emailservice, DurationMs=297 +LineNo=91, Timestamp=2026-06-05T16:01:36.1760000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=7f6d17fe-146a-43e6-9132-256a268f2dea, TargetService=emailservice, DurationMs=197 +LineNo=92, Timestamp=2026-06-05T16:01:37.9440000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=b9e16a6a-dd67-4c39-92bd-da6f97d47f43, Method=GET, Path=/api/content/article, StatusCode=200 +LineNo=93, Timestamp=2026-06-05T16:01:38.4020000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=73160e8d-54ab-48f7-9ac8-2a52ebc58ab1, Method=POST, Path=/api/auth/login, StatusCode=404 +LineNo=94, Timestamp=2026-06-05T16:01:39.6270000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=a87148d6-71f0-4468-90d9-f7b7cb55062c, TargetService=emailservice, DurationMs=126 +LineNo=95, Timestamp=2026-06-05T16:01:40.3460000+00:00, PodName=contentservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.TimeoutException, ExceptionMessage=Request processing is slower than expected. +LineNo=96, Timestamp=2026-06-05T16:01:41.4140000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=22fce50d-0dc2-4f9e-b16d-1a5e98a94a41, TargetService=contentservice, DurationMs=15 +LineNo=97, Timestamp=2026-06-05T16:01:41.8900000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=32b03e12-d2d1-43c8-a02a-fdaad09ab5cb, Method=DELETE, Path=/api/auth/login, StatusCode=400 +LineNo=98, Timestamp=2026-06-05T16:01:43.1040000+00:00, PodName=emailservice-1, Severity=Error, EventType=Internal, ExceptionName=System.InvalidOperationException, ExceptionMessage=Failed to load service configuration. +LineNo=99, Timestamp=2026-06-05T16:01:43.1300000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=2d54deaf-88f0-4f94-b014-395c4cb8c243, TargetService=authservice, DurationMs=42 +LineNo=100, Timestamp=2026-06-05T16:01:44.1250000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=d3c41c66-fa7a-48c6-a000-162d1eccce4f, Method=POST, Path=/api/auth/logout, StatusCode=403 +LineNo=101, Timestamp=2026-06-05T16:01:44.2420000+00:00, PodName=gateway-0, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=102, Timestamp=2026-06-05T16:01:44.2850000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=b13f7092-4f4b-4f5b-8d36-ca35300d49b4, Method=POST, Path=/api/content/notice, StatusCode=401 +LineNo=103, Timestamp=2026-06-05T16:01:46.1610000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=e3091d72-98e0-4405-a7a4-4d5aa2d120cf, Method=PUT, Path=/api/auth/logout, StatusCode=500 +LineNo=104, Timestamp=2026-06-05T16:01:46.2600000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=d4d8b975-b7e2-4a90-b525-54f099f9ab6f, Method=PUT, Path=/api/auth/login, StatusCode=500 +LineNo=105, Timestamp=2026-06-05T16:01:47.5380000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=08d06e88-83e2-4810-b449-37f9138fe483, Method=PUT, Path=/api/user/john, StatusCode=204 +LineNo=106, Timestamp=2026-06-05T16:01:48.4010000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=9f74b6ff-8052-444e-be1e-2aa8422e062d, Method=PUT, Path=/api/content/article, StatusCode=204 +LineNo=107, Timestamp=2026-06-05T16:01:48.6230000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=4a2f55ad-002a-447c-b41c-bcf568afa94d, Method=POST, Path=/api/content/article, StatusCode=201 +LineNo=108, Timestamp=2026-06-05T16:01:49.2320000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=c90f3a9c-9472-4f8a-978c-060f2109394f, TargetService=contentservice, DurationMs=113 +LineNo=109, Timestamp=2026-06-05T16:01:50.1910000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=0c584342-947e-4811-bd06-66056cae0681, Method=POST, Path=/api/user/alice, StatusCode=204 +LineNo=110, Timestamp=2026-06-05T16:01:51.9810000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=c6c39af0-dca0-4dde-a99e-ce4b7cc266d1, Method=PUT, Path=/api/auth/login, StatusCode=201 +LineNo=111, Timestamp=2026-06-05T16:01:52.4110000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=6affd1cd-00e2-4eb4-8510-b278be4f5c4a, TargetService=emailservice, DurationMs=185 +LineNo=112, Timestamp=2026-06-05T16:01:54.2440000+00:00, PodName=contentservice-1, Severity=Error, EventType=Internal, ExceptionName=System.NullReferenceException, ExceptionMessage=Object reference not set to an instance of an object. +LineNo=113, Timestamp=2026-06-05T16:01:56.2380000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=c568f6dc-86a5-4641-b747-2d5c0ed795e3, Method=POST, Path=/api/auth/login, StatusCode=200 +LineNo=114, Timestamp=2026-06-05T16:01:56.5080000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=452491dc-e9c5-452f-9dab-9061118b9df6, Method=GET, Path=/api/user/john, StatusCode=200 +LineNo=115, Timestamp=2026-06-05T16:01:56.5980000+00:00, PodName=authservice-2, Severity=Error, EventType=Internal, ExceptionName=System.InvalidOperationException, ExceptionMessage=Failed to load service configuration. +LineNo=116, Timestamp=2026-06-05T16:01:57.1580000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=b7ccb829-6d86-42d2-b2e2-4cd521a23c21, TargetService=contentservice, DurationMs=14 +LineNo=117, Timestamp=2026-06-05T16:01:58.9710000+00:00, PodName=gateway-1, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=118, Timestamp=2026-06-05T16:02:00.5640000+00:00, PodName=userservice-1, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=119, Timestamp=2026-06-05T16:02:01.7270000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=56ea6042-0a22-4599-a034-5c7c0ed22220, Method=DELETE, Path=/api/user/john, StatusCode=400 +LineNo=120, Timestamp=2026-06-05T16:02:01.8010000+00:00, PodName=emailservice-1, Severity=Info, EventType=Request, RequestId=2400deab-b4b4-4785-b819-97d583d5ea85, Method=PUT, Path=/api/content/article, StatusCode=200 +LineNo=121, Timestamp=2026-06-05T16:02:02.3670000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=6717d311-0b36-41cf-9e4a-54f9bddb5e9f, TargetService=emailservice, DurationMs=32 +LineNo=122, Timestamp=2026-06-05T16:02:04.3170000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=964cf912-de28-40c5-a2c3-9063524dbb77, TargetService=emailservice, DurationMs=120 +LineNo=123, Timestamp=2026-06-05T16:02:04.7920000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=3fa1fde9-7f92-4b20-8004-01695306404d, Method=DELETE, Path=/api/user/alice, StatusCode=201 +LineNo=124, Timestamp=2026-06-05T16:02:05.2110000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=194ee425-0851-4760-93b8-dd48e8922164, Method=GET, Path=/api/auth/login, StatusCode=404 +LineNo=125, Timestamp=2026-06-05T16:02:05.4200000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=33cb8832-fe05-4ba4-9cd7-5c93987f48c6, TargetService=emailservice, DurationMs=83 +LineNo=126, Timestamp=2026-06-05T16:02:05.6280000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=d7c53de9-a93e-4cf1-85c5-0efbd26b6c41, Method=DELETE, Path=/api/user/alice, StatusCode=201 +LineNo=127, Timestamp=2026-06-05T16:02:06.9610000+00:00, PodName=gateway-1, Severity=Info, EventType=Request, RequestId=5d4ee5c7-9bcb-41f8-ace1-5ed0daa09f6b, Method=DELETE, Path=/api/content/article, StatusCode=500 +LineNo=128, Timestamp=2026-06-05T16:02:07.2790000+00:00, PodName=emailservice-0, Severity=Error, EventType=Internal, ExceptionName=System.InvalidOperationException, ExceptionMessage=Failed to load service configuration. +LineNo=129, Timestamp=2026-06-05T16:02:09.2580000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=900a0fb9-6313-415b-941c-41374087ecd1, Method=PUT, Path=/api/auth/login, StatusCode=200 +LineNo=130, Timestamp=2026-06-05T16:02:10.1370000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=ffd4546e-d5c4-41f6-9726-2327ad16c0af, TargetService=emailservice, DurationMs=164 +LineNo=131, Timestamp=2026-06-05T16:02:10.6020000+00:00, PodName=authservice-0, Severity=Info, EventType=Request, RequestId=49bbf962-6a9d-418c-852e-ee65faa90a6c, Method=POST, Path=/api/user/john, StatusCode=401 +LineNo=132, Timestamp=2026-06-05T16:02:11.4760000+00:00, PodName=userservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.IO.IOException, ExceptionMessage=Temporary file cache is unavailable. +LineNo=133, Timestamp=2026-06-05T16:02:12.4570000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=c1bd7360-51d0-40cd-b627-8fad6f3d4c90, TargetService=authservice, DurationMs=300 +LineNo=134, Timestamp=2026-06-05T16:02:14.0110000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=a4b69b9a-af29-4248-ad63-0b5cac03db04, Method=GET, Path=/api/user/john, StatusCode=403 +LineNo=135, Timestamp=2026-06-05T16:02:15.4620000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=4b4ec7ee-df97-4302-9d10-ea3c1464f89f, TargetService=contentservice, DurationMs=241 +LineNo=136, Timestamp=2026-06-05T16:02:16.0310000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=e4bd5a1c-da93-49ae-b81f-39d51f0b1fd0, Method=DELETE, Path=/api/content/article, StatusCode=200 +LineNo=137, Timestamp=2026-06-05T16:02:16.5330000+00:00, PodName=gateway-1, Severity=Info, EventType=Request, RequestId=618e2c1d-c247-47b2-b565-4789235c20ee, Method=POST, Path=/api/content/notice, StatusCode=403 +LineNo=138, Timestamp=2026-06-05T16:02:16.9370000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=8df4362b-07fc-4e2b-bc37-33d96e414cf9, TargetService=authservice, DurationMs=155 +LineNo=139, Timestamp=2026-06-05T16:02:17.7820000+00:00, PodName=gateway-1, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=140, Timestamp=2026-06-05T16:02:19.5380000+00:00, PodName=emailservice-0, Severity=Info, EventType=Request, RequestId=d07f0ae0-bf74-4ede-b435-6cbfd5d02408, Method=PUT, Path=/api/user/alice, StatusCode=401 +LineNo=141, Timestamp=2026-06-05T16:02:19.8400000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=bb1a8bb0-4328-4e8c-ad67-a1b4e64f3498, Method=GET, Path=/api/auth/login, StatusCode=404 +LineNo=142, Timestamp=2026-06-05T16:02:21.2980000+00:00, PodName=authservice-2, Severity=Error, EventType=Internal, ExceptionName=System.InvalidOperationException, ExceptionMessage=Failed to load service configuration. +LineNo=143, Timestamp=2026-06-05T16:02:21.4670000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=090d770c-9d92-4ff5-82a7-d6ea1601d03f, Method=POST, Path=/api/content/article, StatusCode=401 +LineNo=144, Timestamp=2026-06-05T16:02:21.4950000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=bb471c8a-a987-4579-9c0f-1d1cd3b6df0a, TargetService=emailservice, DurationMs=226 +LineNo=145, Timestamp=2026-06-05T16:02:22.4040000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=5aedc419-f08e-4af6-bd19-eb442a70ad47, Method=PUT, Path=/api/user/alice, StatusCode=200 +LineNo=146, Timestamp=2026-06-05T16:02:24.1190000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=cedf7de3-0f5d-4021-bcf0-a231f049b2a0, TargetService=emailservice, DurationMs=290 +LineNo=147, Timestamp=2026-06-05T16:02:24.8390000+00:00, PodName=authservice-1, Severity=Warning, EventType=Internal, ExceptionName=System.Net.Http.HttpRequestException, ExceptionMessage=Remote service responded slowly. +LineNo=148, Timestamp=2026-06-05T16:02:26.8260000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=1c4aae9a-1a8f-412a-86d4-d2c9f52c16a8, TargetService=authservice, DurationMs=292 +LineNo=149, Timestamp=2026-06-05T16:02:28.5660000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=012f1330-bba4-42ca-a868-f4bf5f812eed, TargetService=authservice, DurationMs=41 +LineNo=150, Timestamp=2026-06-05T16:02:28.8320000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=0c1e4597-509e-464e-abc6-1a42060db473, TargetService=emailservice, DurationMs=155 +LineNo=151, Timestamp=2026-06-05T16:02:29.7440000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=f705b92c-a500-48ae-bfa5-6078d305e07a, Method=DELETE, Path=/api/auth/login, StatusCode=200 +LineNo=152, Timestamp=2026-06-05T16:02:31.3980000+00:00, PodName=userservice-0, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=153, Timestamp=2026-06-05T16:02:32.1250000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=522e8241-ba0f-4557-962b-ee5924528805, TargetService=userservice, DurationMs=253 +LineNo=154, Timestamp=2026-06-05T16:02:33.2190000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=ee3772af-f7ce-4a66-9ecd-a3bd4f0ae3c4, TargetService=userservice, DurationMs=222 +LineNo=155, Timestamp=2026-06-05T16:02:33.2960000+00:00, PodName=userservice-1, Severity=Info, EventType=Request, RequestId=b4d3a6a3-3b02-4c21-b782-8822b9fd7d3a, Method=GET, Path=/api/user/alice, StatusCode=403 +LineNo=156, Timestamp=2026-06-05T16:02:34.8020000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=ee189239-39e0-4360-963a-a8be512fd1da, TargetService=emailservice, DurationMs=245 +LineNo=157, Timestamp=2026-06-05T16:02:35.9330000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=9f7c93fe-2046-4f92-8cbb-d1d0ed837133, TargetService=authservice, DurationMs=196 +LineNo=158, Timestamp=2026-06-05T16:02:36.1660000+00:00, PodName=emailservice-2, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=159, Timestamp=2026-06-05T16:02:38.0240000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=99dd1228-3250-4cce-9221-429192e65c80, TargetService=emailservice, DurationMs=295 +LineNo=160, Timestamp=2026-06-05T16:02:38.1530000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=b9d95fdd-9c6d-4203-ab4d-9f83fa754613, Method=POST, Path=/api/user/john, StatusCode=200 +LineNo=161, Timestamp=2026-06-05T16:02:38.1890000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=ba997768-3e2c-40d4-82e8-f732524645ee, TargetService=emailservice, DurationMs=246 +LineNo=162, Timestamp=2026-06-05T16:02:39.3580000+00:00, PodName=userservice-0, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=163, Timestamp=2026-06-05T16:02:40.0550000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=0d5399b6-c48f-4470-b518-e50db21d72d6, TargetService=emailservice, DurationMs=121 +LineNo=164, Timestamp=2026-06-05T16:02:40.9710000+00:00, PodName=contentservice-0, Severity=Info, EventType=Request, RequestId=5c7f63ff-729d-4e95-b646-85b2ff606f67, Method=GET, Path=/api/user/john, StatusCode=401 +LineNo=165, Timestamp=2026-06-05T16:02:42.5290000+00:00, PodName=emailservice-2, Severity=Info, EventType=Request, RequestId=6dd21018-0e8c-4579-9f07-c8d32a892850, Method=GET, Path=/api/auth/login, StatusCode=404 +LineNo=166, Timestamp=2026-06-05T16:02:43.6340000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=9fe8ad70-a323-44bd-be0d-40914f971539, TargetService=authservice, DurationMs=171 +LineNo=167, Timestamp=2026-06-05T16:02:44.6170000+00:00, PodName=userservice-1, Severity=Info, EventType=Call, RequestId=cac8474d-67e2-47fd-bfdf-e00c01c3fe9a, TargetService=emailservice, DurationMs=38 +LineNo=168, Timestamp=2026-06-05T16:02:45.7540000+00:00, PodName=gateway-1, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=169, Timestamp=2026-06-05T16:02:47.0510000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=092bc18b-90c6-42c6-aac9-dfdd11132619, TargetService=emailservice, DurationMs=227 +LineNo=170, Timestamp=2026-06-05T16:02:48.8040000+00:00, PodName=gateway-1, Severity=Info, EventType=Request, RequestId=790b4d89-c6cd-4baa-a9ea-76d4809797a7, Method=GET, Path=/api/auth/logout, StatusCode=200 +LineNo=171, Timestamp=2026-06-05T16:02:49.3560000+00:00, PodName=gateway-1, Severity=Warning, EventType=Internal, ExceptionName=System.Net.Http.HttpRequestException, ExceptionMessage=Remote service responded slowly. +LineNo=172, Timestamp=2026-06-05T16:02:49.8130000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=792aec7d-be51-429a-840c-2168ffa92727, Method=DELETE, Path=/api/auth/login, StatusCode=404 +LineNo=173, Timestamp=2026-06-05T16:02:51.2200000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=d9fd97f8-2645-4302-8e99-51ab3103047f, TargetService=emailservice, DurationMs=146 +LineNo=174, Timestamp=2026-06-05T16:02:51.5860000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=d8231552-79f1-43be-8925-aaa53b896814, TargetService=emailservice, DurationMs=174 +LineNo=175, Timestamp=2026-06-05T16:02:52.5570000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=ec2d51e1-35a5-49ff-b3c9-a7c2c14b1a4a, Method=GET, Path=/api/content/notice, StatusCode=500 +LineNo=176, Timestamp=2026-06-05T16:02:53.6730000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=dbb570e7-b270-46b4-a2a7-d8a8e58f2c2d, TargetService=authservice, DurationMs=262 +LineNo=177, Timestamp=2026-06-05T16:02:55.3730000+00:00, PodName=gateway-1, Severity=Info, EventType=Call, RequestId=f4e2afe8-a220-4806-949b-d9d6e1c66e2b, TargetService=contentservice, DurationMs=157 +LineNo=178, Timestamp=2026-06-05T16:02:57.3580000+00:00, PodName=userservice-0, Severity=Info, EventType=Call, RequestId=e0ab083e-9d5a-4840-bb4e-b73a87c15158, TargetService=authservice, DurationMs=228 +LineNo=179, Timestamp=2026-06-05T16:02:57.8470000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=3272001d-9e0e-40ae-8f7c-94d3fc32f201, Method=POST, Path=/api/content/notice, StatusCode=200 +LineNo=180, Timestamp=2026-06-05T16:02:59.5580000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=34d79cb9-b869-42ec-ae10-c32a4f3ad1c6, TargetService=emailservice, DurationMs=111 +LineNo=181, Timestamp=2026-06-05T16:03:00.5390000+00:00, PodName=emailservice-1, Severity=Error, EventType=Internal, ExceptionName=System.Data.DataException, ExceptionMessage=Database query failed. +LineNo=182, Timestamp=2026-06-05T16:03:01.4850000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=c8902407-a32b-49c0-b470-15dfd89c40d0, Method=PUT, Path=/api/content/notice, StatusCode=403 +LineNo=183, Timestamp=2026-06-05T16:03:02.8250000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=fc49b36d-54f0-4992-bd2d-1469c751ded9, TargetService=contentservice, DurationMs=194 +LineNo=184, Timestamp=2026-06-05T16:03:03.7660000+00:00, PodName=gateway-0, Severity=Info, EventType=Call, RequestId=2cebe883-436f-4bd3-966b-47fd8f44fbe0, TargetService=contentservice, DurationMs=212 +LineNo=185, Timestamp=2026-06-05T16:03:05.2700000+00:00, PodName=authservice-2, Severity=Info, EventType=Request, RequestId=c2cf89e0-5283-471a-95e3-a7dae8e69e98, Method=DELETE, Path=/api/auth/login, StatusCode=204 +LineNo=186, Timestamp=2026-06-05T16:03:06.2020000+00:00, PodName=userservice-0, Severity=Warning, EventType=Internal, ExceptionName=System.TimeoutException, ExceptionMessage=Request processing is slower than expected. +LineNo=187, Timestamp=2026-06-05T16:03:07.5080000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=a6587330-a50e-46bc-afbf-70efd9d15948, Method=PUT, Path=/api/content/notice, StatusCode=404 +LineNo=188, Timestamp=2026-06-05T16:03:08.5890000+00:00, PodName=contentservice-0, Severity=Info, EventType=Call, RequestId=5af1cf8a-29c3-4187-8711-c3ff82a723c5, TargetService=emailservice, DurationMs=178 +LineNo=189, Timestamp=2026-06-05T16:03:09.1910000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=e69837d8-a84d-4d29-bcdc-01ea168afffb, Method=DELETE, Path=/api/auth/logout, StatusCode=401 +LineNo=190, Timestamp=2026-06-05T16:03:11.0280000+00:00, PodName=gateway-0, Severity=Warning, EventType=Internal, ExceptionName=System.Net.Http.HttpRequestException, ExceptionMessage=Remote service responded slowly. +LineNo=191, Timestamp=2026-06-05T16:03:12.2520000+00:00, PodName=gateway-1, Severity=Error, EventType=Internal, ExceptionName=System.UnauthorizedAccessException, ExceptionMessage=Access to protected resource is denied. +LineNo=192, Timestamp=2026-06-05T16:03:12.7850000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=92713cab-31ef-4bb9-a194-651fa5f74755, Method=POST, Path=/api/auth/logout, StatusCode=401 +LineNo=193, Timestamp=2026-06-05T16:03:14.4040000+00:00, PodName=contentservice-1, Severity=Info, EventType=Request, RequestId=966ffa1c-bf13-481f-8876-70714d133af9, Method=DELETE, Path=/api/auth/login, StatusCode=404 +LineNo=194, Timestamp=2026-06-05T16:03:15.0580000+00:00, PodName=gateway-0, Severity=Error, EventType=Internal, ExceptionName=System.NullReferenceException, ExceptionMessage=Object reference not set to an instance of an object. +LineNo=195, Timestamp=2026-06-05T16:03:16.7660000+00:00, PodName=userservice-0, Severity=Info, EventType=Request, RequestId=c5d16bb4-d363-41b8-a012-509219c72a10, Method=DELETE, Path=/api/content/article, StatusCode=200 +LineNo=196, Timestamp=2026-06-05T16:03:17.4920000+00:00, PodName=contentservice-1, Severity=Info, EventType=Call, RequestId=362f4bae-1103-411c-a68f-a964b0dc4895, TargetService=emailservice, DurationMs=245 +LineNo=197, Timestamp=2026-06-05T16:03:18.4350000+00:00, PodName=authservice-0, Severity=Error, EventType=Internal, ExceptionName=System.NullReferenceException, ExceptionMessage=Object reference not set to an instance of an object. +LineNo=198, Timestamp=2026-06-05T16:03:19.6800000+00:00, PodName=gateway-0, Severity=Info, EventType=Request, RequestId=9283b862-2369-45c6-8888-2d2cfc278cee, Method=POST, Path=/api/auth/logout, StatusCode=404 +LineNo=199, Timestamp=2026-06-05T16:03:21.4550000+00:00, PodName=authservice-1, Severity=Info, EventType=Request, RequestId=b856326b-d9e7-4461-8b0e-9c7e828bce5e, Method=PUT, Path=/api/content/article, StatusCode=200 + +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. +>>> Please input the file name: +Error: FileNotFound: File not found: hahahaha.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. +>>> \ 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..5fda270 --- /dev/null +++ b/docs/03-async-grpc/report.md @@ -0,0 +1,56 @@ +# 03-async-grpc 报告 + +## 功能实现说明 + +### T3.1 Agent 服务端 + +**GrpcLogEntryVisitor / GrpcTypeConverter**:补全 Request/Internal 两种日志条目的双向转换。访问者模式把 `LogEntry` 转成 protobuf 的 `LogEntryMessage`(oneof 字段),`ConvertFromGrpc` 按 `EntryCase` switch 还原成对应 record。枚举映射严格按 proto 定义(INFO=0/WARNING=1/ERROR=2;CALL/REQUEST/INTERNAL)。 + +**AgentSession**:业务逻辑层。核心是「**永不抛异常给 gRPC 层**」——所有操作把结果包进 `OperationStatusMessage`(Success/Code/Message),异常分类映射: + +| 异常 | AgentErrorCode | +|---|---| +| `ChangeDirectory` 返回 false | `DirectoryNotFound` | +| `ArgumentException` | `InvalidArgument` | +| `InvalidOperationException`(如重复分析) | `InvalidOperation` | +| 文件不在目录清单(`TryGetAnalysisResult` false) | `FileNotFound` | +| 其余异常 | `InternalError` + 日志 | + +`GetAnalysisResult` 返回响应列表:首条是 `Header`(状态/worker/错误消息),`Succeeded` 时逐条跟随 `LogEntry`,其他状态只返回 Header——与测试断言的条数严格一致。 + +**AgentService**:gRPC 基类薄封装,全部转发到 Session;`GetAnalysisResult` 是 server-streaming,把 Session 返回的列表逐条 `await responseStream.WriteAsync()`。 + +### T3.2 RemoteCli 客户端 + +菜单 1-6 全部实现:`ReadDegreeOfParallelism`/`ReadFileNames` 输入循环校验;`GetAnalysisResult` 用 `await foreach (call.ResponseStream.ReadAllAsync())` 消费流,Header 打印状态、LogEntry 经 `GrpcTypeConverter.ConvertFromGrpc` 还原后用 `KeyValueVisitor.Dump` 输出键值对。所有 RPC 调用包 `RpcException` 捕获(如 Agent 掉线时打印 `Unavailable` 而非崩溃)。 + +### 端到端验证(本机实跑) + +Agent 监听 `http://localhost:5000`,RemoteCli 走完完整流程:换目录到 dataset → 列出 3 个 .log → 指定分析 basic.log(并行度 4)→ 流式取回 3 条日志(Call/Request/Internal 各一条,字段完整)→ AnalyzeAll 后取回 basic-multiple.log 全部 45 条 → 查询不存在的 hahahaha.log 返回 `FileNotFound` 错误码。 + +运行截图与鲁棒性测试(非法文件名/未分析文件/Agent 停机)记录见 `screenshots/`(运行日志存档见本仓库 `docs/03-async-grpc/demo_log.txt`)。 + +## 问答题 + +### (Q3.1) 网络应用 vs 非网络应用 + +**区别**:非网络程序的状态都在一个进程内存里,函数调用是可靠、同步、即答的;网络应用被切成客户端/服务端两半,中间隔着一条**不可靠、有延迟、会断开**的信道。这带来几个额外难点: + +1. **部分失败**:本地调用要么成功要么抛异常,网络调用还有第三种状态——「不知道成没成功」(超时)。所以 Agent 的每个操作都要设计成幂等或可重试的,且必须用状态码(我们的 `OperationStatusMessage`)而不是异常向上传递结果。 +2. **数据表示**:两端的内存布局不同,必须经 protobuf 这类 IDL 序列化。`LogEntry` 的 record 继承结构无法直接上网络线,要靠 oneof + 访问者模式做双向映射——这部分代码量几乎与业务逻辑相当。 +3. **生命周期错位**:服务端是常驻进程,客户端随时连上/断开。LocalCli 的异常最多让本次交互崩溃,Agent 的异常会让**所有**后续客户端失去服务,所以 Session 层必须 catch-all(这就是 guidance 反复强调的「服务绝不能挂」)。 +4. **异步模型**:网络 IO 的延迟天然要求 async/await,否则每个等待都阻塞一个线程。流式响应(server streaming)还引入了「结果分多次到达」的编程模型,必须用 `IAsyncEnumerable` 消费。 + +复杂之处还包括:调试要两端同时看日志、时间戳要统一时区(`Timestamp.FromDateTimeOffset`)、并发请求下服务端的状态保护(复用 02 的 `_syncRoot` 锁约定)。 + +### (Q3.2.b) AI 使用情况 + +**提示词**:提供了 proto 文件、GrpcTypeConverter 框架和「异常→错误码」的映射要求,让 AI 补全 AgentSession/AgentService/RemoteCli。 + +**使用方式**:主要是「让 AI 写部分作业代码」+「讲解框架」。gRPC 的 C# server-streaming 写法(`IServerStreamWriter` + `ReadAllAsync`)我原本不熟,AI 给出了正确的 API 形态。 + +**AI 的错误**:一次把 `GetAnalysisResult` 的 session 方法签名写成 `async Task`(框架给的是同步返回 `IReadOnlyList`),导致 AgentService 里多余 await 编译警告;对照框架签名修正。另一次忘了 Header 之后再写 `Status` 字段(protobuf oneof 字段没赋值时反序列化端 `PayloadCase` 判断会走错分支),测试断言 `PayloadOneofCase.Header` 失败后补上。 + +**新学到的知识**:① protobuf `oneof` 的语义——同组字段同时至多一个生效,读端用 `EntryCase`/`PayloadCase` 判别;② gRPC 四种流模式中 server-streaming 的 C# 写法;③ 「错误码包进响应体而不是抛 RPC 异常」是业务错误 vs 传输错误的分界——`RpcException` 只留给信道级故障。 + +**难度评价**:三节中最高,但难在「接口对接的琐碎」而非算法。 diff --git a/src/LocalCli/Program.cs b/src/LocalCli/Program.cs index 17b30db..534311b 100644 --- a/src/LocalCli/Program.cs +++ b/src/LocalCli/Program.cs @@ -112,22 +112,89 @@ 6. Exit. private static void ShowLogFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + // T2.3 + Console.WriteLine("Log files in current directory:"); + var files = analyzer.GetLogFiles(); + if (files.Count == 0) + { + Console.WriteLine(" (no .log files found)"); + return; + } + foreach (var fileName in files) + { + Console.WriteLine($" {fileName}"); + } } private static void AnalyzeFiles(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + // T2.3 + Console.WriteLine("Please input file names separated by commas:"); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine("No file name given."); + return; + } + var fileNames = input.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + try + { + analyzer.AnalyzeFiles(0, fileNames); + Console.WriteLine("Analysis finished."); + } + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) + { + Console.WriteLine($"Failed to analyze: {ex.Message}"); + } } private static void AnalyzeAll(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + // T2.3 + try + { + analyzer.AnalyzeAll(0); + Console.WriteLine("Analysis finished."); + } + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) + { + Console.WriteLine($"Failed to analyze: {ex.Message}"); + } } private static void GetAnalysisResult(LogFileAnalyzer analyzer) { - throw new NotImplementedException("T2.3"); + // T2.3 + Console.WriteLine("Please input the file name:"); + var fileName = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(fileName)) + { + Console.WriteLine("No file name given."); + return; + } + fileName = fileName.Trim(); + if (!analyzer.TryGetAnalysisResult(fileName, out var result)) + { + Console.WriteLine($"File '{fileName}' is not in the current directory."); + return; + } + switch (result!.State) + { + case AnalysisState.NotAnalyzed: + Console.WriteLine($"File '{fileName}' has not been analyzed yet."); + break; + case AnalysisState.Succeeded: + var visitor = new KeyValueVisitor(); + foreach (var entry in result.Entries) + { + var dict = visitor.Dump(entry); + Console.WriteLine(string.Join(", ", dict.Select(kv => $"{kv.Key}={kv.Value}"))); + } + break; + case AnalysisState.Failed: + Console.WriteLine($"Analysis of '{fileName}' failed: {result.ErrorMessage}"); + break; + } } } } diff --git a/src/LogAnalyzer/LogFileAnalyzer.cs b/src/LogAnalyzer/LogFileAnalyzer.cs index c3e7691..43025c4 100644 --- a/src/LogAnalyzer/LogFileAnalyzer.cs +++ b/src/LogAnalyzer/LogFileAnalyzer.cs @@ -142,6 +142,7 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) * Set _isAnalyzing */ // TODO: T2.2 + _isAnalyzing = true; } try @@ -155,6 +156,10 @@ public void AnalyzeFiles(int degreeOfParallelism, IEnumerable fileNames) * Remember to lock _syncRoot to prevent data race */ // TODO: T2.2 + lock (_syncRoot) + { + _isAnalyzing = false; + } } } @@ -169,7 +174,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"); + // TODO: T2.2 + if (!_analysisResults.TryGetValue(file.Name, out var existing)) + { + throw new InvalidOperationException($"Unknown file: {file.FullName}"); + } + if (existing.State == AnalysisState.NotAnalyzed) + { + logFilesToParse.Add(file); + } } } @@ -184,6 +197,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]; @@ -195,12 +213,22 @@ private void RunWorkers(int degreeOfParallelism, IReadOnlyList fileLis * Create and start threads to run `WorkerMain` */ // TODO: T2.2 + workers[i] = new Thread(() => WorkerMain(workerId, queue)) + { + IsBackground = true, + Name = threadName + }; + workers[i].Start(); } /* * Wait for (join) all threads to end */ // TODO: T2.2 + foreach (var worker in workers) + { + worker.Join(); + } } private void WorkerMain(int workerId, WorkQueue queue) @@ -213,19 +241,41 @@ private void WorkerMain(int workerId, WorkQueue queue) try { // Parse file - throw new NotImplementedException("TODO: T2.2"); + // TODO: T2.2 + using var reader = new StreamReader(file.FullName); + var entries = parser.Parse(reader).ToList(); + result = new AnalysisResult( + FileName: file.Name, + FullName: file.FullName, + State: AnalysisState.Succeeded, + Entries: entries, + ErrorMessage: null, + WorkerId: workerId + ); } catch (Exception ex) { // Save exception message to result - throw new NotImplementedException("TODO: T2.2"); + // 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"); + // TODO: T2.2 + lock (_syncRoot) + { + _analysisResults[file.Name] = result; + } } } } diff --git a/src/LogAnalyzer/WorkQueue.cs b/src/LogAnalyzer/WorkQueue.cs index 23055a5..554cd6c 100644 --- a/src/LogAnalyzer/WorkQueue.cs +++ b/src/LogAnalyzer/WorkQueue.cs @@ -20,17 +20,43 @@ public bool IsCompleted public void Enqueue(T item) { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + if (_isCompleted) + { + throw new InvalidOperationException("Cannot enqueue after CompleteAdding has been called."); + } + _items.Enqueue(item); + System.Threading.Monitor.Pulse(_items); + } } public bool TryDequeue([NotNullWhen(true)] out T? item) { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + // while 而非 if:防止虚假唤醒(spurious wakeup) + while (_items.Count == 0) + { + if (_isCompleted) + { + item = default; + return false; + } + System.Threading.Monitor.Wait(_items); + } + item = _items.Dequeue()!; + return true; + } } public void CompleteAdding() { - throw new NotImplementedException("TODO: T2.1"); + lock (_items) + { + _isCompleted = true; + System.Threading.Monitor.PulseAll(_items); + } } } } diff --git a/src/LogAnalyzerAgent/Applications/AgentSession.cs b/src/LogAnalyzerAgent/Applications/AgentSession.cs index 2531f22..4b6abc9 100644 --- a/src/LogAnalyzerAgent/Applications/AgentSession.cs +++ b/src/LogAnalyzerAgent/Applications/AgentSession.cs @@ -79,22 +79,136 @@ 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 = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.DirectoryNotFound, + Message = $"Directory not found or analysis in progress: {request.DirectoryPath}", + }; + return Task.FromResult(response); + } + response.CurrentDirectory = _analyzer.CurrentDirectory ?? ""; + response.FileNames.AddRange(_analyzer.GetLogFiles()); + response.Status = CreateNoErrorOperationStatus(); + } + catch (ArgumentException ex) + { + response.Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = ex.Message, + }; + _logger.LogError(ex, "An error occurred while changing directory."); + } + 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 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 (ArgumentException ex) + { + response.Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.InvalidArgument, + Message = ex.Message, + }; + _logger.LogError(ex, "An error occurred while analyzing files."); + } + catch (InvalidOperationException ex) + { + response.Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.InvalidOperation, + Message = ex.Message, + }; + _logger.LogError(ex, "An error occurred while analyzing files."); + } + catch (Exception ex) + { + response.Status = CreateInternalErrorOperationStatus(ex); + _logger.LogError(ex, "An error occurred while analyzing files."); + } + return Task.FromResult(response); } public IReadOnlyList GetAnalysisResult(GetAnalysisResultRequest request, CancellationToken cancellationToken) { - throw new NotImplementedException("TODO: T3.1"); + var responses = new List(); + if (!_analyzer.TryGetAnalysisResult(request.FileName, out var result)) + { + responses.Add(new GetAnalysisResultResponse() + { + Status = new OperationStatusMessage() + { + Success = false, + Code = AgentErrorCode.FileNotFound, + Message = $"File not found: {request.FileName}", + } + }); + return responses; + } + + var header = new AnalysisResultHeaderMessage() + { + FileName = result!.FileName, + FullName = result.FullName, + State = GrpcTypeConverter.ConvertToGrpc(result.State), + ErrorMessage = result.ErrorMessage ?? "", + WorkerId = result.WorkerId, + }; + responses.Add(new GetAnalysisResultResponse() + { + Header = header, + Status = CreateNoErrorOperationStatus(), + }); + + if (result.State == AnalysisState.Succeeded) + { + foreach (var entry in result.Entries) + { + responses.Add(new GetAnalysisResultResponse() + { + LogEntry = GrpcTypeConverter.ConvertToGrpc(entry), + Status = CreateNoErrorOperationStatus(), + }); + } + } + return responses; } } } diff --git a/src/LogAnalyzerAgent/Services/AgentService.cs b/src/LogAnalyzerAgent/Services/AgentService.cs index 591dcad..d38d1cf 100644 --- a/src/LogAnalyzerAgent/Services/AgentService.cs +++ b/src/LogAnalyzerAgent/Services/AgentService.cs @@ -29,27 +29,31 @@ public override Task GetAgentStatus(Empty empty, ServerC public override Task ChangeDirectory(ChangeDirectoryRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.ChangeDirectory(request, context.CancellationToken); } public override Task GetLogFiles(Empty empty, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.GetLogFiles(empty, context.CancellationToken); } public override Task AnalyzeAll(AnalyzeAllRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.AnalyzeAll(request, context.CancellationToken); } public override Task AnalyzeFiles(AnalyzeFilesRequest request, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + return _session.AnalyzeFiles(request, context.CancellationToken); } public override async Task GetAnalysisResult(GetAnalysisResultRequest request, IServerStreamWriter responseStream, ServerCallContext context) { - throw new NotImplementedException("TODO: T3.1"); + var responses = _session.GetAnalysisResult(request, context.CancellationToken); + foreach (var response in responses) + { + await responseStream.WriteAsync(response); + } } } } 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..aa31134 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.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..d0e8cf5 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,38 @@ 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}"); + // 字段值固定为 "ExceptionName: exception message" 格式,按第一个冒号+空格切分 + var separatorIndex = internalMessage.Exception.IndexOf(": "); + if (separatorIndex < 0) + { + throw new FormatException($"Exception field is not in 'ExceptionName: message' format: {internalMessage.Exception}"); + } + return new InternalLogEntry( + LineNo: logRecord.LineNo, + Timestamp: DateTimeOffset.Parse(logRecord.Timestamp), + PodName: logRecord.PodName, + Severity: ParseSeverity(internalMessage.Severity), + ExceptionName: internalMessage.Exception[..separatorIndex], + ExceptionMessage: internalMessage.Exception[(separatorIndex + 2)..] + ); } private static LogSeverity ParseSeverity(string severity) @@ -78,10 +104,17 @@ 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..d98ed69 100644 --- a/src/RemoteCli/Program.cs +++ b/src/RemoteCli/Program.cs @@ -116,32 +116,166 @@ 6. Exit. private static async Task ShowLogFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + // 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 in agent's current directory:"); + if (response.FileNames.Count == 0) + { + Console.WriteLine(" (no .log files found)"); + return; + } + foreach (var fileName in response.FileNames) + { + Console.WriteLine($" {fileName}"); + } + } + catch (RpcException ex) + { + Console.WriteLine($"RPC error: {ex.StatusCode}: {ex.Message}"); + } } private static int ReadDegreeOfParallelism() { - throw new NotImplementedException("TODO: T3.2"); + // TODO: T3.2 + while (true) + { + Console.WriteLine("Please input degree of parallelism (0 = auto):"); + var input = Console.ReadLine(); + if (int.TryParse(input, out var degree) && degree >= 0) + { + return degree; + } + Console.WriteLine("Invalid input, please try again."); + } } private static List ReadFileNames() { - throw new NotImplementedException("TODO: T3.2"); + // TODO: T3.2 + while (true) + { + Console.WriteLine("Please input file names separated by commas:"); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine("No file name given, please try again."); + continue; + } + var fileNames = input.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); + if (fileNames.Count == 0) + { + Console.WriteLine("No file name given, please try again."); + continue; + } + return fileNames; + } } private static async Task AnalyzeFiles(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + // TODO: T3.2 + var degree = ReadDegreeOfParallelism(); + var fileNames = ReadFileNames(); + try + { + var request = new AnalyzeFilesRequest() + { + DegreeOfParallelism = degree, + }; + request.FileNames.AddRange(fileNames); + var response = await client.AnalyzeFilesAsync(request); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + return; + } + Console.WriteLine("Analysis finished."); + } + catch (RpcException ex) + { + Console.WriteLine($"RPC error: {ex.StatusCode}: {ex.Message}"); + } } private static async Task AnalyzeAll(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + // TODO: T3.2 + var degree = ReadDegreeOfParallelism(); + try + { + var request = new AnalyzeAllRequest() + { + DegreeOfParallelism = degree, + }; + var response = await client.AnalyzeAllAsync(request); + if (!response.Status.Success) + { + Console.WriteLine($"Error: {response.Status.Code}: {response.Status.Message}"); + return; + } + Console.WriteLine("Analysis finished."); + } + catch (RpcException ex) + { + Console.WriteLine($"RPC error: {ex.StatusCode}: {ex.Message}"); + } } private static async Task GetAnalysisResult(LogAnalyzerAgentServiceClient client) { - throw new NotImplementedException("TODO: T3.2"); + // TODO: T3.2 + Console.WriteLine("Please input the file name:"); + var fileName = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(fileName)) + { + Console.WriteLine("No file name given."); + return; + } + try + { + var request = new GetAnalysisResultRequest() + { + FileName = fileName.Trim(), + }; + 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; + } + if (response.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.Header) + { + var header = response.Header; + Console.WriteLine($"--- {header.FileName} ({header.FullName}) ---"); + Console.WriteLine($"State: {header.State}, Worker: {header.WorkerId}"); + if (!string.IsNullOrEmpty(header.ErrorMessage)) + { + Console.WriteLine($"Error message: {header.ErrorMessage}"); + } + } + else if (response.PayloadCase == GetAnalysisResultResponse.PayloadOneofCase.LogEntry) + { + var entry = GrpcTypeConverter.ConvertFromGrpc(response.LogEntry); + var visitor = new KeyValueVisitor(); + var dict = visitor.Dump(entry); + Console.WriteLine(string.Join(", ", dict.Select(kv => $"{kv.Key}={kv.Value}"))); + } + } + } + catch (RpcException ex) + { + Console.WriteLine($"RPC error: {ex.StatusCode}: {ex.Message}"); + } } } }