ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Envoy 访问日志 JSON 格式化修复:omit_empty_values 从失效到真正生效的完整解析

Envoy 访问日志 JSON 格式化修复:omit_empty_values 从失效到真正生效的完整解析 Envoy 访问日志 JSON 格式化修复omit_empty_values 从失效到真正生效的完整解析【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy本篇文章围绕 Envoy 访问日志格式化配置项omit_empty_values与json_format结合时的一处行为缺陷展开该配置在 JSON 格式下曾因模板预序列化机制而完全失效导致命令操作符求值为 null 时仍会输出{key:null}。文章将结合本次 bug 修复对应 changelogs/current/bug_fixes/access_log__json-formatter-omit-empty-values.rst剖析缺陷根因、修复后的全新格式化流程、运行时开关的降级方式并给出可直接落地的配置示例。读完本文你将能准确理解omit_empty_values在 text 与 JSON 两种格式下的语义差异掌握如何通过 Envoy 运行时特性开关回退该行为并能在生产访问日志中按需输出紧凑、干净的 JSON 结构。一、问题背景SubstitutionFormatString 与 omit_empty_values 的既有约定Envoy 的访问日志Access Log格式化依赖核心配置类型 SubstitutionFormatString它支持两种输出形态text_format/text_format_source通过命令操作符command operator拼接纯文本json_format通过命令操作符构建结构化 JSON 对象值按类型呈现为字符串、数字或布尔值。在 substitution_format_string.proto 中omit_empty_values字段默认false明确约定了两种格式下空值的处理语义对text_format当命令操作符求值为空时输出从占位符-变为空字符串从而完全省略空值对json_format值为 null 的键应从输出结构中省略全部字段都被省略的嵌套对象应一并移除而空数组需要保留根对象始终输出因此完全为空的结构渲染为{}。从 proto 注释可以确认键为 null 时省略、嵌套对象整体移除、空数组保留、根对象恒输出这四条规则本就是omit_empty_values设计时应有的文档化行为。本次 bug 修复所做的正是让json_format真正兑现这些语义。二、缺陷根因JSON 格式化器的配置加载期预序列化为什么omit_empty_values对json_format此前毫无效果changelog 给出的关键线索是JSON 格式化器在加载配置时会预先序列化模板the JSON formatter pre-serializes the template when loading the configuration。从源码可以还原这一机制。在 source/common/formatter/substitution_formatter.cc 中JsonFormatterImpl::create在配置加载阶段就把Protobuf::Struct模板逐元素解析包含命令操作符的字符串被解析为FormatterProvider列表纯常量字符串则在加载时完成一次性的 JSON 转义与序列化。此后每次访问日志格式化时formatTo见 substitution_formatter.cc只是把预序列化的常量直接拼接进输出缓冲再对命令操作符逐个求值。问题正出在求值失败的分支上const absl::string_view empty_value omit_empty_values_ ? EMPTY_STRING : DefaultUnspecifiedValueStringView;即便设置了omit_empty_values_JsonFormatterImpl也只是把空值占位从-换成空字符串随后在单提供者场景下通过output.addNull()补写null见 substitution_formatter.cc。也就是说旧实现只做到用 null 代替 -并没有真正把键从 JSON 结构中删除于是产生了{key:null}而非预期的{}。这一点在源码注释中也有直接佐证This implementation cannot handle the omit_empty_values for typed value correctly and will always add a null.substitution_formatter.cc。简言之旧的JsonFormatterImpl是按模板顺序流式写输出的线性实现它在写字段时无法回溯自然也无法实现整个对象变空后连同对象本身一起删除这种需要后处理的语义。三、修复方案模板树构建 序列化期回滚的 OmitEmptyJsonFormatterImpl修复引入了一个全新的实现OmitEmptyJsonFormatterImpl其核心思路是在配置加载期把 JSON 模板先构建成一棵结构化模板树格式化时按树递归序列化凡是求值为空的分支立即回滚已写入的输出。3.1 分发逻辑运行时开关决定走哪条实现路径在 source/common/formatter/substitution_format_string.cc 中createJsonFormatter会根据运行时特性开关进行分发if (omit_empty_values Runtime::runtimeFeatureEnabled( envoy.reloadable_features.json_formatter_omit_empty_values)) { return OmitEmptyJsonFormatterImpl::create(struct_format, commands); } return JsonFormatterImpl::create(struct_format, omit_empty_values, commands);也就是说只有同时满足配置中开启omit_empty_values且运行时特性envoy.reloadable_features.json_formatter_omit_empty_values开启两个条件才会走全新的OmitEmptyJsonFormatterImpl否则退回旧的预序列化实现保留 null 键的旧行为。3.2 模板树的数据结构修复在 source/common/formatter/substitution_formatter.cc 定义了三种节点类型JsonFormatMapNodeJSON 对象节点字段按 key 排序存储以保证输出确定性fields_为std::vectorstd::pairstd::string, JsonFormatValueJsonFormatListNodeJSON 数组节点元素存于values_JsonFormatValue变体类型可表示预序列化的常量标量、命令操作符模板、嵌套对象或嵌套数组。其中absl::monostate代表字面 null 或未设置在构建树时即被丢弃因此运行期永远不会遇到字面 null见 substitution_formatter.cc。3.3 构建期null 字面量在配置加载时就剔除buildJsonFormatValue/buildJsonFormatMapNode见 substitution_formatter.cc在构建树时完成如下工作数字、布尔、不含%的常量字符串在加载期一次性序列化避免运行期重复转义含%的字符串被解析为命令操作符模板配置中显式写出的null以及未设置 kind 的值KIND_NOT_SET统一映射为absl::monostate并直接丢弃既不入树、运行期也无需再检查。这解释了为什么配置中显式的null字段会被省略——它们在配置加载时就已经从模板树中消失了。3.4 序列化期递归回滚实现键省略与空对象删除格式化核心在serializeJsonFormatMapNodesubstitution_formatter.cc中实现。对每个字段记录写入位置field_start序列化字段名、冒号后递归序列化值若值序列化返回false即被省略则通过serializer.outputBuffer().resize(field_start)回滚包括字段分隔符、key 与任何部分输出在内的全部字节实现键整体消失若整个对象的字段全部被省略object_is_empty仍为 true则回滚到node_start并返回false让父级或根将该对象整体丢弃。数组的语义则不同。serializeJsonFormatListNodesubstitution_formatter.cc只跳过被省略的元素包括求值为空的命令操作符数组本身始终保留即使所有元素都被省略也输出[]与 proto 文档中empty arrays are preserved的约定一致。单提供者命令操作符求值为 null 时通过ValueSink保留原始值类型并返回consumed()作为省略信号多提供者模板则强制输出字符串缺失值在omit_empty_values下贡献空字符串因此这类键不会被删除见 substitution_formatter.cc。最后OmitEmptyJsonFormatterImpl::formatTosubstitution_formatter.cc处理根对象边界若根对象所有字段都被省略仍强制输出{}保证输出永远是合法 JSON 对象。四、修复后的行为明细什么被省略什么被保留根据本次新增的单元测试见 test/common/formatter/substitution_format_string_test.cc修复后的omit_empty_valuesjson_format行为可归纳为一张速查表场景配置写法输出结果常量字符串present_string: plainpresent_string: plain保留求值成功的命令操作符present_code: %RESPONSE_CODE%present_code: 200保留原始数字类型求值为 null 的命令操作符missing_req: %REQ(missing-header)%键整体省略多提供者模板部分缺失multi_token: %REQ(missing-header)%-%REQ(:method)%multi_token: -GET保留多提供者模板全部缺失both_missing: %REQ(missing-x)%%REQ(missing-y)%both_missing: 保留空字符串全空嵌套对象empty_nested: {a: ..., b: ...}嵌套对象整体删除部分非空嵌套对象partial_nested: {present: ..., missing: ...}仅保留非空字段混合数组array_value: [GET, missing, plain]跳过缺失元素数组保留全空数组empty_array: [missing]empty_array: []保留显式 null / 未设置字段explicit_null: null构建期即被剔除见 测试用例根对象全空所有字段均缺失输出{}见 测试用例值得注意的两个易混淆点多提供者含%拼接模板不会被省略即使所有操作符都求值为空输出也是空字符串而非删除键这是值类型为字符串与单提供者可保持原始类型两条路径的差异数字、布尔值的类型得以保留%RESPONSE_CODE%求值成功后输出为 JSON 数字200而非字符串200这正是ValueSink保留值类型的设计目标。五、运行时开关如何回退到旧行为本次修复属于行为变更behavioral change为兼容已有部署Envoy 提供了运行时特性开关进行灰度与回退。开关定义在 source/common/runtime/runtime_features.ccRUNTIME_GUARD(envoy_reloadable_features_json_formatter_omit_empty_values);该特性默认开启。如需恢复旧行为即json_format下保留 null 键、输出{key:null}可在 Envoy 运行时配置runtime层或 bootstrap 中的runtime_layer中显式关闭runtime: overrides: envoy.reloadable_features.json_formatter_omit_empty_values: false测试 substitution_format_string_test.cc 验证了这一回退路径当把该特性置为false后即使配置了omit_empty_values: true格式化结果仍为{missing_req:null}——说明分发逻辑回退到了旧的预序列化JsonFormatterImpl。需要同时留意的是omit_empty_values开关与运行时特性开关是与的关系。只有配置项为true且特性开启时新的省略逻辑才生效任一层关闭都得不到省略效果。六、实战配置示例为访问日志输出干净的 JSON结合前面的行为表给出一个可直接用于 File Access Log / gRPC Access Log 等场景的配置片段YAML 结构对应envoy.config.core.v3.SubstitutionFormatStringaccess_log: - name: envoy.access_loggers.file typed_config: type: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout log_format: omit_empty_values: true json_format: timestamp: %START_TIME% method: %REQ(:method)% path: %REQ(:path)% status: %RESPONSE_CODE% duration_ms: %DURATION% upstream_host: %UPSTREAM_HOST% request_id: %REQ(x-request-id)% referer: %REQ(referer)% user_agent: %REQ(user-agent)% route_name: %ROUTE_NAME% metadata: trace_id: %DYNAMIC_METADATA(trace:id)% canary: %DYNAMIC_METADATA(route:canary)% tags: - %ENV(ENVIRONMENT)% - %REQ(x-tag)%启用后对没有referer、x-request-id等请求头的访问对应键如referer、request_id将直接消失而不是输出referer: null若trace_id、canary两个动态元数据字段都缺失metadata嵌套对象会被整体删除tags数组即使%ENV(ENVIRONMENT)%、%REQ(x-tag)%全部缺失仍输出tags: []保证下游 JSON 解析器对数组字段的存在性预期不被破坏极端情况下所有字段都缺失输出仍是{}不会产生非法 JSON。若在灰度升级后发现下游系统依赖{key:null}的旧输出格式可通过第五节中的运行时开关一键回退无需改动访问日志配置本身。七、源码验证路径与延伸阅读变更记录changelogs/current/bug_fixes/access_log__json-formatter-omit-empty-values.rst配置项定义api/envoy/config/core/v3/substitution_format_string.proto新旧实现分发source/common/formatter/substitution_format_string.cc旧实现JsonFormatterImplsource/common/formatter/substitution_formatter.cc新实现OmitEmptyJsonFormatterImpl与模板树source/common/formatter/substitution_formatter.cc运行时特性注册source/common/runtime/runtime_features.cc行为验证测试test/common/formatter/substitution_format_string_test.cc八、小结本次 bug 修复的本质是把omit_empty_values对json_format的支持从文档中有、实现上无落实为文档与实现一致通过将预序列化流式输出改为模板树 序列化期回滚的新架构Envoy 现在能够精确地省略 null 键、删除全空嵌套对象、保留空数组与根对象同时保持数字/布尔值的原始类型。对于依赖访问日志 JSON 结构的可观测性系统而言这一变更让日志体积更紧凑、schema 更干净也为缺失字段即不存在的下游消费模式提供了可靠支撑。生产环境升级时请务必结合运行时开关envoy.reloadable_features.json_formatter_omit_empty_values评估下游兼容性。【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进