.NET 10对JSON控制台日志输出做了一项改动,改动幅度小到升级时很容易被忽略:格式化后的消息仍然存在,但典型的日志记录不再在State.Message中重复它。如果采集器、脚本或快照测试只读取这个嵌套属性,就可能开始返回null,而应用程序本身仍在正常记录日志。
只要日志会被其他进程解析,就应该把控制台JSON当作一种schema来对待。这意味着运行时升级需要契约测试,而不只是在终端里做一次目视检查。实际的修复方案是:读取顶层Message,保留State用于结构化值,并为旧记录保留一个窄范围的回退逻辑。
为什么.NET 10 JSON控制台日志会破坏嵌套消息解析器
在.NET 10之前,一条普通的AddJsonConsole记录通常会重复渲染后的文本:
{"Message": "Order 42 moved to ready.","State": {"Message": "Order 42 moved to ready.","OrderId": 42,"Status": "ready","{OriginalFormat}": "Order {OrderId} moved to {Status}."}
在.NET 10中,典型的结构只在顶层保留一条渲染后的消息:
{"Message": "Order 42 moved to ready.","State": {"OrderId": 42,"Status": "ready","{OriginalFormat}": "Order {OrderId} moved to {Status}."}
微软将这一改动记录为行为性破坏性变更,并建议解析器使用顶层属性。官方兼容性说明还给出了一个关键提示:当State.Message的内容与顶层值不同时,它仍可能出现。因此,仅仅因为两条属性同时存在就拒绝一条记录,是不必要的。
这并不意味着结构化日志数据的丢失。OrderId、Status和{OriginalFormat}仍然是State内部有用的字段。变化的是消费者应该从哪里获取渲染后的句子。
优先使用顶层Message,保持State结构化
仅依赖旧位置的提取器是脆弱的,因为它假设重复字段就是契约:
static string? ReadLegacyOnly(JsonElement root) =>root.TryGetProperty("State", out var state) &&state.TryGetProperty("Message", out var message)? message.GetString(): null;
更好的做法是采用顶层优先的规则:
static string? ReadMessage(JsonElement root) =>ReadTopLevelMessage(root) ?? ReadStateMessage(root);
回退逻辑是为存储的.NET 9时代记录或混合版本集群准备的。它不是让新解析器继续锚定旧嵌套位置的理由。当两个值同时存在且内容不同时,顶层字段保持权威,这与微软的迁移指南一致。
结构化属性也应该单独解析。在渲染后的句子中搜索订单ID,会丢掉JSON日志的主要优势。格式化器让这些值在State下保持可寻址,而{OriginalFormat}保留了消息模板,便于分组或诊断。
内置格式化器可以通过AddJsonConsole配置;微软的控制台格式化器文档涵盖了时间戳、作用域和JSON选项。这些选项可能改变记录的其他部分,因此契约测试应只关注消费管道实际需要的字段。
热门跟贴