diff --git a/source/common/http/BUILD b/source/common/http/BUILD index 060f5bba03da9..3aec4e830a19e 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -520,6 +520,22 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "http_service_headers_lib", + srcs = ["http_service_headers.cc"], + hdrs = ["http_service_headers.h"], + deps = [ + ":headers_lib", + "//envoy/formatter:substitution_formatter_interface", + "//envoy/http:header_map_interface", + "//source/common/formatter:substitution_format_string_lib", + "//source/common/formatter:substitution_formatter_lib", + "//source/common/stream_info:stream_info_lib", + "//source/server:generic_factory_context_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "user_agent_lib", srcs = ["user_agent.cc"], diff --git a/source/common/http/http_service_headers.cc b/source/common/http/http_service_headers.cc new file mode 100644 index 0000000000000..058912a63c4ba --- /dev/null +++ b/source/common/http/http_service_headers.cc @@ -0,0 +1,52 @@ +#include "source/common/http/http_service_headers.h" + +#include "source/common/formatter/substitution_format_string.h" +#include "source/common/formatter/substitution_formatter.h" +#include "source/server/generic_factory_context.h" + +namespace Envoy { +namespace Http { + +HttpServiceHeadersApplicator::HttpServiceHeadersApplicator( + const envoy::config::core::v3::HttpService& http_service, + Server::Configuration::ServerFactoryContext& server_context, absl::Status& creation_status) + : stream_info_(server_context.timeSource(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain) { + + // Formatters can only be instantiated on the main thread because some create thread local + // storage. + ASSERT_IS_MAIN_OR_TEST_THREAD(); + + Server::GenericFactoryContextImpl generic_context{server_context, + server_context.messageValidationVisitor()}; + + auto commands = Formatter::SubstitutionFormatStringUtils::parseFormatters( + http_service.formatters(), generic_context); + SET_AND_RETURN_IF_NOT_OK(commands.status(), creation_status); + + for (const auto& header_value_option : http_service.request_headers_to_add()) { + const auto& header = header_value_option.header(); + if (!header.value().empty()) { + auto formatter_or_error = Formatter::FormatterImpl::create(header.value(), false, *commands); + SET_AND_RETURN_IF_NOT_OK(formatter_or_error.status(), creation_status); + formatted_headers_.emplace_back(LowerCaseString(header.key()), + std::move(formatter_or_error.value())); + } else { + static_headers_.emplace_back(LowerCaseString(header.key()), header.raw_value()); + } + } +} + +void HttpServiceHeadersApplicator::apply(RequestHeaderMap& headers) const { + for (const auto& header_pair : static_headers_) { + headers.setReference(header_pair.first, header_pair.second); + } + if (!formatted_headers_.empty()) { + for (const auto& header_pair : formatted_headers_) { + headers.setCopy(header_pair.first, header_pair.second->format({}, stream_info_)); + } + } +} + +} // namespace Http +} // namespace Envoy diff --git a/source/common/http/http_service_headers.h b/source/common/http/http_service_headers.h new file mode 100644 index 0000000000000..3eaad3146f381 --- /dev/null +++ b/source/common/http/http_service_headers.h @@ -0,0 +1,44 @@ +#pragma once + +#include "envoy/config/core/v3/http_service.pb.h" +#include "envoy/formatter/substitution_formatter_base.h" +#include "envoy/http/header_map.h" +#include "envoy/server/factory_context.h" + +#include "source/common/http/headers.h" +#include "source/common/stream_info/stream_info_impl.h" + +namespace Envoy { +namespace Http { + +/** + * Parses and applies request_headers_to_add from an HTTP service configuration. + * + * Separates headers into static (plain string value) and formatted (substitution + * formatter) groups. Static headers are evaluated once at construction time. + * Formatted headers are re-evaluated on each apply() call, so that + * runtime updates such as SDS secret rotation are reflected in outgoing requests. + */ +class HttpServiceHeadersApplicator { +public: + HttpServiceHeadersApplicator(const envoy::config::core::v3::HttpService& http_service, + Server::Configuration::ServerFactoryContext& server_context, + absl::Status& creation_status); + + /** + * Apply all parsed headers to the outgoing request message. + */ + void apply(RequestHeaderMap& headers) const; + +private: + std::vector> static_headers_; + std::vector> formatted_headers_; + + // A `StreamInfo` is required, but in this context we don't have one, so create an empty one. + // This allows formatters that don't require any stream info to succeed, such as extensions that + // load data externally for API keys and similar. + const StreamInfo::StreamInfoImpl stream_info_; +}; + +} // namespace Http +} // namespace Envoy diff --git a/source/extensions/stat_sinks/open_telemetry/BUILD b/source/extensions/stat_sinks/open_telemetry/BUILD index 133140aa0ef2f..7030fae629b9e 100644 --- a/source/extensions/stat_sinks/open_telemetry/BUILD +++ b/source/extensions/stat_sinks/open_telemetry/BUILD @@ -50,6 +50,7 @@ envoy_cc_library( "//source/common/http:async_client_lib", "//source/common/http:async_client_utility_lib", "//source/common/http:header_map_lib", + "//source/common/http:http_service_headers_lib", "//source/common/http:message_lib", "//source/common/http:utility_lib", "//source/common/protobuf", diff --git a/source/extensions/stat_sinks/open_telemetry/config.cc b/source/extensions/stat_sinks/open_telemetry/config.cc index d6e91d5cdcee9..1ae89c68a14cf 100644 --- a/source/extensions/stat_sinks/open_telemetry/config.cc +++ b/source/extensions/stat_sinks/open_telemetry/config.cc @@ -50,7 +50,7 @@ OpenTelemetrySinkFactory::createStatsSink(const Protobuf::Message& config, case SinkConfig::ProtocolSpecifierCase::kHttpService: { std::shared_ptr http_metrics_exporter = std::make_shared(server.clusterManager(), - sink_config.http_service()); + sink_config.http_service(), server); return std::make_unique( otlp_metrics_flusher, http_metrics_exporter, diff --git a/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.cc b/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.cc index 59150e653bad2..7ed44b3c4e2a2 100644 --- a/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.cc +++ b/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.cc @@ -1,6 +1,7 @@ #include "source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.h" #include "source/common/common/enum_to_int.h" +#include "source/common/common/fmt.h" #include "source/common/http/headers.h" #include "source/common/http/message_impl.h" #include "source/common/http/utility.h" @@ -14,12 +15,16 @@ namespace OpenTelemetry { OpenTelemetryHttpMetricsExporter::OpenTelemetryHttpMetricsExporter( Upstream::ClusterManager& cluster_manager, - const envoy::config::core::v3::HttpService& http_service) + const envoy::config::core::v3::HttpService& http_service, + Server::Configuration::ServerFactoryContext& server_context) : cluster_manager_(cluster_manager), http_service_(http_service) { - // Parse headers at construction time to avoid copies per request. - for (const auto& header_value_option : http_service_.request_headers_to_add()) { - parsed_headers_to_add_.push_back({Http::LowerCaseString(header_value_option.header().key()), - header_value_option.header().value()}); + // Create headers applicator to handle substitution formatters. + absl::Status creation_status; + headers_applicator_ = std::make_unique( + http_service_, server_context, creation_status); + if (!creation_status.ok()) { + throw EnvoyException(fmt::format("Failed to create HttpServiceHeadersApplicator: {}", + creation_status.message())); } } @@ -49,10 +54,8 @@ void OpenTelemetryHttpMetricsExporter::send(MetricsExportRequestPtr&& metrics) { // User-Agent header follows the OTLP specification. message->headers().setReferenceUserAgent(AccessLoggers::OpenTelemetry::getOtlpUserAgentHeader()); - // Add custom headers from config. - for (const auto& header_pair : parsed_headers_to_add_) { - message->headers().setReference(header_pair.first, header_pair.second); - } + // Apply custom headers from config using the common helper. + headers_applicator_->apply(message->headers()); message->body().add(request_body); const auto options = diff --git a/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.h b/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.h index c7426542f8a6a..2751f3fe2754b 100644 --- a/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.h +++ b/source/extensions/stat_sinks/open_telemetry/open_telemetry_http_impl.h @@ -5,6 +5,7 @@ #include "source/common/http/async_client_impl.h" #include "source/common/http/async_client_utility.h" +#include "source/common/http/http_service_headers.h" #include "source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.h" namespace Envoy { @@ -21,7 +22,8 @@ class OpenTelemetryHttpMetricsExporter : public OtlpMetricsExporter, public Logger::Loggable { public: OpenTelemetryHttpMetricsExporter(Upstream::ClusterManager& cluster_manager, - const envoy::config::core::v3::HttpService& http_service); + const envoy::config::core::v3::HttpService& http_service, + Server::Configuration::ServerFactoryContext& server_context); // OtlpMetricsExporter void send(MetricsExportRequestPtr&& metrics) override; @@ -36,7 +38,7 @@ class OpenTelemetryHttpMetricsExporter : public OtlpMetricsExporter, envoy::config::core::v3::HttpService http_service_; // Track active HTTP requests to cancel them on destruction. Http::AsyncClientRequestTracker active_requests_; - std::vector> parsed_headers_to_add_; + std::unique_ptr headers_applicator_; }; } // namespace OpenTelemetry diff --git a/source/extensions/tracers/zipkin/BUILD b/source/extensions/tracers/zipkin/BUILD index 2f9ad9778c748..c15cd134cbd8d 100644 --- a/source/extensions/tracers/zipkin/BUILD +++ b/source/extensions/tracers/zipkin/BUILD @@ -47,6 +47,7 @@ envoy_cc_library( "//source/common/config:utility_lib", "//source/common/http:async_client_utility_lib", "//source/common/http:header_map_lib", + "//source/common/http:http_service_headers_lib", "//source/common/http:message_lib", "//source/common/http:utility_lib", "//source/common/json:json_loader_lib", diff --git a/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc b/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc index cb90cc3d7f129..100e809293a25 100644 --- a/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc +++ b/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc @@ -5,6 +5,7 @@ #include "envoy/config/trace/v3/zipkin.pb.h" #include "source/common/common/enum_to_int.h" +#include "source/common/common/fmt.h" #include "source/common/config/utility.h" #include "source/common/http/headers.h" #include "source/common/http/message_impl.h" @@ -70,10 +71,13 @@ Driver::Driver(const envoy::config::trace::v3::ZipkinConfig& zipkin_config, collector_->endpoint_ = path; } - // Parse headers from HttpService - for (const auto& header_option : http_service.request_headers_to_add()) { - const auto& header_value = header_option.header(); - collector_->request_headers_.emplace_back(header_value.key(), header_value.value()); + // Create headers applicator using the common helper for substitution formatter support + absl::Status creation_status; + collector_->headers_applicator_ = std::make_unique( + http_service, context, creation_status); + if (!creation_status.ok()) { + throw EnvoyException(fmt::format("Failed to create HttpServiceHeadersApplicator: {}", + creation_status.message())); } } else { if (zipkin_config.collector_cluster().empty() || zipkin_config.collector_endpoint().empty()) { @@ -210,10 +214,14 @@ void ReporterImpl::flushSpans() { ? Http::Headers::get().ContentTypeValues.Protobuf : Http::Headers::get().ContentTypeValues.Json); - // Add custom headers from collector configuration - for (const auto& header : collector_->request_headers_) { - // Replace any existing header with the configured value - message->headers().setCopy(header.first, header.second); + // Apply custom headers from collector configuration using common helper + if (collector_->headers_applicator_) { + collector_->headers_applicator_->apply(message->headers()); + } else { + // Legacy header application for backward compatibility + for (const auto& header : collector_->request_headers_) { + message->headers().setCopy(header.first, header.second); + } } message->body().add(request_body); diff --git a/source/extensions/tracers/zipkin/zipkin_tracer_impl.h b/source/extensions/tracers/zipkin/zipkin_tracer_impl.h index 90435368fcd2e..4da8d22f2076f 100644 --- a/source/extensions/tracers/zipkin/zipkin_tracer_impl.h +++ b/source/extensions/tracers/zipkin/zipkin_tracer_impl.h @@ -11,6 +11,7 @@ #include "envoy/upstream/cluster_manager.h" #include "source/common/http/async_client_utility.h" +#include "source/common/http/http_service_headers.h" #include "source/common/upstream/cluster_update_tracker.h" #include "source/extensions/tracers/zipkin/span_buffer.h" #include "source/extensions/tracers/zipkin/tracer.h" @@ -63,6 +64,9 @@ struct CollectorInfo { // Only available when using HttpService configuration via request_headers_to_add. // Legacy configuration does not support custom headers. std::vector> request_headers_; + + // Headers applicator for substitution formatter support when using HttpService configuration + std::unique_ptr headers_applicator_; }; using CollectorInfoConstSharedPtr = std::shared_ptr;