Repository: envoyproxy/gateway
Pull request #3677 (2024-06-26 07:53:08)

e2e: Add backend health check e2e case via active http
**What this PR does / why we need it**:

Check whether backend health check via active http works in e2e, inspired by https://github.com/projectcontour/contour/blob/main/test/e2e/httpproxy/http_health_checks_test.go.

EG use the default `50%` from Envoy as cluster [panic threshold](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/panic_threshold). In case of health check fail, all endpoints in cluster become unhealthy and panic mode is activated, request is distributed to all unhealthy `echo-basic` endpoints and get response from `echo-basic`, with same behavior as case of health check pass.

To make those two cases distinguishable, in case of health check fail, Envoy should not distribute request to unhealthy endpoint, respond with **503** `no healthy upstream` directly. This requires cluster panic mode is disabled.

Since EG doesn't have knob to configure cluster panic threshold currently, I use EnvoyPatchPolicy to disable cluster panic mode in this PR.

Issues about panic threshold in _contour_:
https://github.com/projectcontour/contour/issues/579.
https://github.com/projectcontour/contour/issues/1846



**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
xref #2417


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/3677?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 68.33%. Comparing base [(`d784c32`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/d784c3237765aa258c3526b32765dd9c3abf6415?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`3509d8b`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/3509d8bdf748d05a0495755aaa653e72f21ba74d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3677      +/-   ##
==========================================
+ Coverage   68.31%   68.33%   +0.01%     
==========================================
  Files         170      170              
  Lines       20780    20780              
==========================================
+ Hits        14196    14199       +3     
+ Misses       5565     5563       -2     
+ Partials     1019     1018       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/3677?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

arkodg: thanks for picking this one up @aoledk !
would be great to also make sure we can check if the `membership_degraded` stat https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats is also bumped
similar to whats done here https://github.com/ShyunnY/gateway/blob/c74f4fe7e7d9cf258e6d013934bffce85bbc9e9d/test/e2e/tests/connection_limit.go#L70
aoledk: We can use `membership_healthy` to tell whether active http health check works, instead of `membership_degraded` which requires upstream host to respond with special `x-envoy-degraded` header [^1], IMO it's out of this issue's scope.

- In case of health check pass, `membership_healthy` should > 0.
- In case of health check fail, `membership_healthy` should == 0.

```
# TYPE envoy_cluster_membership_degraded gauge
envoy_cluster_membership_degraded{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-fail/rule/0"} 0
envoy_cluster_membership_degraded{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-pass/rule/0"} 0

# TYPE envoy_cluster_membership_excluded gauge
envoy_cluster_membership_excluded{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-fail/rule/0"} 0
envoy_cluster_membership_excluded{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-pass/rule/0"} 0

# TYPE envoy_cluster_membership_healthy gauge
envoy_cluster_membership_healthy{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-fail/rule/0"} 0
envoy_cluster_membership_healthy{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-pass/rule/0"} 2

# TYPE envoy_cluster_membership_total gauge
envoy_cluster_membership_total{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-fail/rule/0"} 2
envoy_cluster_membership_total{envoy_cluster_name="httproute/gateway-conformance-infra/http-with-health-check-active-http-pass/rule/0"} 2
```

[^1]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/health_checking#arch-overview-health-checking-degraded
arkodg: `membership_health` works, thanks !
zirain: /retest

Patch:
diff --git a/api/v1alpha1/policy_helpers.go b/api/v1alpha1/policy_helpers.go
index 31223bf10..f1bc16e17 100644
--- a/api/v1alpha1/policy_helpers.go
+++ b/api/v1alpha1/policy_helpers.go
@@ -10,18 +10,33 @@ import (
 )
 
 type PolicyTargetReferences struct {
-	// TargetRef is the name of the Gateway resource this policy
-	// is being attached to.
-	// This Policy and the TargetRef MUST be in the same namespace
-	// for this Policy to have effect and be applied to the Gateway.
-	// TargetRef
+	// TargetRef is the name of the resource this policy is being attached to.
+	// This policy and the TargetRef MUST be in the same namespace for this
+	// Policy to have effect
 	//
-	// Deprecated: use targetRefs instead
+	// Deprecated: use targetRefs/targetSelectors instead
 	TargetRef *gwapiv1a2.LocalPolicyTargetReferenceWithSectionName `json:"targetRef,omitempty"`
 
 	// TargetRefs are the names of the Gateway resources this policy
 	// is being attached to.
 	TargetRefs []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName `json:"targetRefs,omitempty"`
+
+	// TargetSelectors allow targeting resources for this policy based on labels
+	TargetSelectors []TargetSelector `json:"targetSelectors,omitempty"`
+}
+
+// +kubebuilder:validation:XValidation:rule="has(self.group) ? self.group == 'gateway.networking.k8s.io' : true ", message="group must be gateway.networking.k8s.io"
+type TargetSelector struct {
+	// Group is the group that this selector targets. Defaults to gateway.networking.k8s.io
+	//
+	// +kubebuilder:default:="gateway.networking.k8s.io"
+	Group *gwapiv1a2.Group `json:"group,omitempty"`
+
+	// Kind is the resource kind that this selector targets.
+	Kind gwapiv1a2.Kind `json:"kind"`
+
+	// MatchLabels are the set of label selectors for identifying the targeted resource
+	MatchLabels map[string]string `json:"matchLabels"`
 }
 
 func (p PolicyTargetReferences) GetTargetRefs() []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 56a090358..6e55a9d29 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -3628,6 +3628,13 @@ func (in *PolicyTargetReferences) DeepCopyInto(out *PolicyTargetReferences) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.TargetSelectors != nil {
+		in, out := &in.TargetSelectors, &out.TargetSelectors
+		*out = make([]TargetSelector, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyTargetReferences.
@@ -4724,6 +4731,33 @@ func (in *TLSSettings) DeepCopy() *TLSSettings {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TargetSelector) DeepCopyInto(out *TargetSelector) {
+	*out = *in
+	if in.Group != nil {
+		in, out := &in.Group, &out.Group
+		*out = new(apisv1.Group)
+		**out = **in
+	}
+	if in.MatchLabels != nil {
+		in, out := &in.MatchLabels, &out.MatchLabels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSelector.
+func (in *TargetSelector) DeepCopy() *TargetSelector {
+	if in == nil {
+		return nil
+	}
+	out := new(TargetSelector)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Timeout) DeepCopyInto(out *Timeout) {
 	*out = *in
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 7d7e7c968..d779a2d83 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -917,14 +917,12 @@ spec:
                 type: object
               targetRef:
                 description: |-
-                  TargetRef is the name of the Gateway resource this policy
-                  is being attached to.
-                  This Policy and the TargetRef MUST be in the same namespace
-                  for this Policy to have effect and be applied to the Gateway.
-                  TargetRef
+                  TargetRef is the name of the resource this policy is being attached to.
+                  This policy and the TargetRef MUST be in the same namespace for this
+                  Policy to have effect
 
 
-                  Deprecated: use targetRefs instead
+                  Deprecated: use targetRefs/targetSelectors instead
                 properties:
                   group:
                     description: Group is the group of the target resource.
@@ -1024,6 +1022,39 @@ spec:
                   - name
                   type: object
                 type: array
+              targetSelectors:
+                description: TargetSelectors allow targeting resources for this policy
+                  based on labels
+                items:
+                  properties:
+                    group:
+                      default: gateway.networking.k8s.io
+                      description: Group is the group that this selector targets.
+                        Defaults to gateway.networking.k8s.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      description: Kind is the resource kind that this selector targets.
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: MatchLabels are the set of label selectors for
+                        identifying the targeted resource
+                      type: object
+                  required:
+                  - kind
+                  - matchLabels
+                  type: object
+                  x-kubernetes-validations:
+                  - message: group must be gateway.networking.k8s.io
+                    rule: 'has(self.group) ? self.group == ''gateway.networking.k8s.io''
+                      : true '
+                type: array
               tcpKeepalive:
                 description: |-
                   TcpKeepalive settings associated with the upstream client connection.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
index 218c1cf2e..2e2224ecc 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -328,14 +328,12 @@ spec:
                 type: object
               targetRef:
                 description: |-
-                  TargetRef is the name of the Gateway resource this policy
-                  is being attached to.
-                  This Policy and the TargetRef MUST be in the same namespace
-                  for this Policy to have effect and be applied to the Gateway.
-                  TargetRef
+                  TargetRef is the name of the resource this policy is being attached to.
+                  This policy and the TargetRef MUST be in the same namespace for this
+                  Policy to have effect
 
 
-                  Deprecated: use targetRefs instead
+                  Deprecated: use targetRefs/targetSelectors instead
                 properties:
                   group:
                     description: Group is the group of the target resource.
@@ -435,6 +433,39 @@ spec:
                   - name
                   type: object
                 type: array
+              targetSelectors:
+                description: TargetSelectors allow targeting resources for this policy
+                  based on labels
+                items:
+                  properties:
+                    group:
+                      default: gateway.networking.k8s.io
+                      description: Group is the group that this selector targets.
+                        Defaults to gateway.networking.k8s.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      description: Kind is the resource kind that this selector targets.
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: MatchLabels are the set of label selectors for
+                        identifying the targeted resource
+                      type: object
+                  required:
+                  - kind
+                  - matchLabels
+                  type: object
+                  x-kubernetes-validations:
+                  - message: group must be gateway.networking.k8s.io
+                    rule: 'has(self.group) ? self.group == ''gateway.networking.k8s.io''
+                      : true '
+                type: array
               tcpKeepalive:
                 description: |-
                   TcpKeepalive settings associated with the downstream client connection.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
index 92a0c9def..0b2576030 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml
@@ -194,14 +194,12 @@ spec:
                 type: array
               targetRef:
                 description: |-
-                  TargetRef is the name of the Gateway resource this policy
-                  is being attached to.
-                  This Policy and the TargetRef MUST be in the same namespace
-                  for this Policy to have effect and be applied to the Gateway.
-                  TargetRef
+                  TargetRef is the name of the resource this policy is being attached to.
+                  This policy and the TargetRef MUST be in the same namespace for this
+                  Policy to have effect
 
 
-                  Deprecated: use targetRefs instead
+                  Deprecated: use targetRefs/targetSelectors instead
                 properties:
                   group:
                     description: Group is the group of the target resource.
@@ -301,6 +299,39 @@ spec:
                   - name
                   type: object
                 type: array
+              targetSelectors:
+                description: TargetSelectors allow targeting resources for this policy
+                  based on labels
+                items:
+                  properties:
+                    group:
+                      default: gateway.networking.k8s.io
+                      description: Group is the group that this selector targets.
+                        Defaults to gateway.networking.k8s.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      description: Kind is the resource kind that this selector targets.
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: MatchLabels are the set of label selectors for
+                        identifying the targeted resource
+                      type: object
+                  required:
+                  - kind
+                  - matchLabels
+                  type: object
+                  x-kubernetes-validations:
+                  - message: group must be gateway.networking.k8s.io
+                    rule: 'has(self.group) ? self.group == ''gateway.networking.k8s.io''
+                      : true '
+                type: array
               wasm:
                 description: |-
                   Wasm is a list of Wasm extensions to be loaded by the Gateway.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
index 29c2cb135..e5c0b7fc9 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -988,14 +988,12 @@ spec:
                 type: object
               targetRef:
                 description: |-
-                  TargetRef is the name of the Gateway resource this policy
-                  is being attached to.
-                  This Policy and the TargetRef MUST be in the same namespace
-                  for this Policy to have effect and be applied to the Gateway.
-                  TargetRef
+                  TargetRef is the name of the resource this policy is being attached to.
+                  This policy and the TargetRef MUST be in the same namespace for this
+                  Policy to have effect
 
 
-                  Deprecated: use targetRefs instead
+                  Deprecated: use targetRefs/targetSelectors instead
                 properties:
                   group:
                     description: Group is the group of the target resource.
@@ -1095,6 +1093,39 @@ spec:
                   - name
                   type: object
                 type: array
+              targetSelectors:
+                description: TargetSelectors allow targeting resources for this policy
+                  based on labels
+                items:
+                  properties:
+                    group:
+                      default: gateway.networking.k8s.io
+                      description: Group is the group that this selector targets.
+                        Defaults to gateway.networking.k8s.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      description: Kind is the resource kind that this selector targets.
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: MatchLabels are the set of label selectors for
+                        identifying the targeted resource
+                      type: object
+                  required:
+                  - kind
+                  - matchLabels
+                  type: object
+                  x-kubernetes-validations:
+                  - message: group must be gateway.networking.k8s.io
+                    rule: 'has(self.group) ? self.group == ''gateway.networking.k8s.io''
+                      : true '
+                type: array
             type: object
             x-kubernetes-validations:
             - message: either targetRef or targetRefs must be used
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index bcc6a5507..0750f0085 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -73,7 +73,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv
 	// Process the policies targeting xRoutes
 	for _, currPolicy := range backendTrafficPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes)
 		for _, currTarget := range targetRefs {
 			if currTarget.Kind != KindGateway {
 				policy, found := handledPolicies[policyName]
@@ -147,7 +147,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv
 	// Process the policies targeting Gateways
 	for _, currPolicy := range backendTrafficPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways)
 		for _, currTarget := range targetRefs {
 			if currTarget.Kind == KindGateway {
 				policy, found := handledPolicies[policyName]
diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go
index cde236dbc..7a25a6613 100644
--- a/internal/gatewayapi/clienttrafficpolicy.go
+++ b/internal/gatewayapi/clienttrafficpolicy.go
@@ -70,6 +70,9 @@ func (t *Translator) ProcessClientTrafficPolicies(
 	// before policy with no section so below loops can be flattened into 1.
 	for _, currPolicy := range clientTrafficPolicies {
 		policyName := utils.NamespacedName(currPolicy)
+		// This loop only handles policies that target a specific section. When
+		// targeting a policy with a selector, it's not possible to specify a SectionName
+		// so there's no need to try to match targets with selectors
 		targetRefs := currPolicy.Spec.GetTargetRefs()
 		for _, currTarget := range targetRefs {
 			if hasSectionName(&currTarget) {
@@ -165,7 +168,7 @@ func (t *Translator) ProcessClientTrafficPolicies(
 	// Policy with no section set (targeting all sections)
 	for _, currPolicy := range clientTrafficPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways)
 		for _, currTarget := range targetRefs {
 			if !hasSectionName(&currTarget) {
 
diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go
index 5baecbcb5..9d1b0f922 100644
--- a/internal/gatewayapi/envoyextensionpolicy.go
+++ b/internal/gatewayapi/envoyextensionpolicy.go
@@ -74,7 +74,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies(envoyExtensionPolicies []*egv
 	// Process the policies targeting xRoutes
 	for _, currPolicy := range envoyExtensionPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes)
 		for _, currTarget := range targetRefs {
 			if currTarget.Kind != KindGateway {
 				policy, found := handledPolicies[policyName]
@@ -148,7 +148,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies(envoyExtensionPolicies []*egv
 	// Process the policies targeting Gateways
 	for _, currPolicy := range envoyExtensionPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways)
 		for _, currTarget := range targetRefs {
 			if currTarget.Kind == KindGateway {
 				policy, found := handledPolicies[policyName]
diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go
index 408a40ab0..269c968b4 100644
--- a/internal/gatewayapi/helpers.go
+++ b/internal/gatewayapi/helpers.go
@@ -9,11 +9,15 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"slices"
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -495,3 +499,54 @@ func irConfigName(policy client.Object) string {
 		strings.ToLower(policy.GetObjectKind().GroupVersionKind().Kind),
 		utils.NamespacedName(policy).String())
 }
+
+type targetRefWithTimestamp struct {
+	gwapiv1a2.LocalPolicyTargetReferenceWithSectionName
+	CreationTimestamp metav1.Time
+}
+
+func getPolicyTargetRefs[T client.Object](policy egv1a1.PolicyTargetReferences, potentialTargets []T) []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName {
+	dedup := sets.New[targetRefWithTimestamp]()
+	for _, currSelector := range policy.TargetSelectors {
+		labelSelector := labels.SelectorFromSet(currSelector.MatchLabels)
+		for _, obj := range potentialTargets {
+			gvk := obj.GetObjectKind().GroupVersionKind()
+			if gvk.Kind != string(currSelector.Kind) ||
+				gvk.Group != string(ptr.Deref(currSelector.Group, gwapiv1a2.GroupName)) {
+				continue
+			}
+
+			if labelSelector.Matches(labels.Set(obj.GetLabels())) {
+				dedup.Insert(targetRefWithTimestamp{
+					CreationTimestamp: obj.GetCreationTimestamp(),
+					LocalPolicyTargetReferenceWithSectionName: gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+						LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+							Group: gwapiv1a2.Group(gvk.Group),
+							Kind:  gwapiv1a2.Kind(gvk.Kind),
+							Name:  gwapiv1a2.ObjectName(obj.GetName()),
+						},
+					},
+				})
+			}
+		}
+	}
+	selectorsList := dedup.UnsortedList()
+	slices.SortFunc(selectorsList, func(i, j targetRefWithTimestamp) int {
+		return i.CreationTimestamp.Compare(j.CreationTimestamp.Time)
+	})
+	ret := []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{}
+	for _, v := range selectorsList {
+		ret = append(ret, v.LocalPolicyTargetReferenceWithSectionName)
+	}
+	// Plain targetRefs in the policy don't have an associated creation timestamp, but can still refer
+	// to targets that were already found via the selectors. Only add them to the returned list if
+	// they are not yet there. Always add them at the end.
+	fastLookup := sets.New(ret...)
+	for _, v := range policy.GetTargetRefs() {
+		if !fastLookup.Has(v) {
+			ret = append(ret, v)
+		}
+	}
+
+	return ret
+}
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index 470113b60..57153bc7e 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -85,7 +85,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 	// Process the policies targeting xRoutes
 	for _, currPolicy := range securityPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, routes)
 		for _, currTarget := range targetRefs {
 			if currTarget.Kind != KindGateway {
 				var (
@@ -163,7 +163,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 	// Process the policies targeting Gateways
 	for _, currPolicy := range securityPolicies {
 		policyName := utils.NamespacedName(currPolicy)
-		targetRefs := currPolicy.Spec.GetTargetRefs()
+		targetRefs := getPolicyTargetRefs(currPolicy.Spec.PolicyTargetReferences, gateways)
 		for _, currTarget := range targetRefs {
 			if currTarget.Kind == KindGateway {
 				var (
diff --git a/internal/infrastructure/kubernetes/infra_resource.go b/internal/infrastructure/kubernetes/infra_resource.go
index 01bf35c75..fc471f8af 100644
--- a/internal/infrastructure/kubernetes/infra_resource.go
+++ b/internal/infrastructure/kubernetes/infra_resource.go
@@ -31,20 +31,17 @@ func (i *Infra) createOrUpdateServiceAccount(ctx context.Context, r ResourceRend
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if sa, err = r.ServiceAccount(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
-
+		resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		return err
 	}
 
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -64,11 +61,8 @@ func (i *Infra) createOrUpdateConfigMap(ctx context.Context, r ResourceRender) (
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if cm, err = r.ConfigMap(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
-
+		resourceApplyTotal.WithFailure(metrics.StatusFailure, labels...).Increment()
 		return err
 	}
 
@@ -79,9 +73,9 @@ func (i *Infra) createOrUpdateConfigMap(ctx context.Context, r ResourceRender) (
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -101,11 +95,8 @@ func (i *Infra) createOrUpdateDeployment(ctx context.Context, r ResourceRender)
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if deployment, err = r.Deployment(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
-
+		resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		return err
 	}
 
@@ -119,9 +110,9 @@ func (i *Infra) createOrUpdateDeployment(ctx context.Context, r ResourceRender)
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -141,11 +132,8 @@ func (i *Infra) createOrUpdateDaemonSet(ctx context.Context, r ResourceRender) (
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if daemonSet, err = r.DaemonSet(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
-
+		resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		return err
 	}
 
@@ -159,9 +147,9 @@ func (i *Infra) createOrUpdateDaemonSet(ctx context.Context, r ResourceRender) (
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -179,10 +167,8 @@ func (i *Infra) createOrUpdatePodDisruptionBudget(ctx context.Context, r Resourc
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if pdb, err = r.PodDisruptionBudget(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
+		resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		return err
 	}
 
@@ -195,9 +181,9 @@ func (i *Infra) createOrUpdatePodDisruptionBudget(ctx context.Context, r Resourc
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -218,11 +204,8 @@ func (i *Infra) createOrUpdateHPA(ctx context.Context, r ResourceRender) (err er
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if hpa, err = r.HorizontalPodAutoscaler(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
-
+		resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		return err
 	}
 
@@ -235,9 +218,9 @@ func (i *Infra) createOrUpdateHPA(ctx context.Context, r ResourceRender) (err er
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -257,20 +240,17 @@ func (i *Infra) createOrUpdateService(ctx context.Context, r ResourceRender) (er
 		}
 	)
 
-	resourceApplyTotal.With(labels...).Increment()
-
 	if svc, err = r.Service(); err != nil {
-		resourceApplyFailed.With(labels...).Increment()
-
+		resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		return err
 	}
 
 	defer func() {
 		if err == nil {
 			resourceApplyDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceApplySuccess.With(labels...).Increment()
+			resourceApplyTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceApplyFailed.With(labels...).Increment()
+			resourceApplyTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -295,14 +275,12 @@ func (i *Infra) deleteServiceAccount(ctx context.Context, r ResourceRender) (err
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -327,14 +305,12 @@ func (i *Infra) deleteDeployment(ctx context.Context, r ResourceRender) (err err
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -359,14 +335,12 @@ func (i *Infra) deleteDaemonSet(ctx context.Context, r ResourceRender) (err erro
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -391,14 +365,12 @@ func (i *Infra) deleteConfigMap(ctx context.Context, r ResourceRender) (err erro
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -423,14 +395,12 @@ func (i *Infra) deleteService(ctx context.Context, r ResourceRender) (err error)
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -455,14 +425,12 @@ func (i *Infra) deleteHPA(ctx context.Context, r ResourceRender) (err error) {
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
@@ -487,14 +455,12 @@ func (i *Infra) deletePDB(ctx context.Context, r ResourceRender) (err error) {
 		}
 	)
 
-	resourceDeleteTotal.With(labels...).Increment()
-
 	defer func() {
 		if err == nil {
 			resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
-			resourceDeleteSuccess.With(labels...).Increment()
+			resourceDeleteTotal.WithSuccess(labels...).Increment()
 		} else {
-			resourceDeleteFailed.With(labels...).Increment()
+			resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
 		}
 	}()
 
diff --git a/internal/infrastructure/kubernetes/metrics.go b/internal/infrastructure/kubernetes/metrics.go
index d417845b5..477a87e04 100644
--- a/internal/infrastructure/kubernetes/metrics.go
+++ b/internal/infrastructure/kubernetes/metrics.go
@@ -8,15 +8,27 @@ package kubernetes
 import "github.com/envoyproxy/gateway/internal/metrics"
 
 var (
-	resourceApplyTotal           = metrics.NewCounter("resource_apply_total", "Total number of applied resources.")
-	resourceApplyFailed          = metrics.NewCounter("resource_apply_failed", "Number of applied resources that failed.")
-	resourceApplySuccess         = metrics.NewCounter("resource_apply_success", "Number of applied resources that succeed.")
-	resourceApplyDurationSeconds = metrics.NewHistogram("resource_apply_duration_seconds", "How long in seconds a resource be applied successfully.", []float64{0.001, 0.01, 0.1, 1, 5, 10})
+	resourceApplyTotal = metrics.NewCounter(
+		"resource_apply_total",
+		"Total number of applied resources.",
+	)
 
-	resourceDeleteTotal           = metrics.NewCounter("resource_delete_total", "Total number of deleted resources.")
-	resourceDeleteFailed          = metrics.NewCounter("resource_delete_failed", "Number of deleted resources that failed.")
-	resourceDeleteSuccess         = metrics.NewCounter("resource_delete_success", "Number of deleted resources that succeed.")
-	resourceDeleteDurationSeconds = metrics.NewHistogram("resource_delete_duration_seconds", "How long in seconds a resource be deleted successfully.", []float64{0.001, 0.01, 0.1, 1, 5, 10})
+	resourceApplyDurationSeconds = metrics.NewHistogram(
+		"resource_apply_duration_seconds",
+		"How long in seconds a resource be applied successfully.",
+		[]float64{0.001, 0.01, 0.1, 1, 5, 10},
+	)
+
+	resourceDeleteTotal = metrics.NewCounter(
+		"resource_delete_total",
+		"Total number of deleted resources.",
+	)
+
+	resourceDeleteDurationSeconds = metrics.NewHistogram(
+		"resource_delete_duration_seconds",
+		"How long in seconds a resource be deleted successfully.",
+		[]float64{0.001, 0.01, 0.1, 1, 5, 10},
+	)
 
 	kindLabel      = metrics.NewLabel("kind")
 	nameLabel      = metrics.NewLabel("name")
diff --git a/internal/message/metrics.go b/internal/message/metrics.go
index a78c9f87c..5f1201241 100644
--- a/internal/message/metrics.go
+++ b/internal/message/metrics.go
@@ -8,15 +8,22 @@ package message
 import "github.com/envoyproxy/gateway/internal/metrics"
 
 var (
-	watchableDepth = metrics.NewGauge("watchable_depth", "Current depth of watchable queue.")
-
-	watchableSubscribedDurationSeconds = metrics.NewHistogram("watchable_subscribed_duration_seconds", "How long in seconds a subscribed watchable is handled.", []float64{0.001, 0.01, 0.1, 1, 5, 10})
-
-	watchableSubscribedTotal = metrics.NewCounter("watchable_subscribed_total", "Total number of subscribed watchable.")
-
-	watchableSubscribedErrorsTotal = metrics.NewCounter("watchable_subscribed_errors_total", "Total number of subscribed watchable errors.")
-
-	runnerLabel = metrics.NewLabel("runner")
-
+	watchableDepth = metrics.NewGauge(
+		"watchable_depth",
+		"Current depth of watchable queue.",
+	)
+
+	watchableSubscribeDurationSeconds = metrics.NewHistogram(
+		"watchable_subscribe_duration_seconds",
+		"How long in seconds a subscribed watchable queue is handled.",
+		[]float64{0.001, 0.01, 0.1, 1, 5, 10},
+	)
+
+	watchableSubscribeTotal = metrics.NewCounter(
+		"watchable_subscribe_total",
+		"Total number of subscribed watchable queue.",
+	)
+
+	runnerLabel  = metrics.NewLabel("runner")
 	messageLabel = metrics.NewLabel("message")
 )
diff --git a/internal/message/watchutil.go b/internal/message/watchutil.go
index 2eb95725b..f8391cbc4 100644
--- a/internal/message/watchutil.go
+++ b/internal/message/watchutil.go
@@ -54,7 +54,7 @@ func HandleSubscription[K comparable, V any](
 	go func() {
 		for err := range errChans {
 			logger.WithValues("runner", meta.Runner).Error(err, "observed an error")
-			watchableSubscribedErrorsTotal.With(meta.LabelValues()...).Increment()
+			watchableSubscribeTotal.WithFailure(metrics.ReasonError, meta.LabelValues()...).Increment()
 		}
 	}()
 
@@ -65,8 +65,8 @@ func HandleSubscription[K comparable, V any](
 				Key:   k,
 				Value: v,
 			}, errChans)
-			watchableSubscribedTotal.With(meta.LabelValues()...).Increment()
-			watchableSubscribedDurationSeconds.With(meta.LabelValues()...).Record(time.Since(startHandleTime).Seconds())
+			watchableSubscribeTotal.WithSuccess(meta.LabelValues()...).Increment()
+			watchableSubscribeDurationSeconds.With(meta.LabelValues()...).Record(time.Since(startHandleTime).Seconds())
 		}
 	}
 	for snapshot := range subscription {
@@ -74,8 +74,8 @@ func HandleSubscription[K comparable, V any](
 		for _, update := range snapshot.Updates {
 			startHandleTime := time.Now()
 			handle(Update[K, V](update), errChans)
-			watchableSubscribedTotal.With(meta.LabelValues()...).Increment()
-			watchableSubscribedDurationSeconds.With(meta.LabelValues()...).Record(time.Since(startHandleTime).Seconds())
+			watchableSubscribeTotal.WithSuccess(meta.LabelValues()...).Increment()
+			watchableSubscribeDurationSeconds.With(meta.LabelValues()...).Record(time.Since(startHandleTime).Seconds())
 		}
 	}
 }
diff --git a/internal/metrics/otel_label.go b/internal/metrics/otel_label.go
index 45d04ff4b..bcf4c4722 100644
--- a/internal/metrics/otel_label.go
+++ b/internal/metrics/otel_label.go
@@ -7,6 +7,24 @@ package metrics
 
 import "go.opentelemetry.io/otel/attribute"
 
+var (
+	// statusLabel defines a label to indicate the status of current metric,
+	// e.g. is a SUCCESS or FAILURE status.
+	statusLabel = NewLabel("status")
+
+	// reasonLabel defines a label to indicate the reason of failure status metric,
+	// it's an optional label.
+	reasonLabel = NewLabel("reason")
+)
+
+const (
+	StatusSuccess = "success"
+	StatusFailure = "failure"
+
+	ReasonError    = "error"
+	ReasonConflict = "conflict"
+)
+
 // A Label provides a named dimension for a Metric.
 type Label struct {
 	key attribute.Key
diff --git a/internal/metrics/otel_metric_counter.go b/internal/metrics/otel_metric_counter.go
index 93dcaa136..5cf194dec 100644
--- a/internal/metrics/otel_metric_counter.go
+++ b/internal/metrics/otel_metric_counter.go
@@ -46,3 +46,26 @@ func (f *Counter) With(labelValues ...LabelValue) *Counter {
 
 	return m
 }
+
+func (f *Counter) WithStatus(status string, labelValues ...LabelValue) *Counter {
+	labelValues = append(labelValues, statusLabel.Value(status))
+	return f.With(labelValues...)
+}
+
+func (f *Counter) WithSuccess(labelValues ...LabelValue) *Counter {
+	if len(labelValues) > 0 {
+		labelValues = append(labelValues, statusLabel.Value(StatusSuccess))
+	} else {
+		labelValues = []LabelValue{statusLabel.Value(StatusSuccess)}
+	}
+	return f.With(labelValues...)
+}
+
+func (f *Counter) WithFailure(reason string, labelValues ...LabelValue) *Counter {
+	if len(labelValues) > 0 {
+		labelValues = append(labelValues, statusLabel.Value(StatusFailure), reasonLabel.Value(reason))
+	} else {
+		labelValues = []LabelValue{statusLabel.Value(StatusFailure), reasonLabel.Value(reason)}
+	}
+	return f.With(labelValues...)
+}
diff --git a/internal/provider/kubernetes/metrics.go b/internal/provider/kubernetes/metrics.go
index 236aa150e..ba617e208 100644
--- a/internal/provider/kubernetes/metrics.go
+++ b/internal/provider/kubernetes/metrics.go
@@ -8,12 +8,21 @@ package kubernetes
 import "github.com/envoyproxy/gateway/internal/metrics"
 
 var (
-	statusUpdateTotal           = metrics.NewCounter("status_update_total", "Total number of status updates by object kind.")
-	statusUpdateFailed          = metrics.NewCounter("status_update_failed_total", "Number of status updates that failed by object kind.")
-	statusUpdateConflict        = metrics.NewCounter("status_update_conflict_total", "Number of status update conflicts encountered by object kind.")
-	statusUpdateSuccess         = metrics.NewCounter("status_update_success_total", "Number of status updates that succeeded by object kind.")
-	statusUpdateNoop            = metrics.NewCounter("status_update_noop_total", "Number of status updates that are no-ops by object kind. This is a subset of successful status updates.")
-	statusUpdateDurationSeconds = metrics.NewHistogram("status_update_duration_seconds", "How long a status update takes to finish.", []float64{0.001, 0.01, 0.1, 1, 5, 10})
+	statusUpdateTotal = metrics.NewCounter(
+		"status_update_total",
+		"Total number of status update by object kind.",
+	)
+
+	statusUpdateDurationSeconds = metrics.NewHistogram(
+		"status_update_duration_seconds",
+		"How long a status update takes to finish.",
+		[]float64{0.001, 0.01, 0.1, 1, 5, 10},
+	)
 
 	kindLabel = metrics.NewLabel("kind")
 )
+
+const (
+	// statusNoAction means the status of metric is taking no action.
+	statusNoAction = "NoAction"
+)
diff --git a/internal/provider/kubernetes/status_updater.go b/internal/provider/kubernetes/status_updater.go
index c6bc5d7f1..59ff64d33 100644
--- a/internal/provider/kubernetes/status_updater.go
+++ b/internal/provider/kubernetes/status_updater.go
@@ -24,6 +24,7 @@ import (
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/metrics"
 )
 
 // Update contains an all the information needed to update an object's status.
@@ -75,8 +76,6 @@ func (u *UpdateHandler) apply(update Update) {
 		objKind   = kindOf(obj)
 	)
 
-	statusUpdateTotal.With(kindLabel.Value(objKind)).Increment()
-
 	defer func() {
 		updateDuration := time.Since(startTime)
 		statusUpdateDurationSeconds.With(kindLabel.Value(objKind)).Record(updateDuration.Seconds())
@@ -84,7 +83,7 @@ func (u *UpdateHandler) apply(update Update) {
 
 	if err := retry.OnError(retry.DefaultBackoff, func(err error) bool {
 		if kerrors.IsConflict(err) {
-			statusUpdateConflict.With(kindLabel.Value(objKind)).Increment()
+			statusUpdateTotal.WithFailure(metrics.ReasonConflict, kindLabel.Value(objKind)).Increment()
 			return true
 		}
 		return false
@@ -104,7 +103,7 @@ func (u *UpdateHandler) apply(update Update) {
 				WithName(update.NamespacedName.Namespace).
 				Info("status unchanged, bypassing update")
 
-			statusUpdateNoop.With(kindLabel.Value(objKind)).Increment()
+			statusUpdateTotal.WithStatus(statusNoAction, kindLabel.Value(objKind)).Increment()
 			return nil
 		}
 
@@ -115,9 +114,9 @@ func (u *UpdateHandler) apply(update Update) {
 		u.log.Error(err, "unable to update status", "name", update.NamespacedName.Name,
 			"namespace", update.NamespacedName.Namespace)
 
-		statusUpdateFailed.With(kindLabel.Value(objKind)).Increment()
+		statusUpdateTotal.WithFailure(metrics.ReasonError, kindLabel.Value(objKind)).Increment()
 	} else {
-		statusUpdateSuccess.With(kindLabel.Value(objKind)).Increment()
+		statusUpdateTotal.WithSuccess(kindLabel.Value(objKind)).Increment()
 	}
 }
 
diff --git a/internal/wasm/cache.go b/internal/wasm/cache.go
index 8bd94508b..6082a0788 100644
--- a/internal/wasm/cache.go
+++ b/internal/wasm/cache.go
@@ -239,7 +239,7 @@ func (c *localFileCache) getOrFetch(key cacheKey, opts GetOptions) (*cacheEntry,
 		// Download the Wasm module with http fetcher.
 		b, err = c.httpFetcher.Fetch(ctx, key.downloadURL, insecure)
 		if err != nil {
-			wasmRemoteFetchCount.With(resultTag.Value(downloadFailure)).Increment()
+			wasmRemoteFetchTotal.WithFailure(reasonDownloadError).Increment()
 			return nil, err
 		}
 
@@ -251,7 +251,7 @@ func (c *localFileCache) getOrFetch(key cacheKey, opts GetOptions) (*cacheEntry,
 			isPrivate = true
 		}
 		if imageBinaryFetcher, dChecksum, err = c.prepareFetch(ctx, u, insecure, opts); err != nil {
-			wasmRemoteFetchCount.With(resultTag.Value(manifestFailure)).Increment()
+			wasmRemoteFetchTotal.WithFailure(reasonManifestError).Increment()
 			return nil, fmt.Errorf("could not fetch Wasm OCI image: %w", err)
 		}
 	default:
@@ -261,7 +261,7 @@ func (c *localFileCache) getOrFetch(key cacheKey, opts GetOptions) (*cacheEntry,
 	// If the checksum is provided, check if it matches the downloaded binary.
 	if key.checksum != "" {
 		if dChecksum != key.checksum {
-			wasmRemoteFetchCount.With(resultTag.Value(checksumMismatch)).Increment()
+			wasmRemoteFetchTotal.WithFailure(reasonChecksumMismatch).Increment()
 			return nil, fmt.Errorf("module downloaded from %v has checksum %v, which does not match: %v", key.downloadURL, dChecksum, key.checksum)
 		}
 	} else {
@@ -272,17 +272,18 @@ func (c *localFileCache) getOrFetch(key cacheKey, opts GetOptions) (*cacheEntry,
 	if imageBinaryFetcher != nil {
 		b, err = imageBinaryFetcher()
 		if err != nil {
-			wasmRemoteFetchCount.With(resultTag.Value(downloadFailure)).Increment()
+			wasmRemoteFetchTotal.WithFailure(reasonDownloadError).Increment()
 			return nil, fmt.Errorf("could not fetch Wasm binary: %w", err)
 		}
 	}
 
 	if !isValidWasmBinary(b) {
-		wasmRemoteFetchCount.With(resultTag.Value(fetchFailure)).Increment()
+		wasmRemoteFetchTotal.WithFailure(reasonFetchError).Increment()
 		return nil, fmt.Errorf("fetched Wasm binary from %s is invalid", key.downloadURL)
 	}
 
-	wasmRemoteFetchCount.With(resultTag.Value(fetchSuccess)).Increment()
+	wasmRemoteFetchTotal.WithSuccess().Increment()
+
 	return c.addEntry(key, b, isPrivate)
 }
 
@@ -365,7 +366,9 @@ func (c *localFileCache) addEntry(key cacheKey, wasmModule []byte, isPrivate boo
 	}
 	ce.referencingURLs.Insert(key.downloadURL)
 	c.modules[key.moduleKey] = &ce
+
 	wasmCacheEntries.Record(float64(len(c.modules)))
+
 	return &ce, nil
 }
 
@@ -378,7 +381,7 @@ func (c *localFileCache) getEntry(key cacheKey, pullPolicy PullPolicy, u *url.UR
 	c.mux.Lock()
 	defer func() {
 		c.mux.Unlock()
-		wasmCacheLookupCount.With(hitTag.Value(strconv.FormatBool(cacheHit))).Increment()
+		wasmCacheLookupTotal.With(hitTag.Value(strconv.FormatBool(cacheHit))).Increment()
 	}()
 
 	// If no checksum is provided, check if a wasm module with the same downloading URL has been pulled before.
diff --git a/internal/wasm/metrics.go b/internal/wasm/metrics.go
index 9de7e6208..01cd43645 100644
--- a/internal/wasm/metrics.go
+++ b/internal/wasm/metrics.go
@@ -21,33 +21,29 @@ package wasm
 
 import "github.com/envoyproxy/gateway/internal/metrics"
 
-// Const strings for label value.
 const (
-	// For remote fetch metric.
-	fetchSuccess     = "success"
-	fetchFailure     = "fetch_failure"
-	downloadFailure  = "download_failure"
-	manifestFailure  = "manifest_failure"
-	checksumMismatch = "checksum_mismatched"
+	reasonFetchError       = "fetch_error"
+	reasonDownloadError    = "download_error"
+	reasonManifestError    = "manifest_error"
+	reasonChecksumMismatch = "checksum_mismatched"
 )
 
 var (
-	hitTag    = metrics.NewLabel("hit")
-	resultTag = metrics.NewLabel("result")
+	hitTag = metrics.NewLabel("hit")
 
 	wasmCacheEntries = metrics.NewGauge(
 		"wasm_cache_entries",
-		"number of Wasm remote fetch cache entries.",
+		"Number of Wasm remote fetch cache entries.",
 	)
 
-	wasmCacheLookupCount = metrics.NewCounter(
-		"wasm_cache_lookup_count",
-		"number of Wasm remote fetch cache lookups.",
+	wasmCacheLookupTotal = metrics.NewCounter(
+		"wasm_cache_lookup_total",
+		"Total number of Wasm remote fetch cache lookups.",
 	)
 
-	wasmRemoteFetchCount = metrics.NewCounter(
-		"wasm_remote_fetch_count",
-		"number of Wasm remote fetches and results, including success, download failure, and checksum mismatch.",
+	wasmRemoteFetchTotal = metrics.NewCounter(
+		"wasm_remote_fetch_total",
+		"Total number of Wasm remote fetches and results.",
 	)
 )
 
diff --git a/internal/xds/cache/metrics.go b/internal/xds/cache/metrics.go
index 3480b9c40..f5787e996 100644
--- a/internal/xds/cache/metrics.go
+++ b/internal/xds/cache/metrics.go
@@ -8,16 +8,23 @@ package cache
 import "github.com/envoyproxy/gateway/internal/metrics"
 
 var (
-	xdsSnapshotCreationTotal   = metrics.NewCounter("xds_snapshot_creation_total", "Total number of xds snapshot cache creation.")
-	xdsSnapshotCreationFailed  = metrics.NewCounter("xds_snapshot_creation_failed", "Number of xds snapshot cache creation that failed.")
-	xdsSnapshotCreationSuccess = metrics.NewCounter("xds_snapshot_creation_success", "Number of xds snapshot cache creation that succeeded.")
+	xdsSnapshotCreateTotal = metrics.NewCounter(
+		"xds_snapshot_create_total",
+		"Total number of xds snapshot cache creates.",
+	)
 
-	xdsSnapshotUpdateTotal   = metrics.NewCounter("xds_snapshot_update_total", "Total number of xds snapshot cache updates by node id.")
-	xdsSnapshotUpdateFailed  = metrics.NewCounter("xds_snapshot_update_failed", "Number of xds snapshot cache updates that failed by node id.")
-	xdsSnapshotUpdateSuccess = metrics.NewCounter("xds_snapshot_update_success", "Number of xds snapshot cache updates that succeeded by node id.")
-	nodeIDLabel              = metrics.NewLabel("nodeID")
+	xdsSnapshotUpdateTotal = metrics.NewCounter(
+		"xds_snapshot_update_total",
+		"Total number of xds snapshot cache updates by node id.",
+	)
 
-	xdsStreamDurationSeconds      = metrics.NewHistogram("xds_stream_duration_seconds", "How long a xds stream takes to finish.", []float64{0.1, 10, 50, 100, 1000, 10000})
-	xdsDeltaStreamDurationSeconds = metrics.NewHistogram("xds_delta_stream_duration_seconds", "How long a xds delta stream takes to finish.", []float64{0.1, 10, 50, 100, 1000, 10000})
-	streamIDLabel                 = metrics.NewLabel("streamID")
+	xdsStreamDurationSeconds = metrics.NewHistogram(
+		"xds_stream_duration_seconds",
+		"How long a xds stream takes to finish.",
+		[]float64{0.1, 10, 50, 100, 1000, 10000},
+	)
+
+	nodeIDLabel        = metrics.NewLabel("nodeID")
+	streamIDLabel      = metrics.NewLabel("streamID")
+	isDeltaStreamLabel = metrics.NewLabel("isDeltaStream")
 )
diff --git a/internal/xds/cache/snapshotcache.go b/internal/xds/cache/snapshotcache.go
index 383aad836..633021aa1 100644
--- a/internal/xds/cache/snapshotcache.go
+++ b/internal/xds/cache/snapshotcache.go
@@ -28,6 +28,7 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/envoyproxy/gateway/internal/logging"
+	"github.com/envoyproxy/gateway/internal/metrics"
 	"github.com/envoyproxy/gateway/internal/xds/types"
 )
 
@@ -78,26 +79,22 @@ func (s *snapshotCache) GenerateNewSnapshot(irKey string, resources types.XdsRes
 		version,
 		resources,
 	)
-
-	xdsSnapshotCreationTotal.Increment()
 	if err != nil {
-		xdsSnapshotCreationFailed.Increment()
+		xdsSnapshotCreateTotal.WithFailure(metrics.ReasonError).Increment()
 		return err
-	} else {
-		xdsSnapshotCreationSuccess.Increment()
 	}
+	xdsSnapshotCreateTotal.WithSuccess().Increment()
 
 	s.lastSnapshot[irKey] = snapshot
 
 	for _, node := range s.getNodeIDs(irKey) {
 		s.log.Debugf("Generating a snapshot with Node %s", node)
-		xdsSnapshotUpdateTotal.With(nodeIDLabel.Value(node)).Increment()
 
 		if err = s.SetSnapshot(context.TODO(), node, snapshot); err != nil {
-			xdsSnapshotUpdateFailed.With(nodeIDLabel.Value(node)).Increment()
+			xdsSnapshotUpdateTotal.WithFailure(metrics.ReasonError, nodeIDLabel.Value(node)).Increment()
 			return err
 		} else {
-			xdsSnapshotUpdateSuccess.With(nodeIDLabel.Value(node)).Increment()
+			xdsSnapshotUpdateTotal.WithSuccess(nodeIDLabel.Value(node)).Increment()
 		}
 	}
 
@@ -168,6 +165,7 @@ func (s *snapshotCache) OnStreamClosed(streamID int64, node *corev3.Node) {
 		xdsStreamDurationSeconds.With(
 			streamIDLabel.Value(strconv.FormatInt(streamID, 10)),
 			nodeIDLabel.Value(node.Id),
+			isDeltaStreamLabel.Value("false"),
 		).Record(streamDuration.Seconds())
 	}
 
@@ -267,9 +265,10 @@ func (s *snapshotCache) OnDeltaStreamClosed(streamID int64, node *corev3.Node) {
 
 	if startTime, ok := s.deltaStreamDuration[streamID]; ok {
 		deltaStreamDuration := time.Since(startTime)
-		xdsDeltaStreamDurationSeconds.With(
+		xdsStreamDurationSeconds.With(
 			streamIDLabel.Value(strconv.FormatInt(streamID, 10)),
 			nodeIDLabel.Value(node.Id),
+			isDeltaStreamLabel.Value("true"),
 		).Record(deltaStreamDuration.Seconds())
 	}
 


Test patch:
diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go
index 5f61a1cc1..0339141de 100644
--- a/internal/gatewayapi/helpers_test.go
+++ b/internal/gatewayapi/helpers_test.go
@@ -15,8 +15,13 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/utils/ptr"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 )
 
 func TestValidateGRPCFilterRef(t *testing.T) {
@@ -194,3 +199,289 @@ func TestValidateHTTPFilterRef(t *testing.T) {
 		})
 	}
 }
+
+func TestGetPolicyTargetRefs(t *testing.T) {
+	testCases := []struct {
+		name    string
+		policy  egv1a1.PolicyTargetReferences
+		targets []*unstructured.Unstructured
+		results []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName
+	}{
+		{
+			name: "simple",
+			policy: egv1a1.PolicyTargetReferences{
+				TargetSelectors: []egv1a1.TargetSelector{
+					{
+						Kind:  "Gateway",
+						Group: ptr.To(gwapiv1.Group("gateway.networking.k8s.io")),
+						MatchLabels: map[string]string{
+							"pick": "me",
+						},
+					},
+				},
+			},
+			targets: []*unstructured.Unstructured{
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "first",
+							"namespace": "default",
+							"labels": map[string]any{
+								"some": "random label",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "second",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "TLSRoute",
+						"metadata": map[string]any{
+							"name":      "third",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+			},
+			results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+				{
+					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+						Group: "gateway.networking.k8s.io",
+						Kind:  "Gateway",
+						Name:  "second",
+					},
+				},
+			},
+		},
+		{
+			name: "multiple selectors",
+			policy: egv1a1.PolicyTargetReferences{
+				TargetSelectors: []egv1a1.TargetSelector{
+					{
+						Kind: "TLSRoute",
+						MatchLabels: map[string]string{
+							"pick": "me",
+						},
+					},
+					{
+						Kind: "Gateway",
+						MatchLabels: map[string]string{
+							"pick": "me",
+						},
+					},
+				},
+			},
+			targets: []*unstructured.Unstructured{
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "first",
+							"namespace": "default",
+							"labels": map[string]any{
+								"some": "random label",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "second",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "TLSRoute",
+						"metadata": map[string]any{
+							"name":      "third",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+			},
+			results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+				{
+					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+						Group: "gateway.networking.k8s.io",
+						Kind:  "TLSRoute",
+						Name:  "third",
+					},
+				},
+				{
+					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+						Group: "gateway.networking.k8s.io",
+						Kind:  "Gateway",
+						Name:  "second",
+					},
+				},
+			},
+		},
+		{
+			name: "deduplicated",
+			policy: egv1a1.PolicyTargetReferences{
+				TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+					{
+						LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+							Group: "gateway.networking.k8s.io",
+							Kind:  "TLSRoute",
+							Name:  "third",
+						},
+					},
+				},
+				TargetSelectors: []egv1a1.TargetSelector{
+					{
+						Kind: "TLSRoute",
+						MatchLabels: map[string]string{
+							"pick": "me",
+						},
+					},
+				},
+			},
+			targets: []*unstructured.Unstructured{
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "first",
+							"namespace": "default",
+							"labels": map[string]any{
+								"some": "random label",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "second",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "TLSRoute",
+						"metadata": map[string]any{
+							"name":      "third",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+			},
+			results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+				{
+					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+						Group: "gateway.networking.k8s.io",
+						Kind:  "TLSRoute",
+						Name:  "third",
+					},
+				},
+			},
+		},
+		{
+			name: "bad-group-is-ignored",
+			policy: egv1a1.PolicyTargetReferences{
+				TargetSelectors: []egv1a1.TargetSelector{
+					{
+						Kind:  "Gateway",
+						Group: ptr.To(gwapiv1.Group("bad-group")),
+						MatchLabels: map[string]string{
+							"pick": "me",
+						},
+					},
+				},
+			},
+			targets: []*unstructured.Unstructured{
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "first",
+							"namespace": "default",
+							"labels": map[string]any{
+								"some": "random label",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "Gateway",
+						"metadata": map[string]any{
+							"name":      "second",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+				{
+					Object: map[string]any{
+						"apiVersion": "gateway.networking.k8s.io/v1",
+						"kind":       "TLSRoute",
+						"metadata": map[string]any{
+							"name":      "third",
+							"namespace": "default",
+							"labels": map[string]any{
+								"pick": "me",
+							},
+						},
+					},
+				},
+			},
+			results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			results := getPolicyTargetRefs(tc.policy, tc.targets)
+			require.ElementsMatch(t, results, tc.results)
+		})
+	}
+}
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 37a763981..e72e28dc5 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -446,8 +446,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ |  false  | RateLimit allows the user to limit the number of incoming requests<br />to a predefined value based on attributes within the traffic flow. |
 | `loadBalancer` | _[LoadBalancer](#loadbalancer)_ |  false  | LoadBalancer policy to apply when routing traffic from the gateway to<br />the backend endpoints |
 | `proxyProtocol` | _[ProxyProtocol](#proxyprotocol)_ |  false  | ProxyProtocol enables the Proxy Protocol when communicating with the backend. |
@@ -665,8 +666,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `tcpKeepalive` | _[TCPKeepalive](#tcpkeepalive)_ |  false  | TcpKeepalive settings associated with the downstream client connection.<br />If defined, sets SO_KEEPALIVE on the listener socket to enable TCP Keepalives.<br />Disabled by default. |
 | `enableProxyProtocol` | _boolean_ |  false  | EnableProxyProtocol interprets the ProxyProtocol header and adds the<br />Client Address into the X-Forwarded-For header.<br />Note Proxy Protocol must be present when this field is set, else the connection<br />is closed. |
 | `clientIPDetection` | _[ClientIPDetectionSettings](#clientipdetectionsettings)_ |  false  | ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. |
@@ -900,8 +902,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `wasm` | _[Wasm](#wasm) array_ |  false  | Wasm is a list of Wasm extensions to be loaded by the Gateway.<br />Order matters, as the extensions will be loaded in the order they are<br />defined in this list. |
 | `extProc` | _[ExtProc](#extproc) array_ |  false  | ExtProc is an ordered list of external processing filters<br />that should added to the envoy filter chain |
 
@@ -2607,8 +2610,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 
 
 #### Principal
@@ -3320,8 +3324,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `cors` | _[CORS](#cors)_ |  false  | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). |
 | `basicAuth` | _[BasicAuth](#basicauth)_ |  false  | BasicAuth defines the configuration for the HTTP Basic Authentication. |
 | `jwt` | _[JWT](#jwt)_ |  false  | JWT defines the configuration for JSON Web Token (JWT) authentication. |
@@ -3557,6 +3562,26 @@ _Appears in:_
 | `1.3` | TLSv1.3 specifies TLS version 1.3<br /> | 
 
 
+#### TargetSelector
+
+
+
+
+
+_Appears in:_
+- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
+- [ClientTrafficPolicySpec](#clienttrafficpolicyspec)
+- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec)
+- [PolicyTargetReferences](#policytargetreferences)
+- [SecurityPolicySpec](#securitypolicyspec)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `group` | _[Group](#group)_ |  true  | Group is the group that this selector targets. Defaults to gateway.networking.k8s.io |
+| `kind` | _[Kind](#kind)_ |  true  | Kind is the resource kind that this selector targets. |
+| `matchLabels` | _object (keys:string, values:string)_ |  true  | MatchLabels are the set of label selectors for identifying the targeted resource |
+
+
 #### Timeout
 
 
diff --git a/site/content/en/latest/tasks/observability/gateway-exported-metrics.md b/site/content/en/latest/tasks/observability/gateway-exported-metrics.md
index 9878775e1..eb7b560ee 100644
--- a/site/content/en/latest/tasks/observability/gateway-exported-metrics.md
+++ b/site/content/en/latest/tasks/observability/gateway-exported-metrics.md
@@ -17,12 +17,11 @@ they all follow the design of [Watching Components](../../../contributions/desig
 
 Envoy Gateway collects the following metrics in Watching Components:
 
-| Name                                    | Description                                            |
-|-----------------------------------------|--------------------------------------------------------|
-| `watchable_depth`                       | Current depth of watchable map.                        |
-| `watchable_subscribed_duration_seconds` | How long in seconds a subscribed watchable is handled. |
-| `watchable_subscribed_total`            | Total number of subscribed watchable.                  |
-| `watchable_subscribed_errors_total`     | Total number of subscribed watchable errors.           |
+| Name                                   | Description                                                  |
+|----------------------------------------|--------------------------------------------------------------|
+| `watchable_depth`                      | Current depth of watchable map.                              |
+| `watchable_subscribe_duration_seconds` | How long in seconds a subscribed watchable queue is handled. |
+| `watchable_subscribe_total`            | Total number of subscribed watchable queue.                  |
 
 Each metric includes the `runner` label to identify the corresponding components,
 the relationship between label values and components is as follows:
@@ -35,7 +34,7 @@ the relationship between label values and components is as follows:
 | `xds-translator`   | xDS Translator                  |
 | `global-ratelimit` | Global RateLimit xDS Translator |
 
-Metrics may include one or more additional labels, such as `message` etc.
+Metrics may include one or more additional labels, such as `message`, `status` and `reason` etc.
 
 ## Status Updater
 
@@ -43,14 +42,10 @@ Envoy Gateway monitors the status updates of various resources (like `GatewayCla
 
 Envoy Gateway collects the following metrics in Status Updater:
 
-| Name                             | Description                                                                                             |
-|----------------------------------|---------------------------------------------------------------------------------------------------------|
-| `status_update_total`            | Total number of status updates by object kind.                                                          |
-| `status_update_failed_total`     | Number of status updates that failed by object kind.                                                    |
-| `status_update_conflict_total`   | Number of status update conflicts encountered by object kind.                                           |
-| `status_update_success_total`    | Number of status updates that succeeded by object kind.                                                 |
-| `status_update_noop_total`       | Number of status updates that are no-ops by object kind. This is a subset of successful status updates. |
-| `status_update_duration_seconds` | How long a status update takes to finish.                                                               |
+| Name                             | Description                                    |
+|----------------------------------|------------------------------------------------|
+| `status_update_total`            | Total number of status update by object kind.  |
+| `status_update_duration_seconds` | How long a status update takes to finish.      |
 
 Each metric includes `kind` label to identify the corresponding resources.
 
@@ -60,19 +55,14 @@ Envoy Gateway monitors the cache and xDS connection status in xDS Server.
 
 Envoy Gateway collects the following metrics in xDS Server:
 
-| Name                                | Description                                                   |
-|-------------------------------------|---------------------------------------------------------------|
-| `xds_snapshot_creation_total`       | Total number of xds snapshot cache creation.                  |
-| `xds_snapshot_creation_failed`      | Number of xds snapshot cache creation that failed.            |
-| `xds_snapshot_creation_success`     | Number of xds snapshot cache creation that succeed.           |
-| `xds_snapshot_update_total`         | Total number of xds snapshot cache updates by node id.        |
-| `xds_snapshot_update_failed`        | Number of xds snapshot cache updates that failed by node id.  |
-| `xds_snapshot_update_success`       | Number of xds snapshot cache updates that succeed by node id. |
-| `xds_stream_duration_seconds`       | How long a xds stream takes to finish.                        |
-| `xds_delta_stream_duration_seconds` | How long a xds delta stream takes to finish.                  |
+| Name                          | Description                                            |
+|-------------------------------|--------------------------------------------------------|
+| `xds_snapshot_create_total`   | Total number of xds snapshot cache creates.            |
+| `xds_snapshot_update_total`   | Total number of xds snapshot cache updates by node id. |
+| `xds_stream_duration_seconds` | How long a xds stream takes to finish.                 |
 
 - For xDS snapshot cache update and xDS stream connection status, each metric includes `nodeID` label to identify the connection peer.
-- For xDS stream connection status, each metric also includes `streamID` label to identify the connection stream.
+- For xDS stream connection status, each metric also includes `streamID` label to identify the connection stream, and `isDeltaStream` label to identify the delta connection stream.
 
 ## Infrastructure Manager
 
@@ -83,16 +73,25 @@ Envoy Gateway collects the following metrics in Infrastructure Manager:
 | Name                               | Description                                             |
 |------------------------------------|---------------------------------------------------------|
 | `resource_apply_total`             | Total number of applied resources.                      |
-| `resource_apply_failed`            | Number of applied resources that failed.                |
-| `resource_apply_success`           | Number of applied resources that succeed.               |
 | `resource_apply_duration_seconds`  | How long in seconds a resource be applied successfully. |
 | `resource_delete_total`            | Total number of deleted resources.                      |
-| `resource_delete_failed`           | Number of deleted resources that failed.                |
-| `resource_delete_success`          | Number of deleted resources that succeed.               |
 | `resource_delete_duration_seconds` | How long in seconds a resource be deleted successfully. |
 
 Each metric includes the `kind` label to identify the corresponding resources being applied or deleted by Infrastructure Manager.
 
 Metrics may also include `name` and `namespace` label to identify the name and namespace of corresponding Infrastructure Manager.
 
+## Wasm
+
+Envoy Gateway monitors the status of Wasm remote fetch cache.
+
+| Name                      | Description                                      |
+|---------------------------|--------------------------------------------------|
+| `wasm_cache_entries`      | Number of Wasm remote fetch cache entries.       | 
+| `wasm_cache_lookup_total` | Total number of Wasm remote fetch cache lookups. |
+| `wasm_remote_fetch_total` | Total number of Wasm remote fetches and results. |
+
+For metric `wasm_cache_lookup_total`, we are using `hit` label (boolean) to indicate whether the Wasm cache has been hit.
+
+
 [prom-format]: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index 37a763981..e72e28dc5 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -446,8 +446,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ |  false  | RateLimit allows the user to limit the number of incoming requests<br />to a predefined value based on attributes within the traffic flow. |
 | `loadBalancer` | _[LoadBalancer](#loadbalancer)_ |  false  | LoadBalancer policy to apply when routing traffic from the gateway to<br />the backend endpoints |
 | `proxyProtocol` | _[ProxyProtocol](#proxyprotocol)_ |  false  | ProxyProtocol enables the Proxy Protocol when communicating with the backend. |
@@ -665,8 +666,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `tcpKeepalive` | _[TCPKeepalive](#tcpkeepalive)_ |  false  | TcpKeepalive settings associated with the downstream client connection.<br />If defined, sets SO_KEEPALIVE on the listener socket to enable TCP Keepalives.<br />Disabled by default. |
 | `enableProxyProtocol` | _boolean_ |  false  | EnableProxyProtocol interprets the ProxyProtocol header and adds the<br />Client Address into the X-Forwarded-For header.<br />Note Proxy Protocol must be present when this field is set, else the connection<br />is closed. |
 | `clientIPDetection` | _[ClientIPDetectionSettings](#clientipdetectionsettings)_ |  false  | ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. |
@@ -900,8 +902,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `wasm` | _[Wasm](#wasm) array_ |  false  | Wasm is a list of Wasm extensions to be loaded by the Gateway.<br />Order matters, as the extensions will be loaded in the order they are<br />defined in this list. |
 | `extProc` | _[ExtProc](#extproc) array_ |  false  | ExtProc is an ordered list of external processing filters<br />that should added to the envoy filter chain |
 
@@ -2607,8 +2610,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 
 
 #### Principal
@@ -3320,8 +3324,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the Gateway resource this policy<br />is being attached to.<br />This Policy and the TargetRef MUST be in the same namespace<br />for this Policy to have effect and be applied to the Gateway.<br />TargetRef<br /><br />Deprecated: use targetRefs instead |
+| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ |  true  | TargetRef is the name of the resource this policy is being attached to.<br />This policy and the TargetRef MUST be in the same namespace for this<br />Policy to have effect<br /><br />Deprecated: use targetRefs/targetSelectors instead |
 | `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ |  true  | TargetRefs are the names of the Gateway resources this policy<br />is being attached to. |
+| `targetSelectors` | _[TargetSelector](#targetselector) array_ |  true  | TargetSelectors allow targeting resources for this policy based on labels |
 | `cors` | _[CORS](#cors)_ |  false  | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). |
 | `basicAuth` | _[BasicAuth](#basicauth)_ |  false  | BasicAuth defines the configuration for the HTTP Basic Authentication. |
 | `jwt` | _[JWT](#jwt)_ |  false  | JWT defines the configuration for JSON Web Token (JWT) authentication. |
@@ -3557,6 +3562,26 @@ _Appears in:_
 | `1.3` | TLSv1.3 specifies TLS version 1.3<br /> | 
 
 
+#### TargetSelector
+
+
+
+
+
+_Appears in:_
+- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
+- [ClientTrafficPolicySpec](#clienttrafficpolicyspec)
+- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec)
+- [PolicyTargetReferences](#policytargetreferences)
+- [SecurityPolicySpec](#securitypolicyspec)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `group` | _[Group](#group)_ |  true  | Group is the group that this selector targets. Defaults to gateway.networking.k8s.io |
+| `kind` | _[Kind](#kind)_ |  true  | Kind is the resource kind that this selector targets. |
+| `matchLabels` | _object (keys:string, values:string)_ |  true  | MatchLabels are the set of label selectors for identifying the targeted resource |
+
+
 #### Timeout
 
 
diff --git a/test/e2e/testdata/backend-health-check-active-http.yaml b/test/e2e/testdata/backend-health-check-active-http.yaml
new file mode 100644
index 000000000..999029c9d
--- /dev/null
+++ b/test/e2e/testdata/backend-health-check-active-http.yaml
@@ -0,0 +1,110 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: health-check-active-http-pass-btp
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-with-health-check-active-http-pass
+      namespace: gateway-conformance-infra
+  healthCheck:
+    active:
+      timeout: 3s
+      interval: 5s
+      unhealthyThreshold: 3
+      healthyThreshold: 1
+      type: HTTP
+      http:
+        path: "/status/200"
+        expectedStatuses:
+          - 200
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: health-check-active-http-fail-btp
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+    - group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: http-with-health-check-active-http-fail
+      namespace: gateway-conformance-infra
+  healthCheck:
+    active:
+      timeout: 3s
+      interval: 5s
+      unhealthyThreshold: 3
+      healthyThreshold: 1
+      type: HTTP
+      http:
+        path: "/status/418"
+        expectedStatuses:
+          - 200
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-with-health-check-active-http-pass
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /health-check-active-http-pass
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-with-health-check-active-http-fail
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /health-check-active-http-fail
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+---
+# cluster panic threshold should be set to 0.0 to disable panic, to check whether health check works.
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: EnvoyPatchPolicy
+metadata:
+  name: disable-cluster-panic-patch-policy
+  namespace: gateway-conformance-infra
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: Gateway
+    name: same-namespace
+    namespace: gateway-conformance-infra
+  type: JSONPatch
+  jsonPatches:
+    - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster"
+      name: "httproute/gateway-conformance-infra/http-with-health-check-active-http-pass/rule/0"
+      operation:
+        op: add
+        path: "/common_lb_config"
+        value:
+          healthy_panic_threshold:
+            value: 0.0
+    - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster"
+      name: "httproute/gateway-conformance-infra/http-with-health-check-active-http-fail/rule/0"
+      operation:
+        op: add
+        path: "/common_lb_config"
+        value:
+          healthy_panic_threshold:
+            value: 0.0
diff --git a/test/e2e/tests/backend_health_check.go b/test/e2e/tests/backend_health_check.go
new file mode 100644
index 000000000..b8a384c69
--- /dev/null
+++ b/test/e2e/tests/backend_health_check.go
@@ -0,0 +1,142 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+// +build e2e
+
+package tests
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/types"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/test/e2e/utils/prometheus"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, BackendHealthCheckActiveHTTPTest)
+}
+
+var BackendHealthCheckActiveHTTPTest = suite.ConformanceTest{
+	ShortName:   "BackendHealthCheckActiveHTTP",
+	Description: "Resource with BackendHealthCheckActiveHTTP enabled",
+	Manifests:   []string{"testdata/backend-health-check-active-http.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("active http", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			passRouteNN := types.NamespacedName{Name: "http-with-health-check-active-http-pass", Namespace: ns}
+			failRouteNN := types.NamespacedName{Name: "http-with-health-check-active-http-fail", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), passRouteNN, failRouteNN)
+
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(gatewayapi.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "health-check-active-http-pass-btp", Namespace: ns}, suite.ControllerName, ancestorRef)
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "health-check-active-http-fail-btp", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+			promAddr, err := prometheus.Address(suite.Client,
+				types.NamespacedName{Name: "prometheus", Namespace: "monitoring"},
+			)
+			require.NoError(t, err)
+
+			passClusterName := fmt.Sprintf("httproute/%s/%s/rule/0", ns, passRouteNN.Name)
+			failClusterName := fmt.Sprintf("httproute/%s/%s/rule/0", ns, failRouteNN.Name)
+			gtwName := "same-namespace"
+
+			// health check requests will be distributed to the cluster with configured path.
+			// we can use membership_healthy stats to check whether health check works as expected.
+			passPromQL := fmt.Sprintf(`envoy_cluster_health_check_success{envoy_cluster_name="%s",gateway_envoyproxy_io_owning_gateway_name="%s"}`, passClusterName, gtwName)
+			failPromQL := fmt.Sprintf(`envoy_cluster_health_check_failure{envoy_cluster_name="%s",gateway_envoyproxy_io_owning_gateway_name="%s"}`, failClusterName, gtwName)
+
+			http.AwaitConvergence(
+				t,
+				suite.TimeoutConfig.RequiredConsecutiveSuccesses,
+				suite.TimeoutConfig.MaxTimeToConsistency,
+				func(_ time.Duration) bool {
+					// check membership_healthy stats from Prometheus
+					v, err := prometheus.QuerySum(promAddr, passPromQL)
+					if err != nil {
+						// wait until Prometheus sync stats
+						return false
+					}
+					t.Logf("cluster pass health check: success stats query count: %v", v)
+
+					if v == 0 {
+						t.Error("success is not the same as expected")
+					} else {
+						t.Log("success is the same as expected")
+					}
+
+					return true
+				},
+			)
+
+			http.AwaitConvergence(
+				t,
+				suite.TimeoutConfig.RequiredConsecutiveSuccesses,
+				suite.TimeoutConfig.MaxTimeToConsistency,
+				func(_ time.Duration) bool {
+					// check membership_healthy stats from Prometheus
+					v, err := prometheus.QuerySum(promAddr, failPromQL)
+					if err != nil {
+						// wait until Prometheus sync stats
+						return false
+					}
+					t.Logf("cluster fail health check: failure stats query count: %v", v)
+
+					if v == 0 {
+						t.Error("failure is not same as expected")
+					} else {
+						t.Log("failure is same as expected")
+					}
+
+					return true
+				},
+			)
+
+			t.Run("health check pass", func(t *testing.T) {
+				expectedResponse := http.ExpectedResponse{
+					Request: http.Request{
+						Path: "/health-check-active-http-pass",
+					},
+					Response: http.Response{
+						StatusCode: 200,
+					},
+					Namespace: ns,
+				}
+
+				http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+			})
+
+			t.Run("health check fail", func(t *testing.T) {
+				expectedResponse := http.ExpectedResponse{
+					Request: http.Request{
+						Path: "/health-check-active-http-fail",
+					},
+					Response: http.Response{
+						StatusCode: 503,
+					},
+					Namespace: ns,
+				}
+
+				http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+			})
+		})
+	},
+}