From b0bd18fd78a836222dafd88185c2665c90a3cc2b Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Wed, 14 Jan 2026 14:51:27 -0800 Subject: [PATCH 1/9] Add FIELD accessor support for peer metadata in tracing Signed-off-by: Petr McAllister --- .../filters/http/peer_metadata/filter.cc | 11 +-- .../filters/http/peer_metadata/filter_test.cc | 79 +++++++++++++++++-- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/source/extensions/filters/http/peer_metadata/filter.cc b/source/extensions/filters/http/peer_metadata/filter.cc index f4b93cad44..a56a3b8b32 100644 --- a/source/extensions/filters/http/peer_metadata/filter.cc +++ b/source/extensions/filters/http/peer_metadata/filter.cc @@ -292,12 +292,13 @@ void FilterConfig::setFilterState(StreamInfo::StreamInfo& info, bool downstream, const absl::string_view key = downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer; if (!info.filterState()->hasDataWithName(key)) { - // Use CelState to allow operation filter_state.upstream_peer.labels['role'] - auto pb = value.serializeAsProto(); - auto peer_info = std::make_unique(FilterConfig::peerInfoPrototype()); - peer_info->setValue(absl::string_view(pb->SerializeAsString())); + // Store WorkloadMetadataObject directly for both FIELD accessor and CEL support. + // WorkloadMetadataObject implements: + // - hasFieldSupport() + getField() for FIELD accessor in formatters + // - serializeAsProto() for CEL expressions + auto workload_metadata = std::make_unique(value); info.filterState()->setData( - key, std::move(peer_info), StreamInfo::FilterState::StateType::Mutable, + key, std::move(workload_metadata), StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); } else { ENVOY_LOG(debug, "Duplicate peer metadata, skipping"); diff --git a/source/extensions/filters/http/peer_metadata/filter_test.cc b/source/extensions/filters/http/peer_metadata/filter_test.cc index 995da18b22..a13d33fbb2 100644 --- a/source/extensions/filters/http/peer_metadata/filter_test.cc +++ b/source/extensions/filters/http/peer_metadata/filter_test.cc @@ -79,13 +79,12 @@ class PeerMetadataTest : public testing::Test { downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer)); } void checkPeerNamespace(bool downstream, const std::string& expected) { - const auto* cel_state = + const auto* peer_info = stream_info_.filterState() - ->getDataReadOnly( + ->getDataReadOnly( downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer); - Protobuf::Struct obj; - ASSERT_TRUE(obj.ParseFromString(cel_state->value().data())); - EXPECT_EQ(expected, extractString(obj, "namespace")); + ASSERT_NE(peer_info, nullptr); + EXPECT_EQ(expected, peer_info->namespace_name_); } absl::string_view extractString(const Protobuf::Struct& metadata, absl::string_view key) { @@ -488,6 +487,76 @@ TEST_F(PeerMetadataTest, UpstreamMXPropagationSkipPassthrough) { checkNoPeer(false); } +TEST_F(PeerMetadataTest, FieldAccessorSupport) { + const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", + "v1alpha3", "myapp", "v1", Istio::Common::WorkloadType::Pod, ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) + .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) + -> std::optional { + if (absl::StartsWith(address->asStringView(), "127.0.0.1")) { + return {pod}; + } + return {}; + })); + initialize(R"EOF( + downstream_discovery: + - workload_discovery: {} + )EOF"); + + const auto* peer_info = + stream_info_.filterState() + ->getDataReadOnly(Istio::Common::DownstreamPeer); + ASSERT_NE(peer_info, nullptr); + + // Test hasFieldSupport + EXPECT_TRUE(peer_info->hasFieldSupport()); + + // Test getField() for all 9 fields + EXPECT_EQ("foo", std::get(peer_info->getField("workload"))); + EXPECT_EQ("default", std::get(peer_info->getField("namespace"))); + EXPECT_EQ("my-cluster", std::get(peer_info->getField("cluster"))); + EXPECT_EQ("foo-service", std::get(peer_info->getField("service"))); + EXPECT_EQ("v1alpha3", std::get(peer_info->getField("revision"))); + EXPECT_EQ("myapp", std::get(peer_info->getField("app"))); + EXPECT_EQ("v1", std::get(peer_info->getField("version"))); + EXPECT_EQ("pod", std::get(peer_info->getField("type"))); + EXPECT_EQ("pod-foo-1234", std::get(peer_info->getField("name"))); +} + +TEST_F(PeerMetadataTest, CelExpressionCompatibility) { + const WorkloadMetadataObject pod("pod-bar-5678", "test-cluster", "production", "bar", + "bar-service", "v2", "barapp", "v2", + Istio::Common::WorkloadType::Pod, ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) + .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) + -> std::optional { + if (absl::StartsWith(address->asStringView(), "127.0.0.1")) { + return {pod}; + } + return {}; + })); + initialize(R"EOF( + downstream_discovery: + - workload_discovery: {} + )EOF"); + + const auto* peer_info = + stream_info_.filterState() + ->getDataReadOnly(Istio::Common::DownstreamPeer); + ASSERT_NE(peer_info, nullptr); + + // Test that serializeAsProto still works for CEL compatibility + auto proto = peer_info->serializeAsProto(); + ASSERT_NE(proto, nullptr); + + // Verify the protobuf contains expected data + const auto* struct_proto = dynamic_cast(proto.get()); + ASSERT_NE(struct_proto, nullptr); + EXPECT_EQ("production", extractString(*struct_proto, "namespace")); + EXPECT_EQ("bar", extractString(*struct_proto, "workload")); + EXPECT_EQ("test-cluster", extractString(*struct_proto, "cluster")); +} + } // namespace } // namespace PeerMetadata } // namespace HttpFilters From 378fcfb0e3aafd2036b65b14b93218672f027973 Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Wed, 14 Jan 2026 15:48:11 -0800 Subject: [PATCH 2/9] lint Signed-off-by: Petr McAllister --- .../filters/http/peer_metadata/filter_test.cc | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/source/extensions/filters/http/peer_metadata/filter_test.cc b/source/extensions/filters/http/peer_metadata/filter_test.cc index a13d33fbb2..e0c77b7b3c 100644 --- a/source/extensions/filters/http/peer_metadata/filter_test.cc +++ b/source/extensions/filters/http/peer_metadata/filter_test.cc @@ -79,10 +79,8 @@ class PeerMetadataTest : public testing::Test { downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer)); } void checkPeerNamespace(bool downstream, const std::string& expected) { - const auto* peer_info = - stream_info_.filterState() - ->getDataReadOnly( - downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer); + const auto* peer_info = stream_info_.filterState()->getDataReadOnly( + downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer); ASSERT_NE(peer_info, nullptr); EXPECT_EQ(expected, peer_info->namespace_name_); } @@ -504,8 +502,7 @@ TEST_F(PeerMetadataTest, FieldAccessorSupport) { )EOF"); const auto* peer_info = - stream_info_.filterState() - ->getDataReadOnly(Istio::Common::DownstreamPeer); + stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeer); ASSERT_NE(peer_info, nullptr); // Test hasFieldSupport @@ -541,8 +538,7 @@ TEST_F(PeerMetadataTest, CelExpressionCompatibility) { )EOF"); const auto* peer_info = - stream_info_.filterState() - ->getDataReadOnly(Istio::Common::DownstreamPeer); + stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeer); ASSERT_NE(peer_info, nullptr); // Test that serializeAsProto still works for CEL compatibility From bbbf405e3d4aa85e8bf1ba3f580f61084c563d2a Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Wed, 14 Jan 2026 16:05:20 -0800 Subject: [PATCH 3/9] address PR comments, fix tests Signed-off-by: Petr McAllister --- .../filters/http/istio_stats/istio_stats.cc | 16 ++++++++++------ .../filters/http/peer_metadata/filter_test.cc | 18 +++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/source/extensions/filters/http/istio_stats/istio_stats.cc b/source/extensions/filters/http/istio_stats/istio_stats.cc index 9bedf04292..0f1882fc0b 100644 --- a/source/extensions/filters/http/istio_stats/istio_stats.cc +++ b/source/extensions/filters/http/istio_stats/istio_stats.cc @@ -129,9 +129,15 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { reporter == Reporter::ServerSidecar || reporter == Reporter::ServerGateway ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer; - // This's a workaround before FilterStateObject support operation like `.labels['role']`. - // The workaround is to use CelState to store the peer metadata. - // Rebuild the WorkloadMetadataObject from the CelState. + + // Try reading as WorkloadMetadataObject first (new format) + const auto* peer_info = + filter_state.getDataReadOnly(filter_state_key); + if (peer_info) { + return *peer_info; + } + + // Fall back to CelState for backward compatibility with older deployments const auto* cel_state = filter_state.getDataReadOnly( filter_state_key); @@ -144,7 +150,7 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { return {}; } - Istio::Common::WorkloadMetadataObject peer_info( + return Istio::Common::WorkloadMetadataObject( extractString(obj, Istio::Common::InstanceNameToken), extractString(obj, Istio::Common::ClusterNameToken), extractString(obj, Istio::Common::NamespaceNameToken), @@ -155,8 +161,6 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { extractString(obj, Istio::Common::AppVersionToken), Istio::Common::fromSuffix(extractString(obj, Istio::Common::WorkloadTypeToken)), extractString(obj, Istio::Common::IdentityToken)); - - return peer_info; } // Process-wide context shared with all filter instances. diff --git a/source/extensions/filters/http/peer_metadata/filter_test.cc b/source/extensions/filters/http/peer_metadata/filter_test.cc index e0c77b7b3c..26546d8bf6 100644 --- a/source/extensions/filters/http/peer_metadata/filter_test.cc +++ b/source/extensions/filters/http/peer_metadata/filter_test.cc @@ -509,15 +509,15 @@ TEST_F(PeerMetadataTest, FieldAccessorSupport) { EXPECT_TRUE(peer_info->hasFieldSupport()); // Test getField() for all 9 fields - EXPECT_EQ("foo", std::get(peer_info->getField("workload"))); - EXPECT_EQ("default", std::get(peer_info->getField("namespace"))); - EXPECT_EQ("my-cluster", std::get(peer_info->getField("cluster"))); - EXPECT_EQ("foo-service", std::get(peer_info->getField("service"))); - EXPECT_EQ("v1alpha3", std::get(peer_info->getField("revision"))); - EXPECT_EQ("myapp", std::get(peer_info->getField("app"))); - EXPECT_EQ("v1", std::get(peer_info->getField("version"))); - EXPECT_EQ("pod", std::get(peer_info->getField("type"))); - EXPECT_EQ("pod-foo-1234", std::get(peer_info->getField("name"))); + EXPECT_EQ("foo", std::get(peer_info->getField("workload"))); + EXPECT_EQ("default", std::get(peer_info->getField("namespace"))); + EXPECT_EQ("my-cluster", std::get(peer_info->getField("cluster"))); + EXPECT_EQ("foo-service", std::get(peer_info->getField("service"))); + EXPECT_EQ("v1alpha3", std::get(peer_info->getField("revision"))); + EXPECT_EQ("myapp", std::get(peer_info->getField("app"))); + EXPECT_EQ("v1", std::get(peer_info->getField("version"))); + EXPECT_EQ("pod", std::get(peer_info->getField("type"))); + EXPECT_EQ("pod-foo-1234", std::get(peer_info->getField("name"))); } TEST_F(PeerMetadataTest, CelExpressionCompatibility) { From 90d4b0862728552a3f42a2f95432dab624acb0d6 Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Wed, 14 Jan 2026 16:58:45 -0800 Subject: [PATCH 4/9] fix test when labels are not present Signed-off-by: Petr McAllister --- extensions/common/metadata_object.cc | 9 ++++----- .../filters/http/istio_stats/istio_stats.cc | 14 +++++++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/extensions/common/metadata_object.cc b/extensions/common/metadata_object.cc index e5f763b233..caa9a0d5db 100644 --- a/extensions/common/metadata_object.cc +++ b/extensions/common/metadata_object.cc @@ -95,11 +95,10 @@ Envoy::ProtobufTypes::MessagePtr WorkloadMetadataObject::serializeAsProto() cons (*message->mutable_fields())[IdentityToken].set_string_value(identity_); } - if (!labels_.empty()) { - auto* labels = (*message->mutable_fields())[LabelsToken].mutable_struct_value(); - for (const auto& l : labels_) { - (*labels->mutable_fields())[std::string(l.first)].set_string_value(std::string(l.second)); - } + // Always create labels field for CEL compatibility, even if empty + auto* labels = (*message->mutable_fields())[LabelsToken].mutable_struct_value(); + for (const auto& l : labels_) { + (*labels->mutable_fields())[std::string(l.first)].set_string_value(std::string(l.second)); } return message; diff --git a/source/extensions/filters/http/istio_stats/istio_stats.cc b/source/extensions/filters/http/istio_stats/istio_stats.cc index 0f1882fc0b..50b408ea3c 100644 --- a/source/extensions/filters/http/istio_stats/istio_stats.cc +++ b/source/extensions/filters/http/istio_stats/istio_stats.cc @@ -150,7 +150,7 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { return {}; } - return Istio::Common::WorkloadMetadataObject( + Istio::Common::WorkloadMetadataObject peer_info( extractString(obj, Istio::Common::InstanceNameToken), extractString(obj, Istio::Common::ClusterNameToken), extractString(obj, Istio::Common::NamespaceNameToken), @@ -161,6 +161,18 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { extractString(obj, Istio::Common::AppVersionToken), Istio::Common::fromSuffix(extractString(obj, Istio::Common::WorkloadTypeToken)), extractString(obj, Istio::Common::IdentityToken)); + + // Extract labels from the "labels" field + const auto& labels_it = obj.fields().find(Istio::Common::LabelsToken); + if (labels_it != obj.fields().end() && labels_it->second.has_struct_value()) { + std::vector> labels; + for (const auto& label : labels_it->second.struct_value().fields()) { + labels.push_back({std::string(label.first), std::string(label.second.string_value())}); + } + peer_info.setLabels(labels); + } + + return peer_info; } // Process-wide context shared with all filter instances. From a10e100a4a40c73ce12e496cf8b22de458424fd8 Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Wed, 14 Jan 2026 21:59:00 -0800 Subject: [PATCH 5/9] test fixes Signed-off-by: Petr McAllister --- extensions/common/metadata_object.cc | 10 +++++++--- .../extensions/filters/http/istio_stats/istio_stats.cc | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/extensions/common/metadata_object.cc b/extensions/common/metadata_object.cc index caa9a0d5db..a282615ac7 100644 --- a/extensions/common/metadata_object.cc +++ b/extensions/common/metadata_object.cc @@ -96,9 +96,13 @@ Envoy::ProtobufTypes::MessagePtr WorkloadMetadataObject::serializeAsProto() cons } // Always create labels field for CEL compatibility, even if empty - auto* labels = (*message->mutable_fields())[LabelsToken].mutable_struct_value(); - for (const auto& l : labels_) { - (*labels->mutable_fields())[std::string(l.first)].set_string_value(std::string(l.second)); + if (!labels_.empty()) { + auto* labels = (*message->mutable_fields())[LabelsToken].mutable_struct_value(); + for (const auto& l : labels_) { + (*labels->mutable_fields())[std::string(l.first)].set_string_value(std::string(l.second)); + } + } else { + (*message->mutable_fields())[LabelsToken].mutable_struct_value(); } return message; diff --git a/source/extensions/filters/http/istio_stats/istio_stats.cc b/source/extensions/filters/http/istio_stats/istio_stats.cc index 50b408ea3c..59de25714b 100644 --- a/source/extensions/filters/http/istio_stats/istio_stats.cc +++ b/source/extensions/filters/http/istio_stats/istio_stats.cc @@ -150,7 +150,7 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { return {}; } - Istio::Common::WorkloadMetadataObject peer_info( + Istio::Common::WorkloadMetadataObject result( extractString(obj, Istio::Common::InstanceNameToken), extractString(obj, Istio::Common::ClusterNameToken), extractString(obj, Istio::Common::NamespaceNameToken), @@ -169,10 +169,10 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { for (const auto& label : labels_it->second.struct_value().fields()) { labels.push_back({std::string(label.first), std::string(label.second.string_value())}); } - peer_info.setLabels(labels); + result.setLabels(labels); } - return peer_info; + return result; } // Process-wide context shared with all filter instances. From 11d5500c7048c591eb807144033a793b0b584082 Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Thu, 15 Jan 2026 10:05:42 -0800 Subject: [PATCH 6/9] Store both CelState and WorkloadMetadataObject for CEL and FIELD accessor support Signed-off-by: Petr McAllister --- extensions/common/metadata_object.cc | 3 --- .../filters/http/peer_metadata/filter.cc | 15 +++++++++++---- .../filters/http/peer_metadata/filter_test.cc | 6 ++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/extensions/common/metadata_object.cc b/extensions/common/metadata_object.cc index a282615ac7..e5f763b233 100644 --- a/extensions/common/metadata_object.cc +++ b/extensions/common/metadata_object.cc @@ -95,14 +95,11 @@ Envoy::ProtobufTypes::MessagePtr WorkloadMetadataObject::serializeAsProto() cons (*message->mutable_fields())[IdentityToken].set_string_value(identity_); } - // Always create labels field for CEL compatibility, even if empty if (!labels_.empty()) { auto* labels = (*message->mutable_fields())[LabelsToken].mutable_struct_value(); for (const auto& l : labels_) { (*labels->mutable_fields())[std::string(l.first)].set_string_value(std::string(l.second)); } - } else { - (*message->mutable_fields())[LabelsToken].mutable_struct_value(); } return message; diff --git a/source/extensions/filters/http/peer_metadata/filter.cc b/source/extensions/filters/http/peer_metadata/filter.cc index a56a3b8b32..6e4703b298 100644 --- a/source/extensions/filters/http/peer_metadata/filter.cc +++ b/source/extensions/filters/http/peer_metadata/filter.cc @@ -292,10 +292,17 @@ void FilterConfig::setFilterState(StreamInfo::StreamInfo& info, bool downstream, const absl::string_view key = downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer; if (!info.filterState()->hasDataWithName(key)) { - // Store WorkloadMetadataObject directly for both FIELD accessor and CEL support. - // WorkloadMetadataObject implements: - // - hasFieldSupport() + getField() for FIELD accessor in formatters - // - serializeAsProto() for CEL expressions + // Store CelState for CEL expressions like filter_state.downstream_peer.labels['role'] + auto pb = value.serializeAsProto(); + auto cel_state = std::make_unique(FilterConfig::peerInfoPrototype()); + cel_state->setValue(absl::string_view(pb->SerializeAsString())); + info.filterState()->setData( + key, std::move(cel_state), StreamInfo::FilterState::StateType::Mutable, + StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); + + // Also store WorkloadMetadataObject for FIELD accessor support. + // WorkloadMetadataObject implements hasFieldSupport() + getField() for + // formatters using %FILTER_STATE(downstream_peer:FIELD:fieldname)% syntax. auto workload_metadata = std::make_unique(value); info.filterState()->setData( key, std::move(workload_metadata), StreamInfo::FilterState::StateType::Mutable, diff --git a/source/extensions/filters/http/peer_metadata/filter_test.cc b/source/extensions/filters/http/peer_metadata/filter_test.cc index 26546d8bf6..dcbb7f89e7 100644 --- a/source/extensions/filters/http/peer_metadata/filter_test.cc +++ b/source/extensions/filters/http/peer_metadata/filter_test.cc @@ -537,6 +537,12 @@ TEST_F(PeerMetadataTest, CelExpressionCompatibility) { - workload_discovery: {} )EOF"); + // Verify both CelState and WorkloadMetadataObject can be retrieved + const auto* cel_state = stream_info_.filterState() + ->getDataReadOnly( + Istio::Common::DownstreamPeer); + ASSERT_NE(cel_state, nullptr); + const auto* peer_info = stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeer); ASSERT_NE(peer_info, nullptr); From f88d90987228d7d6bf6a95d377efd6d8ae883d1c Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Thu, 15 Jan 2026 10:52:15 -0800 Subject: [PATCH 7/9] Use separate keys for CelState and WorkloadMetadataObject storage Signed-off-by: Petr McAllister --- extensions/common/metadata_object.h | 5 +++++ source/extensions/filters/http/peer_metadata/filter.cc | 8 +++++--- .../extensions/filters/http/peer_metadata/filter_test.cc | 9 +++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/extensions/common/metadata_object.h b/extensions/common/metadata_object.h index 2a0e65a8e3..5d39e22f3d 100644 --- a/extensions/common/metadata_object.h +++ b/extensions/common/metadata_object.h @@ -27,9 +27,14 @@ namespace Istio { namespace Common { // Filter state key to store the peer metadata under. +// CelState is stored under these keys for CEL expression support. constexpr absl::string_view DownstreamPeer = "downstream_peer"; constexpr absl::string_view UpstreamPeer = "upstream_peer"; +// Filter state keys for WorkloadMetadataObject (FIELD accessor support). +constexpr absl::string_view DownstreamPeerObj = "downstream_peer_obj"; +constexpr absl::string_view UpstreamPeerObj = "upstream_peer_obj"; + // Special filter state key to indicate the filter is done looking for peer metadata. // This is used by network metadata exchange on failure. constexpr absl::string_view NoPeer = "peer_not_found"; diff --git a/source/extensions/filters/http/peer_metadata/filter.cc b/source/extensions/filters/http/peer_metadata/filter.cc index 6e4703b298..cf7a47e91e 100644 --- a/source/extensions/filters/http/peer_metadata/filter.cc +++ b/source/extensions/filters/http/peer_metadata/filter.cc @@ -291,6 +291,8 @@ void FilterConfig::setFilterState(StreamInfo::StreamInfo& info, bool downstream, const PeerInfo& value) const { const absl::string_view key = downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer; + const absl::string_view obj_key = + downstream ? Istio::Common::DownstreamPeerObj : Istio::Common::UpstreamPeerObj; if (!info.filterState()->hasDataWithName(key)) { // Store CelState for CEL expressions like filter_state.downstream_peer.labels['role'] auto pb = value.serializeAsProto(); @@ -300,12 +302,12 @@ void FilterConfig::setFilterState(StreamInfo::StreamInfo& info, bool downstream, key, std::move(cel_state), StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); - // Also store WorkloadMetadataObject for FIELD accessor support. + // Also store WorkloadMetadataObject under a separate key for FIELD accessor support. // WorkloadMetadataObject implements hasFieldSupport() + getField() for - // formatters using %FILTER_STATE(downstream_peer:FIELD:fieldname)% syntax. + // formatters using %FILTER_STATE(downstream_peer_obj:FIELD:fieldname)% syntax. auto workload_metadata = std::make_unique(value); info.filterState()->setData( - key, std::move(workload_metadata), StreamInfo::FilterState::StateType::Mutable, + obj_key, std::move(workload_metadata), StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); } else { ENVOY_LOG(debug, "Duplicate peer metadata, skipping"); diff --git a/source/extensions/filters/http/peer_metadata/filter_test.cc b/source/extensions/filters/http/peer_metadata/filter_test.cc index dcbb7f89e7..b355ed7f51 100644 --- a/source/extensions/filters/http/peer_metadata/filter_test.cc +++ b/source/extensions/filters/http/peer_metadata/filter_test.cc @@ -80,7 +80,7 @@ class PeerMetadataTest : public testing::Test { } void checkPeerNamespace(bool downstream, const std::string& expected) { const auto* peer_info = stream_info_.filterState()->getDataReadOnly( - downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer); + downstream ? Istio::Common::DownstreamPeerObj : Istio::Common::UpstreamPeerObj); ASSERT_NE(peer_info, nullptr); EXPECT_EQ(expected, peer_info->namespace_name_); } @@ -502,7 +502,7 @@ TEST_F(PeerMetadataTest, FieldAccessorSupport) { )EOF"); const auto* peer_info = - stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeer); + stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeerObj); ASSERT_NE(peer_info, nullptr); // Test hasFieldSupport @@ -537,14 +537,15 @@ TEST_F(PeerMetadataTest, CelExpressionCompatibility) { - workload_discovery: {} )EOF"); - // Verify both CelState and WorkloadMetadataObject can be retrieved + // Verify CelState is stored under downstream_peer for CEL expressions const auto* cel_state = stream_info_.filterState() ->getDataReadOnly( Istio::Common::DownstreamPeer); ASSERT_NE(cel_state, nullptr); + // Verify WorkloadMetadataObject is stored under downstream_peer_obj for FIELD accessor const auto* peer_info = - stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeer); + stream_info_.filterState()->getDataReadOnly(Istio::Common::DownstreamPeerObj); ASSERT_NE(peer_info, nullptr); // Test that serializeAsProto still works for CEL compatibility From aa6f7d6a1a14533b977cbb2a9369c12530e64994 Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Thu, 15 Jan 2026 12:05:20 -0800 Subject: [PATCH 8/9] Update istio_stats to use new peer metadata object keys Signed-off-by: Petr McAllister --- .../filters/http/istio_stats/istio_stats.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/source/extensions/filters/http/istio_stats/istio_stats.cc b/source/extensions/filters/http/istio_stats/istio_stats.cc index 59de25714b..2d25f7860d 100644 --- a/source/extensions/filters/http/istio_stats/istio_stats.cc +++ b/source/extensions/filters/http/istio_stats/istio_stats.cc @@ -125,14 +125,17 @@ bool peerInfoRead(Reporter reporter, const StreamInfo::FilterState& filter_state std::optional peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { - const auto& filter_state_key = + const auto& cel_state_key = reporter == Reporter::ServerSidecar || reporter == Reporter::ServerGateway ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer; + const auto& obj_key = reporter == Reporter::ServerSidecar || reporter == Reporter::ServerGateway + ? Istio::Common::DownstreamPeerObj + : Istio::Common::UpstreamPeerObj; - // Try reading as WorkloadMetadataObject first (new format) + // Try reading as WorkloadMetadataObject first (new format, stored under *_obj key) const auto* peer_info = - filter_state.getDataReadOnly(filter_state_key); + filter_state.getDataReadOnly(obj_key); if (peer_info) { return *peer_info; } @@ -140,7 +143,7 @@ peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { // Fall back to CelState for backward compatibility with older deployments const auto* cel_state = filter_state.getDataReadOnly( - filter_state_key); + cel_state_key); if (!cel_state) { return {}; } From 61680b074586ea5c94da303c5a1df0d972ff5de3 Mon Sep 17 00:00:00 2001 From: Petr McAllister Date: Mon, 19 Jan 2026 13:26:03 -0800 Subject: [PATCH 9/9] Changed peerInfoRead to check the *PeerObj Signed-off-by: Petr McAllister --- source/extensions/filters/http/istio_stats/istio_stats.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/extensions/filters/http/istio_stats/istio_stats.cc b/source/extensions/filters/http/istio_stats/istio_stats.cc index 2d25f7860d..3f85652a0b 100644 --- a/source/extensions/filters/http/istio_stats/istio_stats.cc +++ b/source/extensions/filters/http/istio_stats/istio_stats.cc @@ -114,11 +114,12 @@ enum class Reporter { }; // Detect if peer info read is completed by TCP metadata exchange. +// Checks for WorkloadMetadataObject key (set atomically with CelState by peer_metadata filter). bool peerInfoRead(Reporter reporter, const StreamInfo::FilterState& filter_state) { const auto& filter_state_key = reporter == Reporter::ServerSidecar || reporter == Reporter::ServerGateway - ? Istio::Common::DownstreamPeer - : Istio::Common::UpstreamPeer; + ? Istio::Common::DownstreamPeerObj + : Istio::Common::UpstreamPeerObj; return filter_state.hasDataWithName(filter_state_key) || filter_state.hasDataWithName(Istio::Common::NoPeer); }