Repository: kubernetes-sigs/apiserver-network-proxy

Issue:
[BUG] The backend ServerMetric is not correct
As the konnectivity server may has multiple BackendManagers, the `backend` metric should record the total backends stored in various of BackendManager even if we can record them with a label. 

However, the `backend` metric is recorded by DefaultBackendStorage, which is the underlying storage implementation. It will result in only some BackendManager record  its number of backends.

The solution is that `backend` metric should be recorded by konnectivity server instead of DefaultBackendStorage.

Discussion:
@mainred: I don't think it's a bug. It's not only `DefaultBackendManager` that can record the backend but all backend managers _inherit_ `AddBackend` from `DefaultBackendStorage`. Please reconsider this.@YRXING: Yes, every backend manager will inherit `AddBackend` method.

What I wonder is that the `DefaultBackendStorage` record `backend` metric through setting their own backends count while the `backend` metric is belong to proxy server. And the backend managers will overwrite each other's values.

For example, the proxy server has two backend managers. One has three backends and the other has five backends. Now the backend metric will only be one of these tow values while the actual value may be greater than three and smaller than five.



The related codes as following shows: 

```go
// SetBackendCount sets the number of backend connection.
func (a *ServerMetrics) SetBackendCount(count int) {
	a.backend.WithLabelValues().Set(float64(count))
}


func (s *DefaultBackendStorage) AddBackend(identifier string, idType pkgagent.IdentifierType, conn agent.AgentService_ConnectServer) Backend {
	// ...
	addedBackend := newBackend(conn)
	// ...
	metrics.Metrics.SetBackendCount(len(s.backends))
	// ...
	return addedBackend
}
```

@mainred: Thanks for your explanation, let's go on with your fix. proxy server is designed to support multiple backend managers, though currently only one is used. @jkh52: /cc @yan-lgtm 
/cc @cheftako @k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Reopen this issue or PR with `/reopen`
- Mark this issue or PR as fresh with `/remove-lifecycle rotten`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/close

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-ci-robot: @k8s-triage-robot: Closing this issue.

<details>

