Envoy OpenTelemetry Access Logger 修复详解:custom_tags 中 formatter 命令不再被忽略
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本篇技术指南聚焦 Envoy 中 OpenTelemetry 访问日志器(access logger)的一个关键缺陷修复:custom_tags在构建时曾忽略通过formatters字段配置的命令解析器,导致自定义标签值中引用 formatter 扩展命令时以Not supported field in StreamInfo失败。文章将带你理解该缺陷的成因、修复后的调用链,以及如何在配置中正确组合formatters与custom_tags,并给出源码级证据与可复现的配置示例。
背景:OpenTelemetry 访问日志器与 OTLP 日志导出
Envoy 内置的 OpenTelemetry 访问日志扩展(envoy.access_loggers.open_telemetry)将请求访问日志转换为 OpenTelemetry Protocol(OTLP)日志,并通过 gRPC 或 HTTP 两种传输方式推送到上游后端。其配置消息为envoy.extensions.access_loggers.open_telemetry.v3.OpenTelemetryAccessLogConfig,定义于 api/envoy/extensions/access_loggers/open_telemetry/v3/logs_service.proto。
每条日志记录(LogRecord)主要由三部分内容构成:
body:日志主体,支持%COMMAND%形式的 Envoy 访问日志格式化占位符;attributes:一组键值对(KeyValueList),值同样支持格式化占位符;custom_tags:自定义标签(repeated type.tracing.v3.CustomTag),作为日志属性(attribute)附加到每条记录上,机制与 Envoy tracing 的 custom tag 一致。
而formatters(repeated config.core.v3.TypedExtensionConfig,proto 字段 7)则用于挂载 formatter 插件(扩展类别envoy.formatter),例如 CEL 表达式等自定义命令,供body、attributes乃至custom_tags中的格式化字符串调用。
缺陷现象:custom_tags 中的 formatter 命令解析失败
本次修复对应的变更记录位于 changelogs/current/bug_fixes/open_telemetry__custom-tags-formatters.rst,其描述的问题如下:
OpenTelemetry 访问日志器(gRPC 与 HTTP 两种变体)在构建
custom_tags时忽略了已配置的formatters。此前,自定义标签的值若使用了 formatter 扩展命令,会以Not supported field in StreamInfo错误失败,而同样的命令在body和attributes中却可以正常工作。
也就是说,同样的 formatter 命令存在“位置差异”:
| 使用位置 | 修复前 | 修复后 |
|---|---|---|
body中的格式化字符串 | 正常解析 | 正常解析 |
attributes中的格式化字符串 | 正常解析 | 正常解析 |
custom_tags中value字段的格式化字符串 | 报Not supported field in StreamInfo | 正常解析 |
根因分析:命令解析器没有传递到 custom-tag 创建流程
要理解根因,需要追踪custom_tags的构建路径。在 source/extensions/access_loggers/open_telemetry/access_log_impl.cc 的构造函数中:
AccessLog::AccessLog( ::Envoy::AccessLog::FilterPtr&& filter, envoy::extensions::access_loggers::open_telemetry::v3::OpenTelemetryAccessLogConfig config, ThreadLocal::SlotAllocator& tls, GrpcAccessLoggerCacheSharedPtr access_logger_cache, const std::vector<Formatter::CommandParserPtr>& commands) : Common::ImplBase(std::move(filter)), tls_slot_(tls.allocateSlot()), access_logger_cache_(std::move(access_logger_cache)), filter_state_objects_to_log_(getFilterStateObjectsToLog(config)), custom_tags_(getCustomTags(config, commands)) { ... }注意构造函数的最后一个参数commands—— 它正是从config.formatters()解析得到的命令解析器集合。这个集合同时被传入:
OpenTelemetryFormatter(用于body和attributes的格式化),见access_log_impl.cc中的body_formatter_与attributes_formatter_初始化;getCustomTags(config, commands),用于构建 custom tags。
而在 source/extensions/access_loggers/open_telemetry/otlp_log_utils.cc 中,getCustomTags的实现为:
std::vector<Tracing::CustomTagConstSharedPtr> getCustomTags( const envoy::extensions::access_loggers::open_telemetry::v3::OpenTelemetryAccessLogConfig& config, const Formatter::CommandParserPtrVector& command_parsers) { std::vector<Tracing::CustomTagConstSharedPtr> custom_tags; for (const auto& custom_tag : config.custom_tags()) { custom_tags.push_back(Tracing::CustomTagUtility::createCustomTag(custom_tag, command_parsers)); } return custom_tags; }关键点在于Tracing::CustomTagUtility::createCustomTag。从 source/common/tracing/custom_tag_impl.cc 可以看到,CustomTag的oneof type中有五种取值:literal、environment、request_header、metadata与value。其中value类型的标签值就是一段 Envoy 访问日志格式化字符串(见 api/envoy/type/tracing/v3/custom_tag.proto 中value字段的注释:与 HTTP 访问日志使用相同的 format specifier),它由FormatterCustomTag负责解析:
FormatterCustomTag::FormatterCustomTag(absl::string_view tag, absl::string_view value, const Formatter::CommandParserPtrVector& command_parsers) : tag_(tag) { auto formatter_or = Formatter::FormatterImpl::create(value, true, command_parsers); THROW_IF_NOT_OK_REF(formatter_or.status()); formatter_ = std::move(formatter_or.value()); }缺陷根因正是:修复前,getCustomTags创建 custom tag 时没有把配置的command_parsers传递进去。于是value中的 formatter 扩展命令(如 CEL 表达式)在解析阶段就找不到对应解析器,进而产生Not supported field in StreamInfo错误;而body/attributes走的OpenTelemetryFormatter路径一直正确地接收了commands,所以同样的命令在那里可用。
本次修复的核心动作,就是将配置解析得到的命令解析器集合显式地贯穿到 custom-tag 创建流程中(getCustomTags增加command_parsers参数并转发给createCustomTag)。修复覆盖 gRPC 与 HTTP 两种访问日志变体,因为两者共用同一套 custom-tag 构建逻辑。
修复后的完整调用链
修复后,从配置到日志属性的完整链路如下(以 gRPC 变体为例,HTTP 变体走 http_access_log_impl.cc 的HttpAccessLog,逻辑等价):
- 配置解析:工厂
AccessLogFactory::createAccessLogInstance(source/extensions/access_loggers/open_telemetry/config.cc)调用Formatter::SubstitutionFormatStringUtils::parseFormatters(proto_config.formatters(), context, std::move(command_parsers))生成commands向量; - 实例构造:
commands被同时传给OpenTelemetryFormatter(body/attributes)与getCustomTags(config, commands)(custom tags); - custom tag 创建:
getCustomTags将commands传给CustomTagUtility::createCustomTag,其中value类型标签经FormatterCustomTag用FormatterImpl::create(value, true, command_parsers)解析成 formatter 提供器; - 日志生成:
emitLog中调用addCustomTagsToAttributes(otlp_log_utils.cc),将 custom tag 的格式化结果写入LogRecord的attributes。
其中addCustomTagsToAttributes的实现值得注意:它通过一个临时AccessLogCommon调用每个 custom tag 的applyLog,再遍历temp_log.custom_tags()复制到 OTLP attributes;如果请求上下文不存在(如 TCP 场景),会回退到空请求头映射,保证 custom tags 依然可用:
void addCustomTagsToAttributes(const std::vector<Tracing::CustomTagConstSharedPtr>& custom_tags, const Formatter::Context& context, const StreamInfo::StreamInfo& stream_info, opentelemetry::proto::logs::v1::LogRecord& log_entry) { if (custom_tags.empty()) { return; } const Http::RequestHeaderMap* headers_ptr = context.requestHeaders().has_value() ? &static_cast<const Http::RequestHeaderMap&>(context.requestHeaders().value()) : Http::StaticEmptyHeaders::get().request_headers.get(); ... for (const auto& custom_tag : custom_tags) { custom_tag->applyLog(temp_log, tag_context); } for (const auto& [key, value] : temp_log.custom_tags()) { auto* attr = log_entry.add_attributes(); attr->set_key(key); attr->mutable_value()->set_string_value(value); } }测试验证:custom_tags 正确使用配置的 formatter
修复在 test/extensions/access_loggers/open_telemetry/otlp_log_utils_test.cc 中有专门的回归测试CustomTagsUseConfiguredFormatters(注释中引用了 issue #45453)。测试构造了一个最小命令解析器FakeCommandParser,将%FAKE_COMMAND%解析为固定值fake_value:
TEST(OtlpLogUtilsTest, CustomTagsUseConfiguredFormatters) { ... envoy::extensions::access_loggers::open_telemetry::v3::OpenTelemetryAccessLogConfig config; auto* tag = config.add_custom_tags(); tag->set_tag("fmt_tag"); tag->set_value("%FAKE_COMMAND%"); std::vector<Formatter::CommandParserPtr> commands; commands.push_back(std::make_unique<FakeCommandParser>()); auto custom_tags = getCustomTags(config, commands); ... addCustomTagsToAttributes(custom_tags, context, stream_info, log_entry); opentelemetry::proto::logs::v1::LogRecord expected; auto* attr = expected.add_attributes(); attr->set_key("fmt_tag"); attr->mutable_value()->set_string_value("fake_value"); EXPECT_TRUE(TestUtility::protoEqual(log_entry, expected)); }该测试直接验证了“配置的命令解析器被用于 custom-tag 创建”这一修复要点。此外,同一测试文件中还有两个补充用例:AddCustomTagsToAttributesEmptyTags(空 custom tags 列表是 no-op)与AddCustomTagsToAttributesWithoutRequestHeaders(请求头不可用时回退到空头映射,模拟 TCP 场景),保证修复没有破坏其他路径。
实战配置:在 custom_tags 中安全使用 formatter 命令
基于 logs_service.proto 中定义的字段(log_name、grpc_service/http_service、body、attributes、formatters、custom_tags等),下面给出一个同时使用formatters与custom_tags的完整配置示例(gRPC 传输):
access_log: - name: envoy.access_loggers.open_telemetry typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.open_telemetry.v3.OpenTelemetryAccessLogConfig log_name: "envoy_accesslog" grpc_service: envoy_grpc: cluster_name: opentelemetry_collector # 挂载 formatter 插件(扩展类别 envoy.formatter),供下方所有 %COMMAND% 使用 formatters: - name: envoy.formatter.cel typed_config: "@type": type.googleapis.com/envoy.extensions.formatter.cel.v3.Cel body: string_value: "%REQ(:METHOD)% %PROTOCOL% %RESPONSE_CODE%" attributes: values: - key: "user_agent" value: string_value: "%REQ(USER-AGENT)%" custom_tags: # literal 类型:静态值 - tag: "region" literal: value: "cn-north-7" # environment 类型:读环境变量,带默认值 - tag: "deploy_env" environment: name: "ENVOY_DEPLOY_ENV" default_value: "dev" # request_header 类型:读请求头 - tag: "trace_client" request_header: name: "X-Client-Id" default_value: "unknown" # value 类型:格式化字符串,可使用 formatters 挂载的命令 - tag: "expr_result" value: "%CEL(environment['HOSTNAME'])%"配置要点:
custom_tags中tag字段指定标签名(写入日志的 attribute key),oneof type中literal/environment/request_header/metadata/value五选一;- 只有
value类型的自定义标签会走 formatter 解析(对应FormatterCustomTag),因此要在其中使用 formatter 扩展命令,必须保证formatters字段已挂载对应插件——这正是本次修复保证的前提条件; formatters同时服务于body、attributes与custom_tags,三者的命令解析行为在修复后保持一致;- 传输方式三选一:
common_config.grpc_service(已弃用,将于 3.0 移除)、grpc_service或http_service,工厂代码(config.cc)会校验恰好配置一个; - 若使用
http_service,其request_headers_to_add也支持 substitution formatter(但不能访问 HTTP/连接属性,可加载环境变量、文件或密钥),集成测试 access_log_integration_test.cc 中的HttpExportWithFormatterHeader用例验证了%HOSTNAME%在导出 HTTP 请求头中的展开。
常见问题与排查建议
- 现象:自定义标签的值中使用 formatter 命令,日志导出时报
Not supported field in StreamInfo。排查:确认使用的是本次修复之后的 Envoy 版本(修复前 gRPC 与 HTTP 两种访问日志器均存在此缺陷);确认formatters中已挂载对应插件,且custom_tags中标签类型为value(其余类型不参与 formatter 解析)。 - 现象:
body/attributes中命令可用,但custom_tags中不可用。这正是本次修复前的典型症状,根因是命令解析器未传递到 custom-tag 创建路径,而非命令本身非法。 - 现象:未配置任何 custom tags 或请求头不可用。源码中已做兜底:空标签列表直接返回(见
addCustomTagsToAttributes开头的custom_tags.empty()检查),请求头缺失时使用静态空头映射,不会导致崩溃或导出失败。
小结
本次修复的实质是在 OpenTelemetry 访问日志器中统一 formatter 命令的解析入口:将从formatters字段解析得到的命令解析器显式传递到custom_tags的构建过程,消除了body、attributes与custom_tags三处格式化行为的不一致。源码层面,修复体现在 otlp_log_utils.cc 的getCustomTags签名与其调用方(access_log_impl.cc 与 http_access_log_impl.cc),并由 otlp_log_utils_test.cc 的CustomTagsUseConfiguredFormatters回归测试锁定。现在,你可以在custom_tags中放心组合 formatter 插件,构建更丰富的 OTLP 日志属性。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考