Repository: kiali/kiali

Issue:
document how to test a remote cluster secret / kubeconfig
We should add an FAQ entry on how to test a remote cluster secret to help debug issues with the kubeconfig.

See: https://github.com/kiali/kiali/discussions/8356#discussioncomment-12912561

That comment provides easy steps a user can do in order to quickly test their remote cluster secret's kubeconfig. We can tweek those instructions for the FAQ as necessary. But in short, we need to check that the kubeconfig is authorized to "watch" resources and it should be able to "get" resources as well.  We should consult the [server Role](https://github.com/kiali/helm-charts/blob/master/kiali-server/templates/role.yaml) and see if we can get the FAQ to be able to check for as many as we can.

Hints from issue comments:
@jmazzitelli: I have a script to do this verification - but I guess I should add a kiali.io PR to document it somewhere.

Solution patch:
diff --git a/business/apps.go b/business/apps.go
index 2b9cdbc2e..71b5e1d7d 100644
--- a/business/apps.go
+++ b/business/apps.go
@@ -80,7 +80,7 @@ func (in *AppService) GetClusterAppList(ctx context.Context, criteria AppCriteri
 	ctx, end = observability.StartSpan(ctx, "GetClusterAppList",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", criteria.Namespace),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("includeHealth", criteria.IncludeHealth),
 		observability.Attribute("includeIstioResources", criteria.IncludeIstioResources),
 		observability.Attribute("rateInterval", criteria.RateInterval),
@@ -204,7 +204,7 @@ func (in *AppService) GetAppList(ctx context.Context, criteria AppCriteria) (mod
 	ctx, end = observability.StartSpan(ctx, "GetAppList",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", criteria.Namespace),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("includeHealth", criteria.IncludeHealth),
 		observability.Attribute("includeIstioResources", criteria.IncludeIstioResources),
 		observability.Attribute("rateInterval", criteria.RateInterval),
@@ -359,7 +359,7 @@ func (in *AppService) GetAppDetails(ctx context.Context, criteria AppCriteria) (
 	ctx, end = observability.StartSpan(ctx, "GetApp",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", criteria.Namespace),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("appName", criteria.AppName),
 		observability.Attribute("rateInterval", criteria.RateInterval),
 		observability.Attribute("queryTime", criteria.QueryTime),
diff --git a/business/health.go b/business/health.go
index d02948568..7e3d8da82 100644
--- a/business/health.go
+++ b/business/health.go
@@ -56,7 +56,7 @@ func (in *HealthService) GetAppHealth(ctx context.Context, namespace, cluster, a
 	_, end = observability.StartSpan(ctx, "GetAppHealth",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", namespace),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("app", app),
 		observability.Attribute("rateInterval", rateInterval),
 		observability.Attribute("queryTime", queryTime),
@@ -125,7 +125,7 @@ func (in *HealthService) GetNamespaceAppHealth(ctx context.Context, criteria Nam
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetNamespaceAppHealth",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("namespace", criteria.Namespace),
 		observability.Attribute("rateInterval", criteria.RateInterval),
 		observability.Attribute("queryTime", criteria.QueryTime),
@@ -194,7 +194,7 @@ func (in *HealthService) GetNamespaceServiceHealth(ctx context.Context, criteria
 	ctx, end = observability.StartSpan(ctx, "GetNamespaceServiceHealth",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", criteria.Namespace),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("rateInterval", criteria.RateInterval),
 		observability.Attribute("queryTime", criteria.QueryTime),
 	)
@@ -273,7 +273,7 @@ func (in *HealthService) GetNamespaceWorkloadHealth(ctx context.Context, criteri
 	ctx, end = observability.StartSpan(ctx, "GetNamespaceWorkloadHealth",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", namespace),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("rateInterval", rateInterval),
 		observability.Attribute("queryTime", queryTime),
 	)
diff --git a/business/istio_config.go b/business/istio_config.go
index 214c02577..1470a2ff2 100644
--- a/business/istio_config.go
+++ b/business/istio_config.go
@@ -501,7 +501,7 @@ func (in *IstioConfigService) GetIstioConfigDetails(ctx context.Context, cluster
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetIstioConfigDetails",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("objectGVK", objectGVK.String()),
 		observability.Attribute("object", object),
diff --git a/business/istio_validations.go b/business/istio_validations.go
index ed35a7055..12dbf66c0 100644
--- a/business/istio_validations.go
+++ b/business/istio_validations.go
@@ -270,7 +270,7 @@ func (in *IstioValidationsService) Validate(ctx context.Context, cluster string,
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "getValidations",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 	)
 	defer end()
 
@@ -491,7 +491,7 @@ func (in *IstioValidationsService) ValidateIstioObject(ctx context.Context, clus
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetIstioObjectValidations",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", "cluster"),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("objectGVK", objectGVK.String()),
 		observability.Attribute("object", object),
diff --git a/business/namespaces.go b/business/namespaces.go
index 1ef779805..6e99086b8 100644
--- a/business/namespaces.go
+++ b/business/namespaces.go
@@ -261,7 +261,7 @@ func (in *NamespaceService) GetClusterNamespace(ctx context.Context, namespace s
 	_, end = observability.StartSpan(ctx, "GetClusterNamespace",
 		observability.Attribute("package", "business"),
 		observability.Attribute("namespace", namespace),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 	)
 	defer end()
 
diff --git a/business/services.go b/business/services.go
index 2e09fc78b..9e5530403 100644
--- a/business/services.go
+++ b/business/services.go
@@ -50,7 +50,7 @@ func (in *SvcService) GetServiceList(ctx context.Context, criteria ServiceCriter
 
 	ctx, end = observability.StartSpan(ctx, "GetServiceList",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("namespace", criteria.Namespace),
 		observability.Attribute("includeHealth", criteria.IncludeHealth),
 		observability.Attribute("includeIstioResources", criteria.IncludeIstioResources),
@@ -432,7 +432,7 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace,
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetServiceDetails",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("service", service),
 		observability.Attribute("interval", interval),
@@ -456,7 +456,6 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace,
 		return nil, err
 	}
 
-	var eps *core_v1.Endpoints
 	var pods []core_v1.Pod
 	var hth models.ServiceHealth
 	var istioConfigList *models.IstioConfigList
@@ -498,15 +497,6 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace,
 		}
 	}
 
-	eps = &core_v1.Endpoints{}
-	if err := kubeCache.Get(ctx, client.ObjectKey{Name: service, Namespace: namespace}, eps); err != nil {
-		if errors.IsNotFound(err) {
-			eps = nil
-		} else {
-			return nil, fmt.Errorf("Error fetching Endpoints namespace %s: and service %s: %s", namespace, service, err)
-		}
-	}
-
 	wg.Add(1)
 	go func(ctx context.Context) {
 		defer wg.Done()
@@ -622,14 +612,14 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace,
 
 	s := models.ServiceDetails{Workloads: wo, Health: hth, NamespaceMTLS: nsmtls, SubServices: serviceOverviews}
 	s.Service = svc
-	s.SetPods(kubernetes.FilterPodsByEndpoints(eps, pods))
+	s.SetPods(pods)
+	s.Endpoints = *models.GetEndpointsFromPods(pods)
 	// ServiceDetail will consider if the Service is a External/Federation entry
 	if s.Service.Type == "External" || s.Service.Type == "Federation" {
 		s.IstioSidecar = true
 	} else {
 		s.SetIstioSidecar(wo)
 	}
-	s.SetEndpoints(eps)
 	s.IstioPermissions = models.ResourcePermissions{
 		Create: vsCreate,
 		Update: vsUpdate,
@@ -781,7 +771,7 @@ func (in *SvcService) UpdateService(ctx context.Context, cluster, namespace, ser
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "UpdateService",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("service", service),
 		observability.Attribute("interval", interval),
@@ -833,7 +823,7 @@ func (in *SvcService) GetService(ctx context.Context, cluster, namespace, servic
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetService",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("service", service),
 	)
@@ -896,7 +886,7 @@ func (in *SvcService) GetServiceTracingName(ctx context.Context, cluster, namesp
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetServiceTracingName",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("service", service),
 	)
diff --git a/business/tls.go b/business/tls.go
index 363f833be..68b972c8d 100644
--- a/business/tls.go
+++ b/business/tls.go
@@ -37,7 +37,7 @@ func (in *TLSService) MeshWidemTLSStatus(ctx context.Context, cluster string, re
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "MeshWidemTLSStatus",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("revision", revision),
 	)
 	defer end()
@@ -109,7 +109,7 @@ func (in *TLSService) NamespaceWidemTLSStatus(ctx context.Context, namespace, cl
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "NamespaceWidemTLSStatus",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 	)
 	defer end()
@@ -159,7 +159,7 @@ func (in *TLSService) ClusterWideNSmTLSStatus(ctx context.Context, namespaces []
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "ClusterWideNSmTLSStatus",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 	)
 	defer end()
 
diff --git a/business/tracing.go b/business/tracing.go
index 7443d0af8..441b3b642 100644
--- a/business/tracing.go
+++ b/business/tracing.go
@@ -74,7 +74,7 @@ func (in *TracingService) GetServiceSpans(ctx context.Context, ns, service strin
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetServiceSpans",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", query.Cluster),
+		observability.Attribute(observability.TracingClusterTag, query.Cluster),
 		observability.Attribute("namespace", ns),
 		observability.Attribute("service", service),
 	)
@@ -106,7 +106,7 @@ func (in *TracingService) GetWorkloadSpans(ctx context.Context, ns, workload str
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetWorkloadSpans",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", query.Cluster),
+		observability.Attribute(observability.TracingClusterTag, query.Cluster),
 		observability.Attribute("namespace", ns),
 		observability.Attribute("workload", workload),
 	)
@@ -165,7 +165,7 @@ func (in *TracingService) GetServiceTraces(ctx context.Context, ns, service stri
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetServiceTraces",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", query.Cluster),
+		observability.Attribute(observability.TracingClusterTag, query.Cluster),
 		observability.Attribute("namespace", ns),
 		observability.Attribute("service", service),
 	)
@@ -211,7 +211,7 @@ func (in *TracingService) GetWorkloadTraces(ctx context.Context, ns, workload st
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetWorkloadTraces",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", query.Cluster),
+		observability.Attribute(observability.TracingClusterTag, query.Cluster),
 		observability.Attribute("namespace", ns),
 		observability.Attribute("workload", workload),
 	)
diff --git a/business/workloads.go b/business/workloads.go
index 49a5b60f3..dbc47acc0 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -158,7 +158,7 @@ func (in *WorkloadService) GetAllWorkloads(ctx context.Context, cluster string,
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetAllWorkloads",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 	)
 	defer end()
 
@@ -212,7 +212,7 @@ func (in *WorkloadService) GetWorkloadList(ctx context.Context, criteria Workloa
 		observability.Attribute("package", "business"),
 		observability.Attribute("includeHealth", criteria.IncludeHealth),
 		observability.Attribute("includeIstioResources", criteria.IncludeIstioResources),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("namespace", criteria.Namespace),
 		observability.Attribute("rateInterval", criteria.RateInterval),
 		observability.Attribute("queryTime", criteria.QueryTime),
@@ -438,7 +438,7 @@ func (in *WorkloadService) GetWorkload(ctx context.Context, criteria WorkloadCri
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetWorkload",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", criteria.Cluster),
+		observability.Attribute(observability.TracingClusterTag, criteria.Cluster),
 		observability.Attribute("namespace", criteria.Namespace),
 		observability.Attribute("workloadName", criteria.WorkloadName),
 		observability.Attribute("workloadType", criteria.WorkloadGVK.String()),
@@ -501,7 +501,7 @@ func (in *WorkloadService) UpdateWorkload(ctx context.Context, cluster string, n
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "UpdateWorkload",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("workloadName", workloadName),
 		observability.Attribute("workloadKind", workloadGVK.Kind),
@@ -2481,7 +2481,7 @@ func (in *WorkloadService) GetWorkloadTracingName(ctx context.Context, cluster,
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetWorkloadTracingName",
 		observability.Attribute("package", "business"),
-		observability.Attribute("cluster", cluster),
+		observability.Attribute(observability.TracingClusterTag, cluster),
 		observability.Attribute("namespace", namespace),
 		observability.Attribute("workload", workload),
 	)
diff --git a/cache/transform.go b/cache/transform.go
index 72c3f80e9..b3cbde424 100644
--- a/cache/transform.go
+++ b/cache/transform.go
@@ -28,11 +28,12 @@ func TransformPod(pod any) (any, error) {
 			Hostname:           obj.Spec.Hostname,
 		},
 		Status: corev1.PodStatus{
-			Phase:                 obj.Status.Phase,
+			ContainerStatuses:     obj.Status.ContainerStatuses,
+			InitContainerStatuses: obj.Status.InitContainerStatuses,
 			Message:               obj.Status.Message,
+			Phase:                 obj.Status.Phase,
+			PodIP:                 obj.Status.PodIP,
 			Reason:                obj.Status.Reason,
-			InitContainerStatuses: obj.Status.InitContainerStatuses,
-			ContainerStatuses:     obj.Status.ContainerStatuses,
 		},
 	}
 
diff --git a/frontend/src/components/MTls/MeshMTLSStatus.tsx b/frontend/src/components/MTls/MeshMTLSStatus.tsx
index 053f22d50..326267a96 100644
--- a/frontend/src/components/MTls/MeshMTLSStatus.tsx
+++ b/frontend/src/components/MTls/MeshMTLSStatus.tsx
@@ -98,7 +98,6 @@ class MeshMTLSStatusComponent extends React.Component<Props> {
     // leaving empty cluster param here, home cluster will be used by default
     API.getMeshTls(this.props.cluster, this.props.revision)
       .then(response => {
-        console.log(response);
         return this.props.setMeshTlsStatus(response.data);
       })
       .catch(error => {
diff --git a/hack/ci-kind-molecule-tests.sh b/hack/ci-kind-molecule-tests.sh
index 21baf0217..60c065a96 100755
--- a/hack/ci-kind-molecule-tests.sh
+++ b/hack/ci-kind-molecule-tests.sh
@@ -403,7 +403,8 @@ fi
 # if requested, install OLM and the Kiali Operator via OLM
 if [ "${OLM_ENABLED}" == "true" ]; then
   if [ "${OLM_VERSION}" == "latest" ]; then
-    OLM_VERSION="$(curl -s https://api.github.com/repos/operator-framework/operator-lifecycle-manager/releases 2> /dev/null | grep "tag_name" | sed -e 's/.*://' -e 's/ *"//' -e 's/",//' | grep -v "snapshot" | sort -t "." -k 1.2g,1 -k 2g,2 -k 3g | tail -n 1)"
+    for i in {1..60}; do curl_output=$(curl -s https://api.github.com/repos/operator-framework/operator-lifecycle-manager/releases 2> /dev/null) && [ -n "$curl_output" ] && break || { echo "Retry $i/60: Attempting to get the latest OLM version from GitHub, retrying in 60 seconds..."; sleep 60; }; done; if [ -z "$curl_output" ]; then echo "Failed to obtain the latest OLM version from GitHub - curl failed"; exit 1; fi
+    OLM_VERSION="$(echo "$curl_output" | grep "tag_name" | sed -e 's/.*://' -e 's/ *"//' -e 's/",//' | grep -v "snapshot" | sort -t "." -k 1.2g,1 -k 2g,2 -k 3g | tail -n 1)"
     if [ -z "${OLM_VERSION}" ]; then
       infomsg "Failed to obtain the latest OLM version from Github."
       exit 1
@@ -421,13 +422,13 @@ if [ "${OLM_ENABLED}" == "true" ]; then
   export CLIENT_EXE
   export -f kubectl
   curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/${OLM_VERSION}/install.sh | bash -s ${OLM_VERSION}
-  [ "$?" != "0" ] && echo "ERROR: Failed to install OLM" && exit 1
+  [ "$?" != "0" ] && infomsg "ERROR: Failed to install OLM" && exit 1
   unset -f kubectl
 
   infomsg "OLM ${OLM_VERSION} is installed."
 
   infomsg "Installing Kiali Operator via OLM"
-  ${CLIENT_EXE} create -f https://operatorhub.io/install/stable/kiali.yaml
+  for i in {1..60}; do ${CLIENT_EXE} create -f https://operatorhub.io/install/stable/kiali.yaml && break || { [ "$i" -lt 60 ] && infomsg "Retry $i/60: Attempting to install Kiali Operator subscription, retrying in 60 seconds..." && sleep 60; }; done || { infomsg "Error: Cannot install Kiali Operator subscription."; exit 1; }
 
   infomsg -n "Waiting for Kiali CRD to be created."
   timeout 1h bash -c "until ${CLIENT_EXE} get crd kialis.kiali.io >& /dev/null; do echo -n '.' ; sleep 3; done"
diff --git a/hack/istio/multicluster/verify-kiali-permissions.sh b/hack/istio/multicluster/verify-kiali-permissions.sh
new file mode 100755
index 000000000..3cfb4582b
--- /dev/null
+++ b/hack/istio/multicluster/verify-kiali-permissions.sh
@@ -0,0 +1,222 @@
+#!/usr/bin/env bash
+
+##############################################################################
+# This script checks the permissions of a given kubeconfig to ensure it
+# has the necessary permissions so Kiali can use it to access a remote
+# cluster.
+#
+# The kubeconfig must be provided as either a kubeconfig file or
+# found within a secret (thus you can generate a Kiali remote cluster secret
+# and use this script to confirm its validity). See the options
+# --kubeconfig-file and --kubeconfig-secret.
+#
+# You must tell the script what version of Kiali you want to test with.
+# See the option --kiali-version.
+#
+# This script will perform auth can-i checks on resources
+# that the kubeconfig should provide permissions for.
+#
+# You must have `helm` and `yq` installed in order to use this script.
+#
+# If you are connected to a cluster with a kubeconfig that you think can be
+# used as a Kiali remote cluster config, you can store it as a secret and
+# then test that to see if it is a valid Kiali remote cluster secret:
+#
+# 1. Save your current kubeconfig context to a file:
+#    kubectl config view --minify --raw > ./active-context-kubeconfig.yaml
+#
+# 2. Create a secret with that kubeconfig file:
+#    kubectl create secret generic mycluster-secret \
+#      --namespace default \
+#      --from-file=mycluster=./active-context-kubeconfig.yaml
+#
+# 3. Use this test script to verify the permissions are correct:
+#    ./verify-kiali-permissions.sh \
+#      --kubeconfig-secret default:mycluster-secret:mycluster \
+#      --kiali-version v2.9.0
+#
+# 4. Once testing is complete, remove the kubeconfig file and delete the secret:
+#    rm ./active-context-kubeconfig.yaml
+#    kubectl delete secret --namespace default mycluster-secret
+##############################################################################
+
+set -euo pipefail
+
+print_help() {
+  cat <<EOF
+Usage: $0 [OPTIONS]
+
+Options:
+  --kubeconfig-file FILE
+        Path to a kubeconfig file.
+  --kubeconfig-secret NS:NAME:KEY
+        Specify kubeconfig as a Kubernetes secret with namespace, secret name, and secret key.
+        Format: --kubeconfig-secret <namespace>:<secretName>:<secretKey>
+  --kiali-version vX.Y.Z
+        Kiali version whose needed permissions will be checked (e.g. v2.9.0).
+        Required.
+  --help
+        Show this help message and exit.
+
+Notes:
+  Exactly one of --kubeconfig-file or --kubeconfig-secret must be specified.
+EOF
+}
+
+KUBECONFIG_FILE=""
+KUBECONFIG_SECRET_SPEC=""
+KIALI_VERSION=""
+
+if [[ $# -eq 0 ]]; then
+  print_help
+  exit 1
+fi
+
+while [[ $# -gt 0 ]]; do
+  case "$1" in
+    --kubeconfig-file)
+      if [[ $# -lt 2 ]]; then
+        echo "Error: --kubeconfig-file requires a file path argument"
+        exit 1
+      fi
+      KUBECONFIG_FILE="$2"
+      shift 2
+      ;;
+    --kubeconfig-secret)
+      if [[ $# -lt 2 ]]; then
+        echo "Error: --kubeconfig-secret requires a secret spec argument"
+        exit 1
+      fi
+      KUBECONFIG_SECRET_SPEC="$2"
+      shift 2
+      ;;
+    --kiali-version)
+      if [[ $# -lt 2 ]]; then
+        echo "Error: --kiali-version requires a version argument"
+        exit 1
+      fi
+      KIALI_VERSION="$2"
+      # Validate version format: must be X.Y.Z or vX.Y.Z
+      if [[ ! "$KIALI_VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+        echo "Error: --kiali-version must be in the form X.Y.Z (optionally prefixed with 'v')"
+        exit 1
+      fi
+      shift 2
+      ;;
+    --help)
+      print_help
+      exit 0
+      ;;
+    *)
+      echo "Unknown option: $1"
+      print_help
+      exit 1
+      ;;
+  esac
+done
+
+# Validate required --kiali-version
+if [[ -z "$KIALI_VERSION" ]]; then
+  echo "Error: --kiali-version is required"
+  print_help
+  exit 1
+fi
+
+# Validate exclusive kubeconfig input
+if [[ -n "$KUBECONFIG_FILE" && -n "$KUBECONFIG_SECRET_SPEC" ]]; then
+  echo "Error: Cannot specify both --kubeconfig-file and --kubeconfig-secret"
+  exit 1
+fi
+
+if [[ -z "$KUBECONFIG_FILE" && -z "$KUBECONFIG_SECRET_SPEC" ]]; then
+  echo "Error: Must specify exactly one of --kubeconfig-file or --kubeconfig-secret"
+  exit 1
+fi
+
+# Parse secret spec if provided
+if [[ -n "$KUBECONFIG_SECRET_SPEC" ]]; then
+  if ! [[ "$KUBECONFIG_SECRET_SPEC" =~ ^[^:]+:[^:]+:[^:]+$ ]]; then
+    echo "Error: --kubeconfig-secret must be in format <namespace>:<secretName>:<secretKey>"
+    exit 1
+  fi
+  IFS=':' read -r KUBECONFIG_SECRET_NS KUBECONFIG_SECRET_NAME KUBECONFIG_SECRET_KEY <<< "$KUBECONFIG_SECRET_SPEC"
+
+  TMP_KUBECONFIG="$(mktemp)"
+  trap 'rm -f "$TMP_KUBECONFIG"' EXIT
+  kubectl get secret "$KUBECONFIG_SECRET_NAME" -n "$KUBECONFIG_SECRET_NS" -o "jsonpath={.data.${KUBECONFIG_SECRET_KEY}}" | base64 -d > "$TMP_KUBECONFIG"
+  KUBECONFIG_FILE="$TMP_KUBECONFIG"
+fi
+
+# Normalize KIALI_VERSION to have "v" prefix
+if [[ "$KIALI_VERSION" != v* ]]; then
+  KIALI_VERSION="v$KIALI_VERSION"
+fi
+
+if [ ! -f "$KUBECONFIG_FILE" ]; then
+  echo "Kubeconfig file [$KUBECONFIG_FILE] does not exist."
+  exit 1
+fi
+
+echo "=== SETTINGS ==="
+echo "KUBECONFIG_FILE=$KUBECONFIG_FILE"
+echo "KUBECONFIG_SECRET_SPEC=${KUBECONFIG_SECRET_SPEC:-<not set>}"
+echo "KIALI_VERSION=$KIALI_VERSION"
+echo "================"
+
+if ! command -v helm &>/dev/null; then
+  echo "Error: 'helm' is required but not found in PATH."
+  exit 1
+fi
+
+if ! command -v yq &>/dev/null; then
+  echo "Error: 'yq' is required but not found in PATH."
+  exit 1
+fi
+
+# Ensure Kiali Helm repo is added and updated
+helm repo add kiali https://kiali.org/helm-charts --force-update
+
+ROLE_YAML=$(helm template kiali-server kiali/kiali-server --version "$KIALI_VERSION" --set auth.strategy=anonymous --show-only templates/role.yaml)
+
+# Extract all resources from ClusterRole rules
+mapfile -t RESOURCES_ARR < <(echo "$ROLE_YAML" | yq eval-all '. | select(.kind == "ClusterRole") | .rules[].resources[]' - | sort -u)
+
+# Initialize an array to hold final resource list
+declare -a RESOURCES_FINAL=()
+
+for resource in "${RESOURCES_ARR[@]}"; do
+  if [[ "$resource" == "*" ]]; then
+    # If resource is "*", get all resource kinds from API server via kubectl
+    # This gets all resource kinds (including CRDs) available
+    mapfile -t all_resources < <(kubectl --kubeconfig="$KUBECONFIG_FILE" api-resources -o name)
+    RESOURCES_FINAL+=("${all_resources[@]}")
+  else
+    RESOURCES_FINAL+=("$resource")
+  fi
+done
+
+# Remove duplicates from final resource list
+IFS=$'\n' RESOURCES=($(sort -u <<<"${RESOURCES_FINAL[*]}"))
+unset IFS
+
+if [[ ${#RESOURCES[@]} -eq 0 ]]; then
+  echo "Error: Could not retrieve resource list from ClusterRole"
+  exit 1
+fi
+
+echo
+echo "Resources to check:"
+printf '%s\n' "${RESOURCES[@]}"
+
+for verb in get list watch; do
+  echo
+  echo "🔍 Checking permission: $verb"
+  for res in "${RESOURCES[@]}"; do
+    echo -n "  Can $verb $res: "
+    if output="$(kubectl --kubeconfig="$KUBECONFIG_FILE" auth can-i "$verb" "$res" --all-namespaces 2>&1 | tr '\n' ' ')"; then
+      echo "$output ✅"
+    else
+      echo "$output ❌"
+    fi
+  done
+done
diff --git a/handlers/graph.go b/handlers/graph.go
index f701b49fc..23e84292a 100644
--- a/handlers/graph.go
+++ b/handlers/graph.go
@@ -63,9 +63,6 @@ func GraphNamespaces(
 	return func(w http.ResponseWriter, r *http.Request) {
 		defer handlePanic(r.Context(), w)
 
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.GraphLogName)
-
 		// TODO: getLayer and its downstream call chain has our logger in the request context now; it just needs to extract and use it (which it does not today)
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		graph.CheckError(err)
@@ -91,9 +88,6 @@ func GraphNode(
 	return func(w http.ResponseWriter, r *http.Request) {
 		defer handlePanic(r.Context(), w)
 
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.GraphLogName)
-
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		graph.CheckError(err)
 
diff --git a/handlers/istio_config.go b/handlers/istio_config.go
index 76206b758..d16534a07 100644
--- a/handlers/istio_config.go
+++ b/handlers/istio_config.go
@@ -32,8 +32,6 @@ func IstioConfigList(
 	grafana *grafana.Service,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.IstioConfigLogName)
 
 		params := mux.Vars(r)
 		namespace := params["namespace"]
@@ -118,8 +116,6 @@ func IstioConfigDetails(
 	grafana *grafana.Service,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.IstioConfigLogName)
 
 		params := mux.Vars(r)
 		namespace := params["namespace"]
@@ -235,8 +231,6 @@ func IstioConfigDelete(
 	grafana *grafana.Service,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.IstioConfigLogName)
 
 		params := mux.Vars(r)
 		namespace := params["namespace"]
@@ -286,8 +280,6 @@ func IstioConfigUpdate(
 	grafana *grafana.Service,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.IstioConfigLogName)
 
 		params := mux.Vars(r)
 		namespace := params["namespace"]
@@ -343,8 +335,6 @@ func IstioConfigCreate(
 	grafana *grafana.Service,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.IstioConfigLogName)
 
 		// Feels kinda replicated for multiple functions..
 		params := mux.Vars(r)
@@ -417,8 +407,6 @@ func IstioConfigPermissions(
 	grafana *grafana.Service,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.IstioConfigLogName)
 
 		// query params
 		params := r.URL.Query()
@@ -458,9 +446,6 @@ func IstioConfigValidationSummary(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		// note we put this in the "validations" group log name since it is more about validations that just istio config
-		r = log.AddGroupToLoggerInRequestContext(r, log.ValidationLogName)
 
 		params := r.URL.Query()
 		namespaces := params.Get("namespaces") // csl of namespaces
diff --git a/handlers/namespaces.go b/handlers/namespaces.go
index 604e7c26f..d446d8ee5 100644
--- a/handlers/namespaces.go
+++ b/handlers/namespaces.go
@@ -77,8 +77,6 @@ func NamespaceValidationSummary(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.ValidationLogName)
 
 		query := r.URL.Query()
 		vars := mux.Vars(r)
diff --git a/handlers/tracing.go b/handlers/tracing.go
index 97668df04..b5e1fe61b 100644
--- a/handlers/tracing.go
+++ b/handlers/tracing.go
@@ -16,7 +16,6 @@ import (
 	"github.com/kiali/kiali/grafana"
 	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
-	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/tracing"
@@ -61,8 +60,6 @@ func AppTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -101,8 +98,6 @@ func ServiceTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -137,8 +132,6 @@ func WorkloadTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -174,8 +167,6 @@ func ErrorTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -212,8 +203,6 @@ func TraceDetails(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -248,8 +237,6 @@ func AppSpans(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -288,8 +275,6 @@ func ServiceSpans(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
@@ -328,8 +313,6 @@ func WorkloadSpans(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		// prepare the logger in a context, and replace the request context with ours that has our logger in it
-		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
 
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
diff --git a/kiali.go b/kiali.go
index 623ac9d1d..40b4c8245 100644
--- a/kiali.go
+++ b/kiali.go
@@ -379,7 +379,6 @@ func newManager(ctx context.Context, conf *config.Config, logger *zerolog.Logger
 						&batchv1.CronJob{},
 						&batchv1.Job{},
 						&appsv1.Deployment{},
-						&corev1.Endpoints{},
 						&appsv1.ReplicaSet{},
 						&networkingv1.Gateway{},
 						&networkingv1.DestinationRule{},
diff --git a/kubernetes/filters.go b/kubernetes/filters.go
index 4cd6d9919..427c8be48 100644
--- a/kubernetes/filters.go
+++ b/kubernetes/filters.go
@@ -269,30 +269,6 @@ func FilterPeerAuthenticationsBySelector(workloadSelector string, peerauthentica
 	return filtered
 }
 
-// FilterPodsByEndpoints performs a second pass was selector may return too many data
-// This case happens when a "nil" selector (such as one of default/kubernetes service) is used
-func FilterPodsByEndpoints(endpoints *core_v1.Endpoints, unfiltered []core_v1.Pod) []core_v1.Pod {
-	var pods []core_v1.Pod
-	if endpoints == nil {
-		return pods
-	}
-	endpointPods := make(map[string]bool)
-	for _, subset := range endpoints.Subsets {
-		for _, address := range subset.Addresses {
-			if address.TargetRef != nil && address.TargetRef.Kind == "Pod" {
-				endpointPods[address.TargetRef.Name] = true
-			}
-		}
-	}
-
-	for _, pod := range unfiltered {
-		if _, ok := endpointPods[pod.Name]; ok {
-			pods = append(pods, pod)
-		}
-	}
-	return pods
-}
-
 func FilterPodsBySelector(selector labels.Selector, allPods []core_v1.Pod) []core_v1.Pod {
 	var pods []core_v1.Pod
 	for _, pod := range allPods {
diff --git a/kubernetes/types.go b/kubernetes/types.go
index b8f0daeed..66e2fc20b 100644
--- a/kubernetes/types.go
+++ b/kubernetes/types.go
@@ -44,7 +44,6 @@ const (
 	DaemonSetType             = "DaemonSet"
 	DeploymentType            = "Deployment"
 	DeploymentConfigType      = "DeploymentConfig"
-	EndpointsType             = "Endpoints"
 	JobType                   = "Job"
 	PodType                   = "Pod"
 	ReplicationControllerType = "ReplicationController"
@@ -90,7 +89,6 @@ var (
 	DaemonSets             = AppsGroupVersionV1.WithKind(DaemonSetType)
 	Deployments            = AppsGroupVersionV1.WithKind(DeploymentType)
 	DeploymentConfigs      = AppsOpenShiftGroupVersionV1.WithKind(DeploymentConfigType)
-	Endpoints              = CoreGroupVersionV1.WithKind(EndpointsType)
 	Jobs                   = BatchGroupVersionV1.WithKind(JobType)
 	Pods                   = CoreGroupVersionV1.WithKind(PodType)
 	ReplicationControllers = CoreGroupVersionV1.WithKind(ReplicationControllerType)
diff --git a/log/log.go b/log/log.go
index 2a04f4165..29ca72530 100644
--- a/log/log.go
+++ b/log/log.go
@@ -81,17 +81,10 @@ func setLogFormat(l zerolog.Logger) zerolog.Logger {
 // WithGroup is a simple convienence function that provides a zerolog logger that will log messages associated with a group name in the log context
 // Use this to obtain a logger that has only group name associated with it - there will be no associated data.
 func WithGroup(group string) *zerolog.Logger {
-	zl := log.With().Str("group", group).Logger()
+	zl := log.With().Str(GroupLogName, group).Logger()
 	return &zl
 }
 
-// AddGroup is a convienence function that adds to the logger context a group name.
-// Use this to retain the original logger context (and all its data) - you are simply adding a group to it.
-func AddGroup(zl *zerolog.Logger, group string) *zerolog.Logger {
-	newLogger := zl.With().Str("group", group).Logger()
-	return &newLogger
-}
-
 // FromRequest is a convienence wrapper around zerolog's FromRequest thus helping callers avoid having to explicitly import hlog
 func FromRequest(r *http.Request) *zerolog.Logger {
 	return hlog.FromRequest(r)
@@ -110,13 +103,6 @@ func ToContext(ctx context.Context, zl *zerolog.Logger) context.Context {
 	return zl.WithContext(ctx)
 }
 
-// AddGroupToLoggerInRequestContext will add a group log name to the logger found in the given request.
-// That logger will be added back into the request's context. The caller must use the returned request in lieu
-// of the given request in order to be able to use the new logger.
-func AddGroupToLoggerInRequestContext(r *http.Request, group string) *http.Request {
-	return r.WithContext(ToContext(r.Context(), AddGroup(FromContext(r.Context()), group)))
-}
-
 // Info logs a message via the global logger
 func Info(args ...interface{}) {
 	log.Info().Msgf("%s", args...)
diff --git a/log/log_names.go b/log/log_names.go
index 14cce8f29..083f26313 100644
--- a/log/log_names.go
+++ b/log/log_names.go
@@ -1,14 +1,28 @@
 package log
 
-// these are variables used to define logger group names or other names of structured data fields used when logging messages
+// these are variables used to define logger group names
 // see log.WithGroup for when these are typically used
+var (
+	AuthenticateLogName = "authenticate"
+	ClustersLogName     = "clusters"
+	ConfigLogName       = "config"
+	GraphLogName        = "graph"
+	IstioConfigLogName  = "istioConfig"
+	KialiCacheLogName   = "kialiCache"
+	MeshLogName         = "mesh"
+	MetricsLogName      = "metrics"
+	PromCacheLogName    = "promCache"
+	PrometheusLogName   = "prometheus"
+	ResourcesLogName    = "resources"
+	StatusLogName       = "status"
+	TracingLogName      = "tracing"
+	ValidationLogName   = "validation"
+)
+
+// these are structured attribute key names for log messages
 var (
 	GraphAppenderLogName = "appender"
-	GraphLogName         = "graph"
-	IstioConfigLogName   = "istioConfig"
-	KialiCacheLogName    = "kialiCache"
-	PromCacheLogName     = "promCache"
-	PrometheusLogName    = "prometheus"
-	TracingLogName       = "tracing"
-	ValidationLogName    = "validation"
+	GroupLogName         = "group"
+	RouteLogName         = "route"
+	RoutePatternLogName  = "route-pattern"
 )
diff --git a/models/address.go b/models/address.go
index 15be14125..8e86ae48f 100644
--- a/models/address.go
+++ b/models/address.go
@@ -1,28 +1,8 @@
 package models
 
-import core_v1 "k8s.io/api/core/v1"
-
 type Addresses []Address
 type Address struct {
 	Kind string `json:"kind"`
 	Name string `json:"name"`
 	IP   string `json:"ip"`
-	Port uint32 `json:"port"`
-}
-
-func (addresses *Addresses) Parse(as []core_v1.EndpointAddress) {
-	for _, address := range as {
-		castedAddress := Address{}
-		castedAddress.Parse(address)
-		*addresses = append(*addresses, castedAddress)
-	}
-}
-
-func (address *Address) Parse(a core_v1.EndpointAddress) {
-	address.IP = a.IP
-
-	if a.TargetRef != nil {
-		address.Kind = a.TargetRef.Kind
-		address.Name = a.TargetRef.Name
-	}
 }
diff --git a/models/endpoint.go b/models/endpoint.go
index 2fc72b1df..7b82b2d21 100644
--- a/models/endpoint.go
+++ b/models/endpoint.go
@@ -1,8 +1,6 @@
 package models
 
-import (
-	core_v1 "k8s.io/api/core/v1"
-)
+import core_v1 "k8s.io/api/core/v1"
 
 type Endpoints []Endpoint
 type Endpoint struct {
@@ -10,19 +8,23 @@ type Endpoint struct {
 	Ports     Ports     `json:"ports"`
 }
 
-func (endpoints *Endpoints) Parse(es *core_v1.Endpoints) {
-	if es == nil {
-		return
+// GetEndpointsFromPods gets IP addresses from Pods
+func GetEndpointsFromPods(pods []core_v1.Pod) *Endpoints {
+	endpointPodAddresses := Endpoints{}
+	for _, pod := range pods {
+		if pod.Status.PodIP != "" { // make sure Pod's IP address is not empty
+			ep := Endpoint{
+				Addresses: make(Addresses, 0),
+				Ports:     make(Ports, 0),
+			}
+			ep.Addresses = append(ep.Addresses, Address{
+				Kind: pod.Kind,
+				Name: pod.Name,
+				IP:   pod.Status.PodIP,
+			})
+			endpointPodAddresses = append(endpointPodAddresses, ep)
+		}
 	}
 
-	for _, subset := range es.Subsets {
-		endpoint := Endpoint{}
-		endpoint.Parse(subset)
-		*endpoints = append(*endpoints, endpoint)
-	}
-}
-
-func (endpoint *Endpoint) Parse(s core_v1.EndpointSubset) {
-	(&endpoint.Ports).ParseEndpointPorts(s.Ports)
-	(&endpoint.Addresses).Parse(s.Addresses)
+	return &endpointPodAddresses
 }
diff --git a/models/port.go b/models/port.go
index 98d2bd4c2..367b0b31b 100644
--- a/models/port.go
+++ b/models/port.go
@@ -8,10 +8,9 @@ import (
 
 type Ports []Port
 type Port struct {
-	Name        string  `json:"name"`
-	Protocol    string  `json:"protocol"`
-	AppProtocol *string `json:"appProtocol,omitempty"`
-	Port        int32   `json:"port"`
+	Name     string `json:"name"`
+	Protocol string `json:"protocol"`
+	Port     int32  `json:"port"`
 }
 
 func (ports *Ports) Parse(ps []core_v1.ServicePort) {
@@ -25,7 +24,6 @@ func (ports *Ports) Parse(ps []core_v1.ServicePort) {
 func (port *Port) Parse(p core_v1.ServicePort) {
 	port.Name = p.Name
 	port.Protocol = string(p.Protocol)
-	port.AppProtocol = p.AppProtocol
 	port.Port = p.Port
 }
 
diff --git a/models/service.go b/models/service.go
index 981de69fc..42db06433 100644
--- a/models/service.go
+++ b/models/service.go
@@ -212,10 +212,6 @@ func (s *ServiceDetails) SetService(cluster string, svc *core_v1.Service, conf *
 	s.Service.Parse(cluster, svc, conf)
 }
 
-func (s *ServiceDetails) SetEndpoints(eps *core_v1.Endpoints) {
-	(&s.Endpoints).Parse(eps)
-}
-
 func (s *ServiceDetails) SetPods(pods []core_v1.Pod) {
 	mPods := Pods{}
 	mPods.Parse(pods)
diff --git a/observability/tracing.go b/observability/tracing.go
index 1df76868a..4f74f655d 100644
--- a/observability/tracing.go
+++ b/observability/tracing.go
@@ -28,7 +28,8 @@ import (
 
 const (
 	// TracingService is the name of the kiali tracer service.
-	TracingService = "kiali"
+	TracingClusterTag = "istio.cluster_id"
+	TracingService    = "kiali"
 )
 
 const (
diff --git a/routing/router.go b/routing/router.go
index 2e4e91fb5..c4f2457ad 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -378,13 +378,18 @@ func (a alice) then(h http.Handler) http.Handler {
 
 func buildHttpHandlerLogger(route Route, handlerFunction http.Handler) http.Handler {
 
-	zlc := zerolog.With().Str("route", route.Name)
+	// prepare the request's logger
+	zlc := zerolog.With().Str(log.RouteLogName, route.Name)
 	if log.IsTrace() {
-		zlc = zlc.Str("route-pattern", route.Pattern)
+		zlc = zlc.Str(log.RoutePatternLogName, route.Pattern)
 	}
+	if route.LogGroupName != "" {
+		zlc = zlc.Str(log.GroupLogName, route.LogGroupName)
+	}
+	zl := zlc.Logger()
 
 	c := alice{}
-	c = c.append(hlog.NewHandler(zlc.Logger()))
+	c = c.append(hlog.NewHandler(zl))
 
 	// TODO: commenting out but leaving these here in case we want to look into including them at a future date
 	// c = c.append(hlog.HostHandler("host", true))
diff --git a/routing/routes.go b/routing/routes.go
index 7e2a5123c..44b45ce11 100644
--- a/routing/routes.go
+++ b/routing/routes.go
@@ -13,6 +13,7 @@ import (
 	"github.com/kiali/kiali/handlers/authentication"
 	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/tracing"
 )
@@ -20,6 +21,7 @@ import (
 // Route describes a single route
 type Route struct {
 	Name          string
+	LogGroupName  string
 	Method        string
 	Pattern       string
 	HandlerFunc   http.HandlerFunc
@@ -60,6 +62,7 @@ func NewRoutes(
 		//		200
 		{
 			"Healthz",
+			log.StatusLogName,
 			"GET",
 			"/healthz",
 			handlers.Healthz,
@@ -78,6 +81,7 @@ func NewRoutes(
 		//      200: statusInfo
 		{
 			"Root",
+			log.StatusLogName,
 			"GET",
 			"/api",
 			handlers.Root(conf, clientFactory, kialiCache, grafana),
@@ -100,6 +104,7 @@ func NewRoutes(
 		//      200: userSessionData
 		{
 			"Authenticate",
+			log.AuthenticateLogName,
 			"GET",
 			"/api/authenticate",
 			handlers.Authenticate(conf, authController),
@@ -119,6 +124,7 @@ func NewRoutes(
 		//      200: userSessionData
 		{
 			"OpenshiftCheckToken",
+			log.AuthenticateLogName,
 			"POST",
 			"/api/authenticate",
 			handlers.Authenticate(conf, authController),
@@ -134,6 +140,7 @@ func NewRoutes(
 		//      204: noContent
 		{
 			"Logout",
+			log.AuthenticateLogName,
 			"GET",
 			"/api/logout",
 			handlers.Logout(conf, authController),
@@ -157,6 +164,7 @@ func NewRoutes(
 		//      200: authenticationInfo
 		{
 			"AuthenticationInfo",
+			log.AuthenticateLogName,
 			"GET",
 			"/api/auth/info",
 			handlers.AuthenticationInfo(conf, authController, maps.Keys(clientFactory.GetSAClients())),
@@ -176,6 +184,7 @@ func NewRoutes(
 		//      200: statusInfo
 		{
 			"Status",
+			log.StatusLogName,
 			"GET",
 			"/api/status",
 			handlers.Root(conf, clientFactory, kialiCache, grafana),
@@ -195,6 +204,7 @@ func NewRoutes(
 		//      200: statusInfo
 		{
 			"Config",
+			log.ConfigLogName,
 			"GET",
 			"/api/config",
 			handlers.Config(conf, kialiCache, discovery, clientFactory, prom),
@@ -214,6 +224,7 @@ func NewRoutes(
 		//      200: statusInfo
 		{
 			"Crippled",
+			log.ConfigLogName,
 			"GET",
 			"/api/crippled",
 			handlers.CrippledFeatures(prom),
@@ -233,6 +244,7 @@ func NewRoutes(
 		//      200: istioConfigPermissions
 		{
 			"IstioConfigPermissions",
+			log.IstioConfigLogName,
 			"GET",
 			"/api/istio/permissions",
 			handlers.IstioConfigPermissions(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -253,6 +265,7 @@ func NewRoutes(
 		//
 		{
 			"IstioConfigList",
+			log.IstioConfigLogName,
 			"GET",
 			"/api/namespaces/{namespace}/istio",
 			handlers.IstioConfigList(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -273,6 +286,7 @@ func NewRoutes(
 		//
 		{
 			"IstioConfigListAll",
+			log.IstioConfigLogName,
 			"GET",
 			"/api/istio/config",
 			handlers.IstioConfigList(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -295,6 +309,7 @@ func NewRoutes(
 		//
 		{
 			"IstioConfigDetails",
+			log.IstioConfigLogName,
 			"GET",
 			"/api/namespaces/{namespace}/istio/{group}/{version}/{kind}/{object}",
 			handlers.IstioConfigDetails(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -316,6 +331,7 @@ func NewRoutes(
 		//
 		{
 			"IstioConfigDelete",
+			log.IstioConfigLogName,
 			"DELETE",
 			"/api/namespaces/{namespace}/istio/{group}/{version}/{kind}/{object}",
 			handlers.IstioConfigDelete(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -341,6 +357,7 @@ func NewRoutes(
 		//
 		{
 			"IstioConfigUpdate",
+			log.IstioConfigLogName,
 			"PATCH",
 			"/api/namespaces/{namespace}/istio/{group}/{version}/{kind}/{object}",
 			handlers.IstioConfigUpdate(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -364,6 +381,7 @@ func NewRoutes(
 		//
 		{
 			"IstioConfigCreate",
+			log.IstioConfigLogName,
 			"POST",
 			"/api/namespaces/{namespace}/istio/{group}/{version}/{kind}",
 			handlers.IstioConfigCreate(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -384,6 +402,7 @@ func NewRoutes(
 		//
 		{
 			"ClustersServices",
+			log.ClustersLogName,
 			"GET",
 			"/api/clusters/services",
 			handlers.ClustersServices,
@@ -405,6 +424,7 @@ func NewRoutes(
 		//
 		{
 			"ServiceDetails",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/services/{service}",
 			handlers.ServiceDetails,
@@ -430,6 +450,7 @@ func NewRoutes(
 		//
 		{
 			"ServiceUpdate",
+			log.ResourcesLogName,
 			"PATCH",
 			"/api/namespaces/{namespace}/services/{service}",
 			handlers.ServiceUpdate,
@@ -449,6 +470,7 @@ func NewRoutes(
 		//		200: spansResponse
 		{
 			"AppSpans",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}/spans",
 			handlers.AppSpans(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -468,6 +490,7 @@ func NewRoutes(
 		//		200: spansResponse
 		{
 			"WorkloadSpans",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/workloads/{workload}/spans",
 			handlers.WorkloadSpans(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -487,6 +510,7 @@ func NewRoutes(
 		//		200: spansResponse
 		{
 			"ServiceSpans",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/services/{service}/spans",
 			handlers.ServiceSpans(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -508,6 +532,7 @@ func NewRoutes(
 		//
 		{
 			"AppTraces",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}/traces",
 			handlers.AppTraces(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -529,6 +554,7 @@ func NewRoutes(
 		//
 		{
 			"ServiceTraces",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/services/{service}/traces",
 			handlers.ServiceTraces(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -550,6 +576,7 @@ func NewRoutes(
 		//
 		{
 			"WorkloadTraces",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/workloads/{workload}/traces",
 			handlers.WorkloadTraces(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -571,6 +598,7 @@ func NewRoutes(
 		//
 		{
 			"ErrorTraces",
+			log.TracingLogName,
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}/errortraces",
 			handlers.ErrorTraces(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -592,6 +620,7 @@ func NewRoutes(
 		//
 		{
 			"TracesDetails",
+			log.TracingLogName,
 			"GET",
 			"/api/traces/{traceID}",
 			handlers.TraceDetails(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -612,6 +641,7 @@ func NewRoutes(
 		//
 		{
 			"ClusterWorkloads",
+			log.ClustersLogName,
 			"GET",
 			"/api/clusters/workloads",
 			handlers.ClusterWorkloads(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -633,6 +663,7 @@ func NewRoutes(
 		//
 		{
 			"WorkloadDetails",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/workloads/{workload}",
 			handlers.WorkloadDetails(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -658,6 +689,7 @@ func NewRoutes(
 		//
 		{
 			"WorkloadUpdate",
+			log.ResourcesLogName,
 			"PATCH",
 			"/api/namespaces/{namespace}/workloads/{workload}",
 			handlers.WorkloadUpdate(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -678,6 +710,7 @@ func NewRoutes(
 		//
 		{
 			"ClustersApps",
+			log.ClustersLogName,
 			"GET",
 			"/api/clusters/apps",
 			handlers.ClusterApps(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -699,6 +732,7 @@ func NewRoutes(
 		//
 		{
 			"AppDetails",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}",
 			handlers.AppDetails(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -719,6 +753,7 @@ func NewRoutes(
 		//
 		{
 			"NamespaceList",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces",
 			handlers.NamespaceList(conf, kialiCache, clientFactory, discovery),
@@ -744,6 +779,7 @@ func NewRoutes(
 		//
 		{
 			"NamespaceUpdate",
+			log.ResourcesLogName,
 			"PATCH",
 			"/api/namespaces/{namespace}",
 			handlers.NamespaceUpdate(conf, kialiCache, clientFactory, discovery),
@@ -764,6 +800,7 @@ func NewRoutes(
 		//
 		{
 			"NamespaceInfo",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/info",
 			handlers.NamespaceInfo(conf, kialiCache, clientFactory, discovery),
@@ -785,6 +822,7 @@ func NewRoutes(
 		//
 		{
 			"ServiceMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/services/{service}/metrics",
 			handlers.ServiceMetrics,
@@ -806,6 +844,7 @@ func NewRoutes(
 		//
 		{
 			"AggregateMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/aggregates/{aggregate}/{aggregateValue}/metrics",
 			handlers.AggregateMetrics,
@@ -827,6 +866,7 @@ func NewRoutes(
 		//
 		{
 			"AppMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}/metrics",
 			handlers.AppMetrics,
@@ -848,6 +888,7 @@ func NewRoutes(
 		//
 		{
 			"WorkloadMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/workloads/{workload}/metrics",
 			handlers.WorkloadMetrics,
@@ -869,6 +910,7 @@ func NewRoutes(
 		//
 		{
 			"ControlPlaneMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/controlplanes/{controlplane}/metrics",
 			handlers.ControlPlaneMetrics(handlers.DefaultPromClientSupplier),
@@ -890,6 +932,7 @@ func NewRoutes(
 		//
 		{
 			"UsageMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/{app}/usage_metrics",
 			handlers.ResourceUsageMetrics(handlers.DefaultPromClientSupplier),
@@ -911,6 +954,7 @@ func NewRoutes(
 		//
 		{
 			"ServiceDashboard",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/services/{service}/dashboard",
 			handlers.ServiceDashboard(conf, grafana),
@@ -932,6 +976,7 @@ func NewRoutes(
 		//
 		{
 			"AppDashboard",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}/dashboard",
 			handlers.AppDashboard(conf, grafana),
@@ -953,6 +998,7 @@ func NewRoutes(
 		//
 		{
 			"WorkloadDashboard",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/workloads/{workload}/dashboard",
 			handlers.WorkloadDashboard(conf, grafana),
@@ -974,6 +1020,7 @@ func NewRoutes(
 		//
 		{
 			"ZtunnelDashboard",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/ztunnel/{workload}/dashboard",
 			handlers.ZtunnelDashboard(handlers.DefaultPromClientSupplier, conf, grafana),
@@ -995,6 +1042,7 @@ func NewRoutes(
 		//
 		{
 			"CustomDashboard",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/customdashboard/{dashboard}",
 			handlers.CustomDashboard(conf, grafana),
@@ -1016,6 +1064,7 @@ func NewRoutes(
 		//
 		{
 			"NamespaceMetrics",
+			log.MetricsLogName,
 			"GET",
 			"/api/namespaces/{namespace}/metrics",
 			handlers.NamespaceMetrics(handlers.DefaultPromClientSupplier),
@@ -1037,6 +1086,7 @@ func NewRoutes(
 		//
 		{
 			"ClustersHealth",
+			log.ClustersLogName,
 			"GET",
 			"/api/clusters/health",
 			handlers.ClustersHealth,
@@ -1058,6 +1108,7 @@ func NewRoutes(
 		//
 		{
 			"NamespaceValidationSummary",
+			log.ValidationLogName,
 			"GET",
 			"/api/namespaces/{namespace}/validations",
 			handlers.NamespaceValidationSummary(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1079,6 +1130,7 @@ func NewRoutes(
 		//
 		{
 			"ConfigValidationSummary",
+			log.ValidationLogName,
 			"GET",
 			"/api/istio/validations",
 			handlers.IstioConfigValidationSummary(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1100,6 +1152,7 @@ func NewRoutes(
 		//
 		{
 			"MeshTls",
+			log.StatusLogName,
 			"GET",
 			"/api/mesh/tls",
 			handlers.MeshTls(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1121,6 +1174,7 @@ func NewRoutes(
 		//
 		{
 			"NamespaceTls",
+			log.StatusLogName,
 			"GET",
 			"/api/namespaces/{namespace}/tls",
 			handlers.NamespaceTls(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1142,6 +1196,7 @@ func NewRoutes(
 		//
 		{
 			"ClusterTls",
+			log.ClustersLogName,
 			"GET",
 			"/api/clusters/tls",
 			handlers.ClustersTls(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1163,6 +1218,7 @@ func NewRoutes(
 		//
 		{
 			"IstioStatus",
+			log.StatusLogName,
 			"GET",
 			"/api/istio/status",
 			handlers.IstioStatus(conf, kialiCache, clientFactory, prom, traceClientLoader, discovery, cpm, grafana),
@@ -1184,6 +1240,7 @@ func NewRoutes(
 		//
 		{
 			"GraphNamespaces",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/graph",
 			handlers.GraphNamespaces(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1205,6 +1262,7 @@ func NewRoutes(
 		//
 		{
 			"GraphAggregate",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/{namespace}/aggregates/{aggregate}/{aggregateValue}/graph",
 			handlers.GraphNode(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1226,6 +1284,7 @@ func NewRoutes(
 		//
 		{
 			"GraphAggregateByService",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/{namespace}/aggregates/{aggregate}/{aggregateValue}/{service}/graph",
 			handlers.GraphNode(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1247,6 +1306,7 @@ func NewRoutes(
 		//
 		{
 			"GraphAppVersion",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/{namespace}/applications/{app}/versions/{version}/graph",
 			handlers.GraphNode(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1268,6 +1328,7 @@ func NewRoutes(
 		//
 		{
 			"GraphApp",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/{namespace}/applications/{app}/graph",
 			handlers.GraphNode(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1289,6 +1350,7 @@ func NewRoutes(
 		//
 		{
 			"GraphService",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/{namespace}/services/{service}/graph",
 			handlers.GraphNode(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1310,6 +1372,7 @@ func NewRoutes(
 		//
 		{
 			"GraphWorkload",
+			log.GraphLogName,
 			"GET",
 			"/api/namespaces/{namespace}/workloads/{workload}/graph",
 			handlers.GraphNode(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
@@ -1331,6 +1394,7 @@ func NewRoutes(
 		//
 		{
 			"MeshGraph",
+			log.GraphLogName,
 			"GET",
 			"/api/mesh/graph",
 			handlers.MeshGraph(conf, clientFactory, kialiCache, grafana, prom, traceClientLoader, discovery, cpm),
@@ -1352,6 +1416,7 @@ func NewRoutes(
 		//
 		{
 			"MeshControlPlanes",
+			log.MeshLogName,
 			"GET",
 			"/api/mesh/controlplanes",
 			handlers.ControlPlanes(kialiCache, clientFactory, conf, discovery),
@@ -1374,6 +1439,7 @@ func NewRoutes(
 		//
 		{
 			"GrafanaURL",
+			log.ConfigLogName,
 			"GET",
 			"/api/grafana",
 			handlers.GetGrafanaInfo(conf, grafana),
@@ -1395,6 +1461,7 @@ func NewRoutes(
 		//
 		{
 			"TracingURL",
+			log.ConfigLogName,
 			"GET",
 			"/api/tracing",
 			handlers.GetTracingInfo(conf),
@@ -1416,6 +1483,7 @@ func NewRoutes(
 		//
 		{
 			"PodDetails",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/pods/{pod}",
 			handlers.PodDetails(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -1437,6 +1505,7 @@ func NewRoutes(
 		//
 		{
 			"PodLogs",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/pods/{pod}/logs",
 			handlers.PodLogs(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -1458,6 +1527,7 @@ func NewRoutes(
 		//
 		{
 			"PodConfigDump",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/pods/{pod}/config_dump",
 			handlers.ConfigDump(conf, kialiCache, clientFactory, discovery),
@@ -1479,6 +1549,7 @@ func NewRoutes(
 		//
 		{
 			"PodProxyResource",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/pods/{pod}/config_dump/{resource}",
 			handlers.ConfigDumpResourceEntries(conf, kialiCache, clientFactory, discovery),
@@ -1500,6 +1571,7 @@ func NewRoutes(
 		//
 		{
 			"ZtunnelConfigDump",
+			log.ResourcesLogName,
 			"GET",
 			"/api/namespaces/{namespace}/pods/{pod}/config_dump_ztunnel",
 			handlers.ConfigDumpZtunnel(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -1522,6 +1594,7 @@ func NewRoutes(
 		//
 		{
 			"PodProxyLogging",
+			log.ResourcesLogName,
 			"POST",
 			"/api/namespaces/{namespace}/pods/{pod}/logging",
 			handlers.LoggingUpdate(conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery),
@@ -1543,6 +1616,7 @@ func NewRoutes(
 		//
 		{
 			"ClustersMetrics",
+			log.ClustersLogName,
 			"GET",
 			"/api/clusters/metrics",
 			handlers.ClustersMetrics(handlers.DefaultPromClientSupplier),
@@ -1564,6 +1638,7 @@ func NewRoutes(
 		//		200: metricsStatsResponse
 		{
 			Name:          "MetricsStats",
+			LogGroupName:  log.MetricsLogName,
 			Method:        "POST",
 			Pattern:       "/api/stats/metrics",
 			HandlerFunc:   handlers.MetricsStats,



Test patch:
diff --git a/kubernetes/filters_test.go b/kubernetes/filters_test.go
index 39168aee8..be8aa81bf 100644
--- a/kubernetes/filters_test.go
+++ b/kubernetes/filters_test.go
@@ -19,62 +19,6 @@ import (
 	"github.com/kiali/kiali/config"
 )
 
-func TestFilterPodsForEndpoints(t *testing.T) {
-	assert := assert.New(t)
-
-	endpoints := core_v1.Endpoints{
-		Subsets: []core_v1.EndpointSubset{
-			{
-				Addresses: []core_v1.EndpointAddress{
-					{
-						TargetRef: &core_v1.ObjectReference{
-							Name: "pod-1",
-							Kind: "Pod",
-						},
-					},
-					{
-						TargetRef: &core_v1.ObjectReference{
-							Name: "pod-2",
-							Kind: "Pod",
-						},
-					},
-					{
-						TargetRef: &core_v1.ObjectReference{
-							Name: "other",
-							Kind: "Other",
-						},
-					},
-					{},
-				},
-			},
-			{
-				Addresses: []core_v1.EndpointAddress{
-					{
-						TargetRef: &core_v1.ObjectReference{
-							Name: "pod-3",
-							Kind: "Pod",
-						},
-					},
-				},
-			},
-		},
-	}
-
-	pods := []core_v1.Pod{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "pod-1"}},
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "pod-2"}},
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "pod-3"}},
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "pod-999"}},
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "other"}},
-	}
-
-	filtered := FilterPodsByEndpoints(&endpoints, pods)
-	assert.Len(filtered, 3)
-	assert.Equal("pod-1", filtered[0].Name)
-	assert.Equal("pod-2", filtered[1].Name)
-	assert.Equal("pod-3", filtered[2].Name)
-}
-
 func TestFilterGateways(t *testing.T) {
 	assert := assert.New(t)
 
diff --git a/kubernetes/kubetest/mock_kubernetes.go b/kubernetes/kubetest/mock_kubernetes.go
index 804d93fa9..09d54e8cf 100644
--- a/kubernetes/kubetest/mock_kubernetes.go
+++ b/kubernetes/kubetest/mock_kubernetes.go
@@ -57,11 +57,6 @@ func (o *K8SClientMock) GetDeploymentsByLabel(namespace string, labelSelector st
 	return args.Get(0).([]apps_v1.Deployment), args.Error(1)
 }
 
-func (o *K8SClientMock) GetEndpoints(namespace string, name string) (*core_v1.Endpoints, error) {
-	args := o.Called(namespace, name)
-	return args.Get(0).(*core_v1.Endpoints), args.Error(1)
-}
-
 func (o *K8SClientMock) GetJobs(namespace string) ([]batch_v1.Job, error) {
 	args := o.Called(namespace)
 	return args.Get(0).([]batch_v1.Job), args.Error(1)
diff --git a/models/endpoints_test.go b/models/endpoints_test.go
new file mode 100644
index 000000000..382f4cf75
--- /dev/null
+++ b/models/endpoints_test.go
@@ -0,0 +1,105 @@
+package models
+
+import (
+	"encoding/json"
+	"testing"
+
+	core_v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetEndpointsFromPods(t *testing.T) {
+	cases := []struct {
+		name      string
+		inputPods []core_v1.Pod
+		expected  *Endpoints
+	}{
+		{
+			name:      "Nil input slice",
+			inputPods: nil,
+			expected:  &Endpoints{},
+		},
+		{
+			name:      "Empty input slice",
+			inputPods: []core_v1.Pod{},
+			expected:  &Endpoints{},
+		},
+		{
+			name: "Single pod with IP",
+			inputPods: []core_v1.Pod{
+				{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Pod",
+						APIVersion: "",
+					},
+					ObjectMeta: v1.ObjectMeta{Name: "pod1"},
+					Spec:       core_v1.PodSpec{},
+					Status:     core_v1.PodStatus{PodIP: "10.0.0.1"},
+				},
+			},
+			expected: &Endpoints{
+				{
+					Addresses: Addresses{{Kind: "Pod", Name: "pod1", IP: "10.0.0.1"}},
+					Ports:     make(Ports, 0),
+				},
+			},
+		},
+		{
+			name: "Multiple pods, some with IPs, some without",
+			inputPods: []core_v1.Pod{
+				{
+					TypeMeta: v1.TypeMeta{
+						Kind:       "Pod",
+						APIVersion: "",
+					},
+					ObjectMeta: v1.ObjectMeta{Name: "podA"},
+					Spec:       core_v1.PodSpec{},
+					Status:     core_v1.PodStatus{PodIP: "192.168.1.10"},
+				},
+				{
+					TypeMeta: v1.TypeMeta{
+						Kind:       "Pod",
+						APIVersion: "",
+					},
+					ObjectMeta: v1.ObjectMeta{Name: "podB"},
+					Spec:       core_v1.PodSpec{},
+					Status:     core_v1.PodStatus{PodIP: ""},
+				},
+				{
+					TypeMeta: v1.TypeMeta{
+						Kind:       "Pod",
+						APIVersion: "",
+					},
+					ObjectMeta: v1.ObjectMeta{Name: "podC"},
+					Spec:       core_v1.PodSpec{},
+					Status:     core_v1.PodStatus{PodIP: "192.168.1.12"},
+				},
+			},
+			expected: &Endpoints{
+				{
+					Addresses: Addresses{{Kind: "Pod", Name: "podA", IP: "192.168.1.10"}},
+					Ports:     make(Ports, 0),
+				},
+				{
+					Addresses: Addresses{{Kind: "Pod", Name: "podC", IP: "192.168.1.12"}},
+					Ports:     make(Ports, 0),
+				},
+			},
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			result := GetEndpointsFromPods(tc.inputPods)
+			require := require.New(t)
+			a, err := json.Marshal(tc.expected)
+			require.NoError(err)
+			b, err := json.Marshal(result)
+			require.NoError(err)
+
+			require.Equal(string(b), string(a))
+		})
+	}
+}
diff --git a/models/service_test.go b/models/service_test.go
index 8580ad994..bfb55781a 100644
--- a/models/service_test.go
+++ b/models/service_test.go
@@ -18,7 +18,6 @@ func TestServiceDetailParsing(t *testing.T) {
 
 	service := ServiceDetails{}
 	service.SetService(config.DefaultClusterID, fakeService(), config.Get())
-	service.SetEndpoints(fakeEndpoints())
 	service.SetPods(fakePods())
 	service.SetIstioSidecar(fakeWorkloads())
 
@@ -36,16 +35,6 @@ func TestServiceDetailParsing(t *testing.T) {
 	assert.Equal(service.Service.Ports, Ports{
 		Port{Name: "http", Protocol: "TCP", Port: 3001},
 		Port{Name: "http", Protocol: "TCP", Port: 3000}})
-	assert.Equal(service.Endpoints, Endpoints{
-		Endpoint{
-			Addresses: Addresses{
-				Address{Kind: "Pod", Name: "recommendation-v1", IP: "172.17.0.9"},
-				Address{Kind: "Pod", Name: "recommendation-v2", IP: "172.17.0.8"},
-			},
-			Ports: Ports{
-				Port{Name: "http", Protocol: "TCP", Port: 3001},
-				Port{Name: "http", Protocol: "TCP", Port: 3000},
-			}}})
 }
 
 func TestServiceParse(t *testing.T) {
@@ -99,28 +88,6 @@ func fakeService() *core_v1.Service {
 					Port:     3000}}}}
 }
 
-func fakeEndpoints() *core_v1.Endpoints {
-	return &core_v1.Endpoints{
-		Subsets: []core_v1.EndpointSubset{
-			{
-				Addresses: []core_v1.EndpointAddress{
-					{
-						IP: "172.17.0.9",
-						TargetRef: &core_v1.ObjectReference{
-							Kind: "Pod",
-							Name: "recommendation-v1"}},
-					{
-						IP: "172.17.0.8",
-						TargetRef: &core_v1.ObjectReference{
-							Kind: "Pod",
-							Name: "recommendation-v2"}},
-				},
-				Ports: []core_v1.EndpointPort{
-					{Name: "http", Protocol: "TCP", Port: 3001},
-					{Name: "http", Protocol: "TCP", Port: 3000},
-				}}}}
-}
-
 func fakePods() []core_v1.Pod {
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
 	t2, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:45 +0300")