In response to [this](https://github.com/kubernetes-sigs/apiserver-network-proxy/issues/294#issuecomment-1118959098):

>The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.
>
>This bot triages issues and PRs according to the following rules:
>- After 90d of inactivity, `lifecycle/stale` is applied
>- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
>- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed
>
>You can:
>- Reopen this issue or PR with `/reopen`
>- Mark this issue or PR as fresh with `/remove-lifecycle rotten`
>- Offer to help out with [Issue Triage][1]
>
>Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
>
>/close
>
>[1]: https://www.kubernetes.dev/docs/guide/issue-triage/


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>@jkh52: /assign

Solution patch:
diff --git a/cmd/server/app/options/options.go b/cmd/server/app/options/options.go
index acb2856d..b213522e 100644
--- a/cmd/server/app/options/options.go
+++ b/cmd/server/app/options/options.go
@@ -27,6 +27,7 @@ import (
 	"k8s.io/klog/v2"
 
 	"sigs.k8s.io/apiserver-network-proxy/pkg/server"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/util"
 )
 
@@ -314,7 +315,7 @@ func (o *ProxyRunOptions) Validate() error {
 	if len(o.ProxyStrategies) == 0 {
 		return fmt.Errorf("ProxyStrategies cannot be empty")
 	}
-	if _, err := server.ParseProxyStrategies(o.ProxyStrategies); err != nil {
+	if _, err := proxystrategies.ParseProxyStrategies(o.ProxyStrategies); err != nil {
 		return fmt.Errorf("invalid proxy strategies: %v", err)
 	}
 	if o.XfrChannelSize <= 0 {
diff --git a/cmd/server/app/server.go b/cmd/server/app/server.go
index 98314520..0e6e8f16 100644
--- a/cmd/server/app/server.go
+++ b/cmd/server/app/server.go
@@ -46,6 +46,7 @@ import (
 	"sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/server"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/server/leases"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/util"
 	"sigs.k8s.io/apiserver-network-proxy/proto/agent"
 )
@@ -134,7 +135,7 @@ func (p *Proxy) Run(o *options.ProxyRunOptions, stopCh <-chan struct{}) error {
 		AuthenticationAudience: o.AuthenticationAudience,
 	}
 	klog.V(1).Infoln("Starting frontend server for client connections.")
-	ps, err := server.ParseProxyStrategies(o.ProxyStrategies)
+	ps, err := proxystrategies.ParseProxyStrategies(o.ProxyStrategies)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/server/backend_manager.go b/pkg/server/backend_manager.go
index f42631d2..3ecb6bb8 100644
--- a/pkg/server/backend_manager.go
+++ b/pkg/server/backend_manager.go
@@ -22,7 +22,6 @@ import (
 	"io"
 	"math/rand"
 	"slices"
-	"strings"
 	"sync"
 	"time"
 
@@ -32,72 +31,11 @@ import (
 	commonmetrics "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/common/metrics"
 	client "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/server/metrics"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	"sigs.k8s.io/apiserver-network-proxy/proto/agent"
 	"sigs.k8s.io/apiserver-network-proxy/proto/header"
 )
 
-type ProxyStrategy int
-
-const (
-	// With this strategy the Proxy Server will randomly pick a backend from
-	// the current healthy backends to establish the tunnel over which to
-	// forward requests.
-	ProxyStrategyDefault ProxyStrategy = iota + 1
-	// With this strategy the Proxy Server will pick a backend that has the same
-	// associated host as the request.Host to establish the tunnel.
-	ProxyStrategyDestHost
-	// ProxyStrategyDefaultRoute will only forward traffic to agents that have explicity advertised
-	// they serve the default route through an agent identifier. Typically used in combination with destHost
-	ProxyStrategyDefaultRoute
-)
-
-func (ps ProxyStrategy) String() string {
-	switch ps {
-	case ProxyStrategyDefault:
-		return "default"
-	case ProxyStrategyDestHost:
-		return "destHost"
-	case ProxyStrategyDefaultRoute:
-		return "defaultRoute"
-	}
-	panic(fmt.Sprintf("unhandled ProxyStrategy: %d", ps))
-}
-
-func ParseProxyStrategy(s string) (ProxyStrategy, error) {
-	switch s {
-	case ProxyStrategyDefault.String():
-		return ProxyStrategyDefault, nil
-	case ProxyStrategyDestHost.String():
-		return ProxyStrategyDestHost, nil
-	case ProxyStrategyDefaultRoute.String():
-		return ProxyStrategyDefaultRoute, nil
-	default:
-		return 0, fmt.Errorf("unknown proxy strategy: %s", s)
-	}
-}
-
-// GenProxyStrategiesFromStr generates the list of proxy strategies from the
-// comma-seperated string, i.e., destHost.
-func ParseProxyStrategies(proxyStrategies string) ([]ProxyStrategy, error) {
-	var result []ProxyStrategy
-
-	strs := strings.Split(proxyStrategies, ",")
-	for _, s := range strs {
-		if len(s) == 0 {
-			continue
-		}
-		ps, err := ParseProxyStrategy(s)
-		if err != nil {
-			return nil, err
-		}
-		result = append(result, ps)
-	}
-	if len(result) == 0 {
-		return nil, fmt.Errorf("proxy strategies cannot be empty")
-	}
-	return result, nil
-}
-
 // Backend abstracts a connected Konnectivity agent.
 //
 // In the only currently supported case (gRPC), it wraps an
@@ -271,24 +209,31 @@ type DefaultBackendStorage struct {
 	// e.g., when associating to the DestHostBackendManager, it can only use the
 	// identifiers of types, IPv4, IPv6 and Host.
 	idTypes []header.IdentifierType
+	// proxyStrategy is the proxy strategy of the backend manager this storage
+	// belongs to.
+	// It is used to record metrics.
+	proxyStrategy proxystrategies.ProxyStrategy
 }
 
 // NewDefaultBackendManager returns a DefaultBackendManager.
 func NewDefaultBackendManager() *DefaultBackendManager {
 	return &DefaultBackendManager{
 		DefaultBackendStorage: NewDefaultBackendStorage(
-			[]header.IdentifierType{header.UID})}
+			[]header.IdentifierType{header.UID}, proxystrategies.ProxyStrategyDefault)}
 }
 
 // NewDefaultBackendStorage returns a DefaultBackendStorage
-func NewDefaultBackendStorage(idTypes []header.IdentifierType) *DefaultBackendStorage {
+func NewDefaultBackendStorage(idTypes []header.IdentifierType, proxyStrategy proxystrategies.ProxyStrategy) *DefaultBackendStorage {
 	// Set an explicit value, so that the metric is emitted even when
 	// no agent ever successfully connects.
-	metrics.Metrics.SetBackendCount(0)
+	metrics.Metrics.SetBackendCountDeprecated(0)
+	metrics.Metrics.SetTotalBackendCount(proxyStrategy, 0)
+
 	return &DefaultBackendStorage{
-		backends: make(map[string][]*Backend),
-		random:   rand.New(rand.NewSource(time.Now().UnixNano())), /* #nosec G404 */
-		idTypes:  idTypes,
+		backends:      make(map[string][]*Backend),
+		random:        rand.New(rand.NewSource(time.Now().UnixNano())), /* #nosec G404 */
+		idTypes:       idTypes,
+		proxyStrategy: proxyStrategy,
 	}
 }
 
@@ -317,7 +262,8 @@ func (s *DefaultBackendStorage) addBackend(identifier string, idType header.Iden
 		return
 	}
 	s.backends[identifier] = []*Backend{backend}
-	metrics.Metrics.SetBackendCount(len(s.backends))
+	metrics.Metrics.SetBackendCountDeprecated(len(s.backends))
+	metrics.Metrics.SetTotalBackendCount(s.proxyStrategy, len(s.backends))
 	s.agentIDs = append(s.agentIDs, identifier)
 }
 
@@ -358,7 +304,8 @@ func (s *DefaultBackendStorage) removeBackend(identifier string, idType header.I
 	if !found {
 		klog.V(1).InfoS("Could not find connection matching identifier to remove", "agentID", identifier, "idType", idType)
 	}
-	metrics.Metrics.SetBackendCount(len(s.backends))
+	metrics.Metrics.SetBackendCountDeprecated(len(s.backends))
+	metrics.Metrics.SetTotalBackendCount(s.proxyStrategy, len(s.backends))
 }
 
 // NumBackends resturns the number of available backends
diff --git a/pkg/server/default_route_backend_manager.go b/pkg/server/default_route_backend_manager.go
index f4480eab..e09136c6 100644
--- a/pkg/server/default_route_backend_manager.go
+++ b/pkg/server/default_route_backend_manager.go
@@ -20,6 +20,7 @@ import (
 	"context"
 
 	"k8s.io/klog/v2"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	"sigs.k8s.io/apiserver-network-proxy/proto/header"
 )
 
@@ -32,7 +33,7 @@ var _ BackendManager = &DefaultRouteBackendManager{}
 func NewDefaultRouteBackendManager() *DefaultRouteBackendManager {
 	return &DefaultRouteBackendManager{
 		DefaultBackendStorage: NewDefaultBackendStorage(
-			[]header.IdentifierType{header.DefaultRoute})}
+			[]header.IdentifierType{header.DefaultRoute}, proxystrategies.ProxyStrategyDefaultRoute)}
 }
 
 // Backend tries to get a backend that advertises default route, with random selection.
diff --git a/pkg/server/desthost_backend_manager.go b/pkg/server/desthost_backend_manager.go
index 8c914e4b..28006577 100644
--- a/pkg/server/desthost_backend_manager.go
+++ b/pkg/server/desthost_backend_manager.go
@@ -20,6 +20,7 @@ import (
 	"context"
 
 	"k8s.io/klog/v2"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	"sigs.k8s.io/apiserver-network-proxy/proto/header"
 )
 
@@ -32,7 +33,7 @@ var _ BackendManager = &DestHostBackendManager{}
 func NewDestHostBackendManager() *DestHostBackendManager {
 	return &DestHostBackendManager{
 		DefaultBackendStorage: NewDefaultBackendStorage(
-			[]header.IdentifierType{header.IPv4, header.IPv6, header.Host})}
+			[]header.IdentifierType{header.IPv4, header.IPv6, header.Host}, proxystrategies.ProxyStrategyDestHost)}
 }
 
 func (dibm *DestHostBackendManager) AddBackend(backend *Backend) {
diff --git a/pkg/server/metrics/metrics.go b/pkg/server/metrics/metrics.go
index 9a89d6ee..02c350df 100644
--- a/pkg/server/metrics/metrics.go
+++ b/pkg/server/metrics/metrics.go
@@ -24,6 +24,7 @@ import (
 
 	commonmetrics "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/common/metrics"
 	"sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 )
 
 const (
@@ -51,6 +52,7 @@ type ServerMetrics struct {
 	grpcConnections      *prometheus.GaugeVec
 	httpConnections      prometheus.Gauge
 	backend              *prometheus.GaugeVec
+	totalBackendCount    *prometheus.GaugeVec
 	pendingDials         *prometheus.GaugeVec
 	establishedConns     *prometheus.GaugeVec
 	fullRecvChannels     *prometheus.GaugeVec
@@ -110,10 +112,21 @@ func newServerMetrics() *ServerMetrics {
 			Namespace: Namespace,
 			Subsystem: Subsystem,
 			Name:      "ready_backend_connections",
-			Help:      "Number of konnectivity agent connected to the proxy server",
+			Help:      "Number of konnectivity agent connected to the proxy server. DEPRECATED, please use ready_backends",
 		},
 		[]string{},
 	)
+	totalBackendCount := prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Namespace: Namespace,
+			Subsystem: Subsystem,
+			Name:      "ready_backends",
+			Help:      "Number of konnectivity agent connected to the proxy server",
+		},
+		[]string{
+			"proxy_strategy",
+		},
+	)
 	pendingDials := prometheus.NewGaugeVec(
 		prometheus.GaugeOpts{
 			Namespace: Namespace,
@@ -203,6 +216,7 @@ func newServerMetrics() *ServerMetrics {
 	prometheus.MustRegister(grpcConnections)
 	prometheus.MustRegister(httpConnections)
 	prometheus.MustRegister(backend)
+	prometheus.MustRegister(totalBackendCount)
 	prometheus.MustRegister(pendingDials)
 	prometheus.MustRegister(establishedConns)
 	prometheus.MustRegister(fullRecvChannels)
@@ -220,6 +234,7 @@ func newServerMetrics() *ServerMetrics {
 		grpcConnections:      grpcConnections,
 		httpConnections:      httpConnections,
 		backend:              backend,
+		totalBackendCount:    totalBackendCount,
 		pendingDials:         pendingDials,
 		establishedConns:     establishedConns,
 		fullRecvChannels:     fullRecvChannels,
@@ -240,6 +255,7 @@ func (s *ServerMetrics) Reset() {
 	s.frontendLatencies.Reset()
 	s.grpcConnections.Reset()
 	s.backend.Reset()
+	s.totalBackendCount.Reset()
 	s.pendingDials.Reset()
 	s.establishedConns.Reset()
 	s.fullRecvChannels.Reset()
@@ -279,11 +295,16 @@ func (s *ServerMetrics) HTTPConnectionInc() { s.httpConnections.Inc() }
 // HTTPConnectionDec decrements a finished HTTP CONNECTION connection.
 func (s *ServerMetrics) HTTPConnectionDec() { s.httpConnections.Dec() }
 
-// SetBackendCount sets the number of backend connection.
-func (s *ServerMetrics) SetBackendCount(count int) {
+// SetBackendCountDeprecated sets the number of backend connection.
+func (s *ServerMetrics) SetBackendCountDeprecated(count int) {
 	s.backend.WithLabelValues().Set(float64(count))
 }
 
+// SetTotalBackendCount sets the total number of backend connection.
+func (s *ServerMetrics) SetTotalBackendCount(proxyStrategy proxystrategies.ProxyStrategy, count int) {
+	s.totalBackendCount.WithLabelValues(proxyStrategy.String()).Set(float64(count))
+}
+
 // SetPendingDialCount sets the number of pending dials.
 func (s *ServerMetrics) SetPendingDialCount(count int) {
 	s.pendingDials.WithLabelValues().Set(float64(count))
diff --git a/pkg/server/proxystrategies/proxystrategies.go b/pkg/server/proxystrategies/proxystrategies.go
new file mode 100644
index 00000000..4c2c98e6
--- /dev/null
+++ b/pkg/server/proxystrategies/proxystrategies.go
@@ -0,0 +1,68 @@
+package proxystrategies
+
+import (
+	"fmt"
+	"strings"
+)
+
+type ProxyStrategy int
+
+const (
+	// With this strategy the Proxy Server will randomly pick a backend from
+	// the current healthy backends to establish the tunnel over which to
+	// forward requests.
+	ProxyStrategyDefault ProxyStrategy = iota + 1
+	// With this strategy the Proxy Server will pick a backend that has the same
+	// associated host as the request.Host to establish the tunnel.
+	ProxyStrategyDestHost
+	// ProxyStrategyDefaultRoute will only forward traffic to agents that have explicity advertised
+	// they serve the default route through an agent identifier. Typically used in combination with destHost
+	ProxyStrategyDefaultRoute
+)
+
+func (ps ProxyStrategy) String() string {
+	switch ps {
+	case ProxyStrategyDefault:
+		return "default"
+	case ProxyStrategyDestHost:
+		return "destHost"
+	case ProxyStrategyDefaultRoute:
+		return "defaultRoute"
+	}
+	panic(fmt.Sprintf("unhandled ProxyStrategy: %d", ps))
+}
+
+func ParseProxyStrategy(s string) (ProxyStrategy, error) {
+	switch s {
+	case ProxyStrategyDefault.String():
+		return ProxyStrategyDefault, nil
+	case ProxyStrategyDestHost.String():
+		return ProxyStrategyDestHost, nil
+	case ProxyStrategyDefaultRoute.String():
+		return ProxyStrategyDefaultRoute, nil
+	default:
+		return 0, fmt.Errorf("unknown proxy strategy: %s", s)
+	}
+}
+
+// GenProxyStrategiesFromStr generates the list of proxy strategies from the
+// comma-seperated string, i.e., destHost.
+func ParseProxyStrategies(proxyStrategies string) ([]ProxyStrategy, error) {
+	var result []ProxyStrategy
+
+	strs := strings.Split(proxyStrategies, ",")
+	for _, s := range strs {
+		if len(s) == 0 {
+			continue
+		}
+		ps, err := ParseProxyStrategy(s)
+		if err != nil {
+			return nil, err
+		}
+		result = append(result, ps)
+	}
+	if len(result) == 0 {
+		return nil, fmt.Errorf("proxy strategies cannot be empty")
+	}
+	return result, nil
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 9122c48f..e609584f 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -41,6 +41,7 @@ import (
 	commonmetrics "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/common/metrics"
 	"sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/server/metrics"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/util"
 	"sigs.k8s.io/apiserver-network-proxy/proto/agent"
 	"sigs.k8s.io/apiserver-network-proxy/proto/header"
@@ -213,7 +214,7 @@ type ProxyServer struct {
 	AgentAuthenticationOptions *AgentTokenAuthenticationOptions
 
 	// TODO: move strategies into BackendStorage
-	proxyStrategies []ProxyStrategy
+	proxyStrategies []proxystrategies.ProxyStrategy
 	xfrChannelSize  int
 }
 
@@ -230,11 +231,11 @@ var _ agent.AgentServiceServer = &ProxyServer{}
 
 var _ client.ProxyServiceServer = &ProxyServer{}
 
-func genContext(proxyStrategies []ProxyStrategy, reqHost string) context.Context {
+func genContext(proxyStrategies []proxystrategies.ProxyStrategy, reqHost string) context.Context {
 	ctx := context.Background()
 	for _, ps := range proxyStrategies {
 		switch ps {
-		case ProxyStrategyDestHost:
+		case proxystrategies.ProxyStrategyDestHost:
 			addr := util.RemovePortFromHost(reqHost)
 			ctx = context.WithValue(ctx, destHostKey, addr)
 		}
@@ -373,15 +374,15 @@ func (s *ProxyServer) removeEstablishedForStream(streamUID string) []*ProxyClien
 }
 
 // NewProxyServer creates a new ProxyServer instance
-func NewProxyServer(serverID string, proxyStrategies []ProxyStrategy, serverCount int, agentAuthenticationOptions *AgentTokenAuthenticationOptions, channelSize int) *ProxyServer {
+func NewProxyServer(serverID string, proxyStrategies []proxystrategies.ProxyStrategy, serverCount int, agentAuthenticationOptions *AgentTokenAuthenticationOptions, channelSize int) *ProxyServer {
 	var bms []BackendManager
 	for _, ps := range proxyStrategies {
 		switch ps {
-		case ProxyStrategyDestHost:
+		case proxystrategies.ProxyStrategyDestHost:
 			bms = append(bms, NewDestHostBackendManager())
-		case ProxyStrategyDefault:
+		case proxystrategies.ProxyStrategyDefault:
 			bms = append(bms, NewDefaultBackendManager())
-		case ProxyStrategyDefaultRoute:
+		case proxystrategies.ProxyStrategyDefaultRoute:
 			bms = append(bms, NewDefaultRouteBackendManager())
 		default:
 			klog.ErrorS(nil, "Unknown proxy strategy", "strategy", ps)



Test patch:
diff --git a/pkg/server/backend_manager_test.go b/pkg/server/backend_manager_test.go
index 107103cb..4a885319 100644
--- a/pkg/server/backend_manager_test.go
+++ b/pkg/server/backend_manager_test.go
@@ -18,11 +18,9 @@ package server
 
 import (
 	"context"
-	"fmt"
 	"reflect"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"go.uber.org/mock/gomock"
 	"google.golang.org/grpc/metadata"
 
@@ -385,123 +383,3 @@ func TestDestHostBackendManager_WithDuplicateIdents(t *testing.T) {
 		t.Errorf("expected %v, got %v", e, a)
 	}
 }
-
-func TestProxyStrategy(t *testing.T) {
-	for desc, tc := range map[string]struct {
-		input     ProxyStrategy
-		want      string
-		wantPanic string
-	}{
-		"default": {
-			input: ProxyStrategyDefault,
-			want:  "default",
-		},
-		"destHost": {
-			input: ProxyStrategyDestHost,
-			want:  "destHost",
-		},
-		"defaultRoute": {
-			input: ProxyStrategyDefaultRoute,
-			want:  "defaultRoute",
-		},
-		"unrecognized": {
-			input:     ProxyStrategy(0),
-			wantPanic: "unhandled ProxyStrategy: 0",
-		},
-	} {
-		t.Run(desc, func(t *testing.T) {
-			if tc.wantPanic != "" {
-				assert.PanicsWithValue(t, tc.wantPanic, func() {
-					_ = tc.input.String()
-				})
-			} else {
-				got := tc.input.String()
-				if got != tc.want {
-					t.Errorf("ProxyStrategy.String(): got %v, want %v", got, tc.want)
-				}
-			}
-		})
-	}
-}
-
-func TestParseProxyStrategy(t *testing.T) {
-	for desc, tc := range map[string]struct {
-		input   string
-		want    ProxyStrategy
-		wantErr error
-	}{
-		"empty": {
-			input:   "",
-			wantErr: fmt.Errorf("unknown proxy strategy: "),
-		},
-		"unrecognized": {
-			input:   "unrecognized",
-			wantErr: fmt.Errorf("unknown proxy strategy: unrecognized"),
-		},
-		"default": {
-			input: "default",
-			want:  ProxyStrategyDefault,
-		},
-		"destHost": {
-			input: "destHost",
-			want:  ProxyStrategyDestHost,
-		},
-		"defaultRoute": {
-			input: "defaultRoute",
-			want:  ProxyStrategyDefaultRoute,
-		},
-	} {
-		t.Run(desc, func(t *testing.T) {
-			got, err := ParseProxyStrategy(tc.input)
-			assert.Equal(t, tc.wantErr, err, "ParseProxyStrategy(%s): got error %q, want %v", tc.input, err, tc.wantErr)
-			if got != tc.want {
-				t.Errorf("ParseProxyStrategy(%s): got %v, want %v", tc.input, got, tc.want)
-			}
-		})
-	}
-}
-
-func TestParseProxyStrategies(t *testing.T) {
-	for desc, tc := range map[string]struct {
-		input   string
-		want    []ProxyStrategy
-		wantErr error
-	}{
-		"empty": {
-			input:   "",
-			wantErr: fmt.Errorf("proxy strategies cannot be empty"),
-		},
-		"unrecognized": {
-			input:   "unrecognized",
-			wantErr: fmt.Errorf("unknown proxy strategy: unrecognized"),
-		},
-		"default": {
-			input: "default",
-			want:  []ProxyStrategy{ProxyStrategyDefault},
-		},
-		"destHost": {
-			input: "destHost",
-			want:  []ProxyStrategy{ProxyStrategyDestHost},
-		},
-		"defaultRoute": {
-			input: "defaultRoute",
-			want:  []ProxyStrategy{ProxyStrategyDefaultRoute},
-		},
-		"duplicate": {
-			input: "destHost,defaultRoute,defaultRoute,default",
-			want:  []ProxyStrategy{ProxyStrategyDestHost, ProxyStrategyDefaultRoute, ProxyStrategyDefaultRoute, ProxyStrategyDefault},
-		},
-		"multiple": {
-			input: "destHost,defaultRoute,default",
-			want:  []ProxyStrategy{ProxyStrategyDestHost, ProxyStrategyDefaultRoute, ProxyStrategyDefault},
-		},
-	} {
-		t.Run(desc, func(t *testing.T) {
-			got, err := ParseProxyStrategies(tc.input)
-			assert.Equal(t, tc.wantErr, err, "ParseProxyStrategies(%s): got error %q, want %v", tc.input, err, tc.wantErr)
-			if !reflect.DeepEqual(got, tc.want) {
-				t.Errorf("ParseProxyStrategies(%s): got %v, want %v", tc.input, got, tc.want)
-			}
-		})
-	}
-}
diff --git a/pkg/server/proxystrategies/proxystrategies_test.go b/pkg/server/proxystrategies/proxystrategies_test.go
new file mode 100644
index 00000000..bb050424
--- /dev/null
+++ b/pkg/server/proxystrategies/proxystrategies_test.go
@@ -0,0 +1,129 @@
+package proxystrategies
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProxyStrategy(t *testing.T) {
+	for desc, tc := range map[string]struct {
+		input     ProxyStrategy
+		want      string
+		wantPanic string
+	}{
+		"default": {
+			input: ProxyStrategyDefault,
+			want:  "default",
+		},
+		"destHost": {
+			input: ProxyStrategyDestHost,
+			want:  "destHost",
+		},
+		"defaultRoute": {
+			input: ProxyStrategyDefaultRoute,
+			want:  "defaultRoute",
+		},
+		"unrecognized": {
+			input:     ProxyStrategy(0),
+			wantPanic: "unhandled ProxyStrategy: 0",
+		},
+	} {
+		t.Run(desc, func(t *testing.T) {
+			if tc.wantPanic != "" {
+				assert.PanicsWithValue(t, tc.wantPanic, func() {
+					_ = tc.input.String()
+				})
+			} else {
+				got := tc.input.String()
+				if got != tc.want {
+					t.Errorf("ProxyStrategy.String(): got %v, want %v", got, tc.want)
+				}
+			}
+		})
+	}
+}
+
+func TestParseProxyStrategy(t *testing.T) {
+	for desc, tc := range map[string]struct {
+		input   string
+		want    ProxyStrategy
+		wantErr error
+	}{
+		"empty": {
+			input:   "",
+			wantErr: fmt.Errorf("unknown proxy strategy: "),
+		},
+		"unrecognized": {
+			input:   "unrecognized",
+			wantErr: fmt.Errorf("unknown proxy strategy: unrecognized"),
+		},
+		"default": {
+			input: "default",
+			want:  ProxyStrategyDefault,
+		},
+		"destHost": {
+			input: "destHost",
+			want:  ProxyStrategyDestHost,
+		},
+		"defaultRoute": {
+			input: "defaultRoute",
+			want:  ProxyStrategyDefaultRoute,
+		},
+	} {
+		t.Run(desc, func(t *testing.T) {
+			got, err := ParseProxyStrategy(tc.input)
+			assert.Equal(t, tc.wantErr, err, "ParseProxyStrategy(%s): got error %q, want %v", tc.input, err, tc.wantErr)
+			if got != tc.want {
+				t.Errorf("ParseProxyStrategy(%s): got %v, want %v", tc.input, got, tc.want)
+			}
+		})
+	}
+}
+
+func TestParseProxyStrategies(t *testing.T) {
+	for desc, tc := range map[string]struct {
+		input   string
+		want    []ProxyStrategy
+		wantErr error
+	}{
+		"empty": {
+			input:   "",
+			wantErr: fmt.Errorf("proxy strategies cannot be empty"),
+		},
+		"unrecognized": {
+			input:   "unrecognized",
+			wantErr: fmt.Errorf("unknown proxy strategy: unrecognized"),
+		},
+		"default": {
+			input: "default",
+			want:  []ProxyStrategy{ProxyStrategyDefault},
+		},
+		"destHost": {
+			input: "destHost",
+			want:  []ProxyStrategy{ProxyStrategyDestHost},
+		},
+		"defaultRoute": {
+			input: "defaultRoute",
+			want:  []ProxyStrategy{ProxyStrategyDefaultRoute},
+		},
+		"duplicate": {
+			input: "destHost,defaultRoute,defaultRoute,default",
+			want:  []ProxyStrategy{ProxyStrategyDestHost, ProxyStrategyDefaultRoute, ProxyStrategyDefaultRoute, ProxyStrategyDefault},
+		},
+		"multiple": {
+			input: "destHost,defaultRoute,default",
+			want:  []ProxyStrategy{ProxyStrategyDestHost, ProxyStrategyDefaultRoute, ProxyStrategyDefault},
+		},
+	} {
+		t.Run(desc, func(t *testing.T) {
+			got, err := ParseProxyStrategies(tc.input)
+			assert.Equal(t, tc.wantErr, err, "ParseProxyStrategies(%s): got error %q, want %v", tc.input, err, tc.wantErr)
+			if !reflect.DeepEqual(got, tc.want) {
+				t.Errorf("ParseProxyStrategies(%s): got %v, want %v", tc.input, got, tc.want)
+			}
+		})
+	}
+}
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 55816652..ace44b34 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -39,6 +39,7 @@ import (
 
 	client "sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client"
 	"sigs.k8s.io/apiserver-network-proxy/pkg/server/metrics"
+	"sigs.k8s.io/apiserver-network-proxy/pkg/server/proxystrategies"
 	metricstest "sigs.k8s.io/apiserver-network-proxy/pkg/testing/metrics"
 	agentmock "sigs.k8s.io/apiserver-network-proxy/proto/agent/mocks"
 	"sigs.k8s.io/apiserver-network-proxy/proto/header"
@@ -169,7 +170,7 @@ func TestAgentTokenAuthenticationErrorsToken(t *testing.T) {
 				conn.EXPECT().Recv().Return(nil, io.EOF)
 			}
 
-			p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{
+			p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{
 				Enabled:             true,
 				KubernetesClient:    kcs,
 				AgentNamespace:      tc.wantNamespace,
@@ -197,7 +198,7 @@ func TestRemovePendingDialForStream(t *testing.T) {
 	pending3 := &ProxyClientConnection{frontend: &GrpcFrontend{streamUID: streamUID}}
 	pending4 := &ProxyClientConnection{frontend: &GrpcFrontend{streamUID: "different-uid"}}
 	pending5 := &ProxyClientConnection{frontend: &GrpcFrontend{streamUID: ""}}
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 	p.PendingDial.Add(1, pending1)
 	p.PendingDial.Add(2, pending2)
 	p.PendingDial.Add(3, pending3)
@@ -225,7 +226,7 @@ func TestAddRemoveFrontends(t *testing.T) {
 	agent2ConnID2 := new(ProxyClientConnection)
 	agent3ConnID1 := new(ProxyClientConnection)
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 	p.addEstablished("agent1", int64(1), agent1ConnID1)
 	p.removeEstablished("agent1", int64(1))
 	expectedFrontends := make(map[string]map[int64]*ProxyClientConnection)
@@ -233,7 +234,7 @@ func TestAddRemoveFrontends(t *testing.T) {
 		t.Errorf("expected %v, got %v", e, a)
 	}
 
-	p = NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p = NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 	p.addEstablished("agent1", int64(1), agent1ConnID1)
 	p.addEstablished("agent1", int64(2), agent1ConnID2)
 	p.addEstablished("agent2", int64(1), agent2ConnID1)
@@ -263,7 +264,7 @@ func TestAddRemoveBackends_DefaultStrategy(t *testing.T) {
 	backend2, _ := NewBackend(mockAgentConn(ctrl, "agent2", []string{}))
 	backend3, _ := NewBackend(mockAgentConn(ctrl, "agent3", []string{}))
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 
 	p.addBackend(backend1)
 
@@ -295,7 +296,7 @@ func TestAddRemoveBackends_DefaultRouteStrategy(t *testing.T) {
 	backend2, _ := NewBackend(mockAgentConn(ctrl, "agent2", []string{"default-route=false"}))
 	backend3, _ := NewBackend(mockAgentConn(ctrl, "agent3", []string{"default-route=true"}))
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefaultRoute}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefaultRoute}, 1, nil, xfrChannelSize)
 
 	p.addBackend(backend1)
 
@@ -337,7 +338,7 @@ func TestAddRemoveBackends_DestHostStrategy(t *testing.T) {
 	backend2, _ := NewBackend(mockAgentConn(ctrl, "agent2", []string{"default-route=true"}))
 	backend3, _ := NewBackend(mockAgentConn(ctrl, "agent3", []string{"host=node2.mydomain.com&ipv4=5.6.7.8&ipv6=::"}))
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDestHost}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDestHost}, 1, nil, xfrChannelSize)
 
 	p.addBackend(backend1)
 	p.addBackend(backend2)
@@ -384,7 +385,7 @@ func TestAddRemoveBackends_DestHostSanitizeRequest(t *testing.T) {
 	backend1, _ := NewBackend(mockAgentConn(ctrl, "agent1", []string{"host=localhost&host=node1.mydomain.com&ipv4=1.2.3.4&ipv6=9878::7675:1292:9183:7562"}))
 	backend2, _ := NewBackend(mockAgentConn(ctrl, "agent2", []string{"host=node2.mydomain.com&ipv4=5.6.7.8&ipv6=::"}))
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDestHost}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDestHost}, 1, nil, xfrChannelSize)
 
 	p.addBackend(backend1)
 	p.addBackend(backend2)
@@ -408,7 +409,7 @@ func TestAddRemoveBackends_DestHostWithDefault(t *testing.T) {
 	backend2, _ := NewBackend(mockAgentConn(ctrl, "agent2", []string{"default-route=false"}))
 	backend3, _ := NewBackend(mockAgentConn(ctrl, "agent3", []string{"host=node2.mydomain.com&ipv4=5.6.7.8&ipv6=::"}))
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDestHost, ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDestHost, proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 
 	p.addBackend(backend1)
 	p.addBackend(backend2)
@@ -461,7 +462,7 @@ func TestAddRemoveBackends_DestHostWithDuplicateIdents(t *testing.T) {
 	backend2, _ := NewBackend(mockAgentConn(ctrl, "agent2", []string{"host=localhost&host=node1.mydomain.com&ipv4=1.2.3.4&ipv6=9878::7675:1292:9183:7562"}))
 	backend3, _ := NewBackend(mockAgentConn(ctrl, "agent3", []string{"host=localhost&host=node2.mydomain.com&ipv4=5.6.7.8&ipv6=::"}))
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDestHost, ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDestHost, proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 
 	p.addBackend(backend1)
 	p.addBackend(backend2)
@@ -518,7 +519,7 @@ func TestEstablishedConnsMetric(t *testing.T) {
 	agent2ConnID2 := new(ProxyClientConnection)
 	agent3ConnID1 := new(ProxyClientConnection)
 
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 	p.addEstablished("agent1", int64(1), agent1ConnID1)
 	assertEstablishedConnsMetric(t, 1)
 	p.addEstablished("agent1", int64(2), agent1ConnID2)
@@ -550,7 +551,7 @@ func TestRemoveEstablishedForBackendConn(t *testing.T) {
 	agent2ConnID1 := &ProxyClientConnection{backend: backend2}
 	agent2ConnID2 := &ProxyClientConnection{backend: backend2}
 	agent3ConnID1 := &ProxyClientConnection{backend: backend3}
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 	p.addEstablished("agent1", int64(1), agent1ConnID1)
 	p.addEstablished("agent1", int64(2), agent1ConnID2)
 	p.addEstablished("agent2", int64(1), agent2ConnID1)
@@ -581,7 +582,7 @@ func TestRemoveEstablishedForStream(t *testing.T) {
 	agent2ConnID1 := &ProxyClientConnection{backend: backend2, frontend: &GrpcFrontend{streamUID: streamUID}}
 	agent2ConnID2 := &ProxyClientConnection{backend: backend2}
 	agent3ConnID1 := &ProxyClientConnection{backend: backend3, frontend: &GrpcFrontend{streamUID: streamUID}}
-	p := NewProxyServer("", []ProxyStrategy{ProxyStrategyDefault}, 1, nil, xfrChannelSize)
+	p := NewProxyServer("", []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, nil, xfrChannelSize)
 	p.addEstablished("agent1", int64(1), agent1ConnID1)
 	p.addEstablished("agent1", int64(2), agent1ConnID2)
 	p.addEstablished("agent2", int64(1), agent2ConnID1)
@@ -615,14 +616,17 @@ func prepareFrontendConn(ctrl *gomock.Controller) *agentmock.MockAgentService_Co
 	return frontendConn
 }
 
-func prepareAgentConnMD(t testing.TB, ctrl *gomock.Controller, proxyServer *ProxyServer) (*agentmock.MockAgentService_ConnectServer, *Backend) {
+func prepareAgentConnMD(t testing.TB, ctrl *gomock.Controller, proxyServer *ProxyServer, agentidentifiers []string) (*agentmock.MockAgentService_ConnectServer, *Backend) {
 	t.Helper()
+	if agentidentifiers == nil {
+		agentidentifiers = []string{}
+	}
 	// prepare the the connection to agent of proxy-server
 	agentConn := agentmock.NewMockAgentService_ConnectServer(ctrl)
 	agentConnMD := metadata.MD{
 		":authority":       []string{"127.0.0.1:8091"},
 		"agentid":          []string{uuid.New().String()},
-		"agentidentifiers": []string{},
+		"agentidentifiers": agentidentifiers,
 		"content-type":     []string{"application/grpc"},
 		"user-agent":       []string{"grpc-go/1.42.0"},
 	}
@@ -641,7 +645,7 @@ func baseServerProxyTestWithoutBackend(t *testing.T, validate func(*agentmock.Mo
 	defer ctrl.Finish()
 
 	frontendConn := prepareFrontendConn(ctrl)
-	proxyServer := NewProxyServer(uuid.New().String(), []ProxyStrategy{ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
+	proxyServer := NewProxyServer(uuid.New().String(), []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
 
 	validate(frontendConn)
 
@@ -655,9 +659,9 @@ func baseServerProxyTestWithBackend(t *testing.T, validate func(*agentmock.MockA
 	frontendConn := prepareFrontendConn(ctrl)
 
 	// prepare proxy server
-	proxyServer := NewProxyServer(uuid.New().String(), []ProxyStrategy{ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
+	proxyServer := NewProxyServer(uuid.New().String(), []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
 
-	agentConn, _ := prepareAgentConnMD(t, ctrl, proxyServer)
+	agentConn, _ := prepareAgentConnMD(t, ctrl, proxyServer, nil)
 
 	validate(frontendConn, agentConn)
 
@@ -861,16 +865,37 @@ func TestReadyBackendsMetric(t *testing.T) {
 
 	metrics.Metrics.Reset()
 
-	p := NewProxyServer(uuid.New().String(), []ProxyStrategy{ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
+	p := NewProxyServer(uuid.New().String(), []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
 	assertReadyBackendsMetric(t, 0)
 
-	_, backend := prepareAgentConnMD(t, ctrl, p)
+	_, backend := prepareAgentConnMD(t, ctrl, p, nil)
 	assertReadyBackendsMetric(t, 1)
 
 	p.removeBackend(backend)
 	assertReadyBackendsMetric(t, 0)
 }
 
+func TestTotalReadyBackendsMetric(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	metrics.Metrics.Reset()
+
+	p := NewProxyServer(uuid.New().String(), []proxystrategies.ProxyStrategy{proxystrategies.ProxyStrategyDefault, proxystrategies.ProxyStrategyDestHost}, 1, &AgentTokenAuthenticationOptions{}, xfrChannelSize)
+	assertTotalReadyBackendsMetric(t, map[string]int{proxystrategies.ProxyStrategyDefault.String(): 0, proxystrategies.ProxyStrategyDestHost.String(): 0})
+
+	_, backend1 := prepareAgentConnMD(t, ctrl, p, nil)
+	assertTotalReadyBackendsMetric(t, map[string]int{proxystrategies.ProxyStrategyDefault.String(): 1, proxystrategies.ProxyStrategyDestHost.String(): 0})
+
+	// Add a backend with IPv4 agent identifier.
+	_, backend2 := prepareAgentConnMD(t, ctrl, p, []string{"host=localhost"})
+	assertTotalReadyBackendsMetric(t, map[string]int{proxystrategies.ProxyStrategyDefault.String(): 2, proxystrategies.ProxyStrategyDestHost.String(): 1})
+
+	p.removeBackend(backend1)
+	p.removeBackend(backend2)
+	assertTotalReadyBackendsMetric(t, map[string]int{proxystrategies.ProxyStrategyDefault.String(): 0, proxystrategies.ProxyStrategyDestHost.String(): 0})
+}
+
 func dialReqPkt(dialID int64) *client.Packet {
 	return &client.Packet{
 		Type: client.PacketType_DIAL_REQ,
@@ -932,6 +957,13 @@ func assertReadyBackendsMetric(t testing.TB, expect int) {
 	}
 }
 
+func assertTotalReadyBackendsMetric(t testing.TB, expect map[string]int) {
+	t.Helper()
+	if err := metricstest.DefaultTester.ExpectServerTotalReadyBackends(expect); err != nil {
+		t.Errorf("Expected %s metric for each proxy strategy %+v, but got error: %v", "ready_backends", expect, err)
+	}
+}
+
 func dialClosePkt(dialID int64) *client.Packet {
 	return &client.Packet{
 		Type: client.PacketType_DIAL_CLS,
diff --git a/pkg/testing/metrics/metrics.go b/pkg/testing/metrics/metrics.go
index 664b127f..ffc5bd8e 100644
--- a/pkg/testing/metrics/metrics.go
+++ b/pkg/testing/metrics/metrics.go
@@ -38,10 +38,15 @@ const (
 	serverPendingDialsSample = `konnectivity_network_proxy_server_pending_backend_dials{} %d`
 
 	serverReadyBackendsHeader = `
-# HELP konnectivity_network_proxy_server_ready_backend_connections Number of konnectivity agent connected to the proxy server
+# HELP konnectivity_network_proxy_server_ready_backend_connections Number of konnectivity agent connected to the proxy server. DEPRECATED, please use ready_backends
 # TYPE konnectivity_network_proxy_server_ready_backend_connections gauge`
 	serverReadyBackendsSample = `konnectivity_network_proxy_server_ready_backend_connections{} %d`
 
+	serverTotalReadyBackendsHeader = `
+# HELP konnectivity_network_proxy_server_ready_backends Number of konnectivity agent connected to the proxy server
+# TYPE konnectivity_network_proxy_server_ready_backends gauge`
+	serverTotalReadyBackendsSample = `konnectivity_network_proxy_server_ready_backends{proxy_strategy="%s"} %d`
+
 	serverEstablishedConnsHeader = `
 # HELP konnectivity_network_proxy_server_established_connections Current number of established end-to-end connections (post-dial).
 # TYPE konnectivity_network_proxy_server_established_connections gauge`
@@ -106,6 +111,14 @@ func (t *Tester) ExpectServerReadyBackends(v int) error {
 	return t.ExpectMetric(server.Namespace, server.Subsystem, "ready_backend_connections", expect)
 }
 
+func (t *Tester) ExpectServerTotalReadyBackends(expected map[string]int) error {
+	expect := serverTotalReadyBackendsHeader + "\n"
+	for proxyStrategy, numOfBackends := range expected {
+		expect += fmt.Sprintf(serverTotalReadyBackendsSample+"\n", proxyStrategy, numOfBackends)
+	}
+	return t.ExpectMetric(server.Namespace, server.Subsystem, "ready_backends", expect)
+}
+
 func (t *Tester) ExpectServerEstablishedConns(v int) error {
 	expect := serverEstablishedConnsHeader + "\n"
 	expect += fmt.Sprintf(serverEstablishedConnsSample+"\n", v)