Repository: kiali/kiali

Issue:
expose Kiali logs for debugging purposes in GH actions
### What do you want to improve?
Update the GitHub Actions workflow to expose (file or sdout?) Kiali logs for debugging purposes by following the official Kiali [debugging guide](https://kiali.io/docs/configuration/debugging-kiali/#logs)

### What is the current behavior?
no logs from the container are displayed during regression runs

### What is the new behavior?
- by default, log messages at `info` should be exposed 
- when you check the ✅ `enable debugging` option in GH actions, debug logs from the container should be added 

_noet: this could be also done by cypress itself (beforeEach hook)_

Discussion:
@jshaughn: > when you check the ✅ enable debugging option in GH actions, debug logs from the container should be added

Maybe it should be debug by default/trace with "enable debugging"?  Or would that maybe be too verbose/slow?@ScriptingShrimp: solved in https://github.com/kiali/kiali/pull/8508

> Maybe it should be debug by default/trace with "enable debugging"? Or would that maybe be too verbose/slow?

We can add "enable debugging" condition later if needed. Now the logs are only enabled for nightly.

Solution patch:
diff --git a/.github/workflows/integration-tests-frontend.yml b/.github/workflows/integration-tests-frontend.yml
index 6e2adf155..61686c175 100644
--- a/.github/workflows/integration-tests-frontend.yml
+++ b/.github/workflows/integration-tests-frontend.yml
@@ -13,6 +13,10 @@ on:
         required: false
         type: string
         default: ""
+      stern_logs:
+        required: false
+        type: boolean
+        default: false
 
 env:
   TARGET_BRANCH: ${{ inputs.target_branch }}
@@ -57,7 +61,7 @@ jobs:
       run: yarn install --frozen-lockfile
 
     - name: Run frontend integration tests
-      run: hack/run-integration-tests.sh --test-suite frontend $(if [ -n "${{ inputs.istio_version }}" ]; then echo "--istio-version ${{ inputs.istio_version }}"; fi)
+      run: hack/run-integration-tests.sh --test-suite frontend --stern ${{ inputs.stern_logs }} $(if [ -n "${{ inputs.istio_version }}" ]; then echo "--istio-version ${{ inputs.istio_version }}"; fi)
 
     - name: Get debug info when integration tests fail
       if: failure()
@@ -78,3 +82,11 @@ jobs:
       with:
         name: cypress-screenshots-${{ github.job }}
         path: frontend/cypress/screenshots
+
+    - name: Upload cypress logs from stern
+      uses: actions/upload-artifact@v4
+      if: ${{ inputs.stern_logs == true }}
+      with:
+        name: cypress-logs-${{ github.job }}
+        path: hack/stern
+
diff --git a/.github/workflows/molecules.yml b/.github/workflows/molecules.yml
index f5123a0c4..79e76b5cb 100644
--- a/.github/workflows/molecules.yml
+++ b/.github/workflows/molecules.yml
@@ -72,34 +72,51 @@ jobs:
           esac
         fi
 
-        LATEST_ISTIO_VERSIONS="$(curl -s https://api.github.com/repos/istio/istio/releases | jq -r '.[].tag_name' | sort -rV | awk -F'[-.]' '
-          {
-            minor = $1"."$2
-            is_rc = ($4 == "rc")
-            z = is_rc ? $3 : $3
-            rc_ver = is_rc ? $5 : ""
+        # Retry fetching Istio versions every 60 seconds for 2 hours (120 retries)
+        echo "Fetching Istio versions from GitHub API (with retry logic)..."
+        for retry in {1..120}; do
+          echo "Attempt $retry/120: Fetching Istio release data..."
+          CURL_RESULT=$(curl -s https://api.github.com/repos/istio/istio/releases)
+          if [ $? -eq 0 ] && [ -n "$CURL_RESULT" ] && echo "$CURL_RESULT" | jq -e '.[].tag_name' >/dev/null 2>&1; then
+            echo "Successfully fetched Istio release data."
+            LATEST_ISTIO_VERSIONS="$(echo "$CURL_RESULT" | jq -r '.[].tag_name' | sort -rV | awk -F'[-.]' '
+              {
+                minor = $1"."$2
+                is_rc = ($4 == "rc")
+                z = is_rc ? $3 : $3
+                rc_ver = is_rc ? $5 : ""
 
-            if (!minor_ga[minor] && !minor_rc[minor]) {
-              if (is_rc) { minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver }
-              else { minor_ga[minor] = $0; minor_ga_z[minor] = z }
-            } else if (!is_rc && minor_ga[minor]) {
-              if (z > minor_ga_z[minor]) { minor_ga[minor] = $0; minor_ga_z[minor] = z }
-            } else if (!is_rc && !minor_ga[minor]) {
-              minor_ga[minor] = $0; minor_ga_z[minor] = z; delete minor_rc[minor]; delete minor_rc_z[minor]; delete minor_rc_rc[minor]
-            } else if (is_rc && !minor_ga[minor]) {
-              current_rc_stored = minor_rc[minor]
-              split(current_rc_stored, current_rc_parts, "[-.]")
-              current_rc_num_stored = current_rc_parts[5]
+                if (!minor_ga[minor] && !minor_rc[minor]) {
+                  if (is_rc) { minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver }
+                  else { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+                } else if (!is_rc && minor_ga[minor]) {
+                  if (z > minor_ga_z[minor]) { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+                } else if (!is_rc && !minor_ga[minor]) {
+                  minor_ga[minor] = $0; minor_ga_z[minor] = z; delete minor_rc[minor]; delete minor_rc_z[minor]; delete minor_rc_rc[minor]
+                } else if (is_rc && !minor_ga[minor]) {
+                  current_rc_stored = minor_rc[minor]
+                  split(current_rc_stored, current_rc_parts, "[-.]")
+                  current_rc_num_stored = current_rc_parts[5]
 
-              if (!minor_rc[minor] || z > minor_rc_z[minor] || (z == minor_rc_z[minor] && rc_ver > current_rc_num_stored)) {
-                minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver
+                  if (!minor_rc[minor] || z > minor_rc_z[minor] || (z == minor_rc_z[minor] && rc_ver > current_rc_num_stored)) {
+                    minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver
+                  }
+                }
               }
-            }
-          }
-          END {
-            for (m in minor_ga) { print minor_ga[m] }
-            for (m in minor_rc) { print minor_rc[m] }
-          }' | sort -Vr | grep -v '^$' | head -n $((OFFSET + 1)))"
+              END {
+                for (m in minor_ga) { print minor_ga[m] }
+                for (m in minor_rc) { print minor_rc[m] }
+              }' | sort -Vr | grep -v '^$' | head -n $((OFFSET + 1)))"
+            break
+          else
+            if [ $retry -eq 120 ]; then
+              echo "ERROR: Failed to fetch Istio release data after 2 hours (120 retries). Giving up."
+              exit 1
+            fi
+            echo "Failed to fetch valid Istio release data. Retrying in 60 seconds..."
+            sleep 60
+          fi
+        done
 
         ISTIO_VERSION=$(echo "${LATEST_ISTIO_VERSIONS}" | tail -n 1)
         echo "The latest Istio versions are:"
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index b7481262a..9199edfcc 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -106,3 +106,13 @@ jobs:
         docker login -u ${{ secrets.QUAY_USER }} -p ${{ secrets.QUAY_PASSWORD }} quay.io
 
         make -e DOCKER_CLI_EXPERIMENTAL=enabled build-linux-multi-arch container-multi-arch-all-push-kiali-quay
+
+  integration_tests_frontend:
+    name: Run frontend integration tests
+    uses: ./.github/workflows/integration-tests-frontend.yml
+    needs: [initialize, build_backend]
+    with:
+      target_branch: ${{ needs.initialize.outputs.target-branch }}
+      build_branch: ${{ needs.initialize.outputs.build-branch }}
+      istio_version: ${{ needs.initialize.outputs.istio-version }}
+      stern_logs: true
diff --git a/.github/workflows/tag-creator.yml b/.github/workflows/tag-creator.yml
index cacbffdd7..654b66d43 100644
--- a/.github/workflows/tag-creator.yml
+++ b/.github/workflows/tag-creator.yml
@@ -90,5 +90,5 @@ jobs:
         sed -i -r 's/"version": (.*)/"version": "'${RELEASE_VERSION:1}'",/' frontend/package.json
 
         git add Makefile frontend/package.json
-        git commit -m "Release $RELEASE_VERSION"
-        git push origin && git push origin $(git rev-parse HEAD):refs/tags/$RELEASE_VERSION
+        git commit -m "Bump to version $RELEASE_VERSION"
+        git push origin && git push origin $(git rev-parse HEAD):refs/tags/$RELEASE_VERSION-rc
diff --git a/.github/workflows/test-istio-version.yml b/.github/workflows/test-istio-version.yml
index 0c8ac4ba0..ea0cf985e 100644
--- a/.github/workflows/test-istio-version.yml
+++ b/.github/workflows/test-istio-version.yml
@@ -49,35 +49,53 @@ jobs:
               *) echo "Invalid schedule or unknown trigger! Cannot determine Istio version." && exit 1 ;;
             esac
           fi
-          LATEST_ISTIO_VERSIONS="$(curl -s https://api.github.com/repos/istio/istio/releases | jq -r '.[].tag_name' | sort -rV | awk -F'[-.]' '
-            {
-              minor = $1"."$2
-              is_rc = ($4 == "rc")
-              z = is_rc ? $3 : $3
-              rc_ver = is_rc ? $5 : ""
+          # Retry fetching Istio versions every 60 seconds for 2 hours (120 retries)
+          echo "Fetching Istio versions from GitHub API (with retry logic)..."
+          for retry in {1..120}; do
+            echo "Attempt $retry/120: Fetching Istio release data..."
+            CURL_RESULT=$(curl -s https://api.github.com/repos/istio/istio/releases)
+            if [ $? -eq 0 ] && [ -n "$CURL_RESULT" ] && echo "$CURL_RESULT" | jq -e '.[].tag_name' >/dev/null 2>&1; then
+              echo "Successfully fetched Istio release data."
+              LATEST_ISTIO_VERSIONS="$(echo "$CURL_RESULT" | jq -r '.[].tag_name' | sort -rV | awk -F'[-.]' '
+                {
+                  minor = $1"."$2
+                  is_rc = ($4 == "rc")
+                  z = is_rc ? $3 : $3
+                  rc_ver = is_rc ? $5 : ""
 
-              if (!minor_ga[minor] && !minor_rc[minor]) {
-                if (is_rc) { minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver }
-                else { minor_ga[minor] = $0; minor_ga_z[minor] = z }
-              } else if (!is_rc && minor_ga[minor]) {
-                if (z > minor_ga_z[minor]) { minor_ga[minor] = $0; minor_ga_z[minor] = z }
-              } else if (!is_rc && !minor_ga[minor]) {
-                minor_ga[minor] = $0; minor_ga_z[minor] = z; delete minor_rc[minor]; delete minor_rc_z[minor]; delete minor_rc_rc[minor]
-              } else if (is_rc && !minor_ga[minor]) {
-                current_rc_stored = minor_rc[minor]
-                split(current_rc_stored, current_rc_parts, "[-.]")
-                current_rc_num_stored = current_rc_parts[5]
+                  if (!minor_ga[minor] && !minor_rc[minor]) {
+                    if (is_rc) { minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver }
+                    else { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+                  } else if (!is_rc && minor_ga[minor]) {
+                    if (z > minor_ga_z[minor]) { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+                  } else if (!is_rc && !minor_ga[minor]) {
+                    minor_ga[minor] = $0; minor_ga_z[minor] = z; delete minor_rc[minor]; delete minor_rc_z[minor]; delete minor_rc_rc[minor]
+                  } else if (is_rc && !minor_ga[minor]) {
+                    current_rc_stored = minor_rc[minor]
+                    split(current_rc_stored, current_rc_parts, "[-.]")
+                    current_rc_num_stored = current_rc_parts[5]
 
-                if (!minor_rc[minor] || z > minor_rc_z[minor] || (z == minor_rc_z[minor] && rc_ver > current_rc_num_stored)) {
-                  minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver
+                    if (!minor_rc[minor] || z > minor_rc_z[minor] || (z == minor_rc_z[minor] && rc_ver > current_rc_num_stored)) {
+                      minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver
+                    }
+                  }
                 }
-              }
-            }
-            END {
-              # The latest patch version is not always available in Sail. Use -latest as patch version
-              for (m in minor_ga) { split(minor_ga[m], version_parts, "."); minor_ga[m] = version_parts[1] "." version_parts[2] "-latest"; print minor_ga[m] }
-              for (m in minor_rc) { split(minor_rc[m], version_parts, "."); minor_rc[m] = version_parts[1] "." version_parts[2] "-latest"; print minor_rc[m] }
-            }' | sort -Vr | grep -v '^$' | head -n $((OFFSET + 1)))"
+                END {
+                  # The latest patch version is not always available in Sail. Use -latest as patch version
+                  for (m in minor_ga) { split(minor_ga[m], version_parts, "."); minor_ga[m] = version_parts[1] "." version_parts[2] "-latest"; print minor_ga[m] }
+                  for (m in minor_rc) { split(minor_rc[m], version_parts, "."); minor_rc[m] = version_parts[1] "." version_parts[2] "-latest"; print minor_rc[m] }
+                }' | sort -Vr | grep -v '^$' | head -n $((OFFSET + 1)))"
+              break
+            else
+              if [ $retry -eq 120 ]; then
+                echo "ERROR: Failed to fetch Istio release data after 2 hours (120 retries). Giving up."
+                exit 1
+              fi
+              echo "Failed to fetch valid Istio release data. Retrying in 60 seconds..."
+              sleep 60
+            fi
+          done
+
           ISTIO_VERSION=$(echo "${LATEST_ISTIO_VERSIONS}" | tail -n 1)
           echo "The latest Istio versions are:"
           echo "${LATEST_ISTIO_VERSIONS}"
diff --git a/.gitignore b/.gitignore
index 05ced53b6..584b71fb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,6 +90,8 @@ frontend/yarn-error.log*
 
 frontend/package-lock.json
 
+# Logging and logging binaries
+hack/stern/stern
 frontend/logs
 
 # Cypress
diff --git a/Makefile b/Makefile
index c2b6cf6f1..3847edc4e 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ TARGET_ARCHS ?= amd64 arm64 s390x ppc64le
 
 # Identifies the current build.
 # These will be embedded in the app and displayed when it starts.
-VERSION ?= v2.12.0-SNAPSHOT
+VERSION ?= v2.13.0-SNAPSHOT
 COMMIT_HASH ?= $(shell git rev-parse HEAD)
 
 # Version label is used in the OpenShift/K8S resources to identify
diff --git a/business/checkers/common/multi_match_selector_checker.go b/business/checkers/common/multi_match_selector_checker.go
index 597b2a84a..652bd280e 100644
--- a/business/checkers/common/multi_match_selector_checker.go
+++ b/business/checkers/common/multi_match_selector_checker.go
@@ -81,7 +81,7 @@ func SidecarSelectorMultiMatchChecker(cluster string, objectGVK schema.GroupVers
 	selectors := make(map[int]map[string]string, len(sc))
 	i := 0
 	for _, s := range sc {
-		for ns, _ := range workloadsPerNamespace {
+		for ns := range workloadsPerNamespace {
 			if s.Namespace != ns {
 				// Workloads from Sidecar's own Namespaces only are considered in Selector
 				continue
diff --git a/business/checkers/k8sgateways/multi_match_checker.go b/business/checkers/k8sgateways/multi_match_checker.go
index 5fd67bd52..25ff60b04 100644
--- a/business/checkers/k8sgateways/multi_match_checker.go
+++ b/business/checkers/k8sgateways/multi_match_checker.go
@@ -92,7 +92,7 @@ func (m MultiMatchChecker) findMatch(listener k8s_networking_v1.Listener, gwName
 }
 
 // Check duplicates IP
-func (m MultiMatchChecker) findMatchIP(address k8s_networking_v1.GatewayAddress, gwName string) (bool, []models.IstioValidationKey) {
+func (m MultiMatchChecker) findMatchIP(address k8s_networking_v1.GatewaySpecAddress, gwName string) (bool, []models.IstioValidationKey) {
 	collidingGateways := make([]models.IstioValidationKey, 0)
 
 	for _, aa := range m.K8sGateways {
@@ -101,7 +101,7 @@ func (m MultiMatchChecker) findMatchIP(address k8s_networking_v1.GatewayAddress,
 		}
 
 		for _, a := range aa.Spec.Addresses {
-			if a.Type != nil && address.Type != nil && *a.Type == *address.Type && a.Value == address.Value {
+			if a.Type != nil && address.Type != nil && a.Value != "" && address.Value != "" && *a.Type == *address.Type && a.Value == address.Value {
 				key := models.IstioValidationKey{Name: aa.Name, Namespace: aa.Namespace, ObjectGVK: kubernetes.K8sGateways}
 				collidingGateways = append(collidingGateways, key)
 			}
diff --git a/business/controlplane_monitor.go b/business/controlplane_monitor.go
index 1db806f07..44050949d 100644
--- a/business/controlplane_monitor.go
+++ b/business/controlplane_monitor.go
@@ -60,7 +60,7 @@ type controlPlaneMonitor struct {
 // and update the kialiCache. The proxy status and the registry services are
 // scraped from the debug endpoint.
 func (p *controlPlaneMonitor) RefreshIstioCache(ctx context.Context) error {
-	log.Debug("Scraping istiod for debug info")
+	log.Trace("Scraping istiod for debug info")
 	ctx, cancel := context.WithTimeout(ctx, p.pollingInterval)
 	defer cancel()
 
diff --git a/business/tracing.go b/business/tracing.go
index d1d67bb5a..344272f60 100644
--- a/business/tracing.go
+++ b/business/tracing.go
@@ -382,3 +382,56 @@ func tracesToSpans(ctx context.Context, app models.TracingName, r *model.Tracing
 func (in *TracingService) TracingDiagnose(ctx context.Context, token string) (trace *model.TracingDiagnose, err error) {
 	return tracing.TestNewClient(ctx, in.conf, token)
 }
+
+func (in *TracingService) ValidateConfiguration(ctx context.Context, conf *config.Config, tracingConfig *config.TracingConfig, token string) *model.ConfigurationValidation {
+
+	validation := model.ConfigurationValidation{}
+
+	// Merge config
+	if tracingConfig.Auth.Password == "xxx" {
+		tracingConfig.Auth.Password = conf.ExternalServices.Tracing.Auth.Password
+	}
+	if tracingConfig.Auth.Token == "xxx" {
+		tracingConfig.Auth.Token = conf.ExternalServices.Tracing.Auth.Token
+	}
+	if tracingConfig.Auth.CAFile == "xxx" {
+		tracingConfig.Auth.CAFile = conf.ExternalServices.Tracing.Auth.CAFile
+	}
+	if tracingConfig.Auth.Username == "xxx" {
+		tracingConfig.Auth.Username = conf.ExternalServices.Tracing.Auth.Username
+	}
+
+	newConfig := *conf
+	newConfig.ExternalServices.Tracing = *tracingConfig
+
+	// Try to create client
+	client, err := tracing.NewClient(ctx, &newConfig, token, false)
+	if client == nil {
+		msg := "ValidateConfiguration: Error creating tracing client"
+		if err != nil {
+			msg = fmt.Sprintf("%s: [%v]. ", msg, err)
+		}
+		log.FromContext(ctx).Trace().Msg(msg)
+		validation.Error = msg
+		return &validation
+	}
+
+	// Validate endpoint
+	status, err := client.GetServiceStatus(ctx)
+	log.FromContext(ctx).Trace().Msgf("GetServiceStatus %v", status)
+
+	if err != nil {
+		validation.Error = fmt.Sprintf("Error getting service status: [%v]. ", err)
+		return &validation
+	}
+
+	// Test any service
+	_, err = client.GetServices(ctx)
+	if err != nil {
+		validation.Error = fmt.Sprintf("Error getting services list: [%v]. ", err)
+		return &validation
+	}
+
+	validation.Message = "Success getting service status"
+	return &validation
+}
diff --git a/config/config.go b/config/config.go
index 05834b0ee..0f21cc45e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -206,13 +206,13 @@ type Server struct {
 
 // Auth provides authentication data for external services
 type Auth struct {
-	CAFile             string `yaml:"ca_file"`
-	InsecureSkipVerify bool   `yaml:"insecure_skip_verify"`
-	Password           string `yaml:"password"`
-	Token              string `yaml:"token"`
-	Type               string `yaml:"type"`
-	UseKialiToken      bool   `yaml:"use_kiali_token"`
-	Username           string `yaml:"username"`
+	CAFile             string `yaml:"ca_file" json:"caFile"`
+	InsecureSkipVerify bool   `yaml:"insecure_skip_verify" json:"insecureSkipVerify"`
+	Password           string `yaml:"password" json:"password"`
+	Token              string `yaml:"token" json:"token"`
+	Type               string `yaml:"type" json:"type"`
+	UseKialiToken      bool   `yaml:"use_kiali_token" json:"useKialiToken"`
+	Username           string `yaml:"username" json:"username"`
 }
 
 func (a *Auth) Obfuscate() {
@@ -224,52 +224,52 @@ func (a *Auth) Obfuscate() {
 
 // ThanosProxy describes configuration of the Thanos proxy component
 type ThanosProxy struct {
-	Enabled         bool   `yaml:"enabled,omitempty"`
-	RetentionPeriod string `yaml:"retention_period,omitempty"`
-	ScrapeInterval  string `yaml:"scrape_interval,omitempty"`
+	Enabled         bool   `yaml:"enabled,omitempty" json:"enabled,omitempty"`
+	RetentionPeriod string `yaml:"retention_period,omitempty" json:"retentionPeriod,omitempty"`
+	ScrapeInterval  string `yaml:"scrape_interval,omitempty" json:"scrapeInterval,omitempty"`
 }
 
 // PrometheusConfig describes configuration of the Prometheus component
 type PrometheusConfig struct {
-	Auth            Auth              `yaml:"auth,omitempty"`
-	CacheDuration   int               `yaml:"cache_duration,omitempty"`   // Cache duration per query expressed in seconds
-	CacheEnabled    bool              `yaml:"cache_enabled,omitempty"`    // Enable cache for Prometheus queries
-	CacheExpiration int               `yaml:"cache_expiration,omitempty"` // Global cache expiration expressed in seconds
-	CustomHeaders   map[string]string `yaml:"custom_headers,omitempty"`
-	HealthCheckUrl  string            `yaml:"health_check_url,omitempty"`
-	IsCore          bool              `yaml:"is_core,omitempty"`
-	QueryScope      map[string]string `yaml:"query_scope,omitempty"`
-	ThanosProxy     ThanosProxy       `yaml:"thanos_proxy,omitempty"`
-	URL             string            `yaml:"url,omitempty"`
+	Auth            Auth              `yaml:"auth,omitempty" json:"auth,omitempty"`
+	CacheDuration   int               `yaml:"cache_duration,omitempty" json:"cacheDuration,omitempty"`     // Cache duration per query expressed in seconds
+	CacheEnabled    bool              `yaml:"cache_enabled,omitempty" json:"cacheEnabled,omitempty"`       // Enable cache for Prometheus queries
+	CacheExpiration int               `yaml:"cache_expiration,omitempty" json:"cacheExpiration,omitempty"` // Global cache expiration expressed in seconds
+	CustomHeaders   map[string]string `yaml:"custom_headers,omitempty" json:"customHeaders,omitempty"`
+	HealthCheckUrl  string            `yaml:"health_check_url,omitempty" json:"healthCheckUrl,omitempty"`
+	IsCore          bool              `yaml:"is_core,omitempty" json:"isCore,omitempty"`
+	QueryScope      map[string]string `yaml:"query_scope,omitempty" json:"queryScope,omitempty"`
+	ThanosProxy     ThanosProxy       `yaml:"thanos_proxy,omitempty" json:"thanosProxy,omitempty"`
+	URL             string            `yaml:"url,omitempty" json:"url,omitempty"`
 }
 
 // CustomDashboardsConfig describes configuration specific to Custom Dashboards
 type CustomDashboardsConfig struct {
-	DiscoveryEnabled       string           `yaml:"discovery_enabled,omitempty"`
-	DiscoveryAutoThreshold int              `yaml:"discovery_auto_threshold,omitempty"`
-	Enabled                bool             `yaml:"enabled,omitempty"`
-	IsCore                 bool             `yaml:"is_core,omitempty"`
-	NamespaceLabel         string           `yaml:"namespace_label,omitempty"`
-	Prometheus             PrometheusConfig `yaml:"prometheus,omitempty"`
+	DiscoveryEnabled       string           `yaml:"discovery_enabled,omitempty" json:"discoveryEnabled,omitempty"`
+	DiscoveryAutoThreshold int              `yaml:"discovery_auto_threshold,omitempty" json:"discoveryAutoThreshold,omitempty"`
+	Enabled                bool             `yaml:"enabled,omitempty" json:"enabled,omitempty"`
+	IsCore                 bool             `yaml:"is_core,omitempty" json:"isCore,omitempty"`
+	NamespaceLabel         string           `yaml:"namespace_label,omitempty" json:"namespaceLabel,omitempty"`
+	Prometheus             PrometheusConfig `yaml:"prometheus,omitempty" json:"prometheus,omitempty"`
 }
 
 // GrafanaConfig describes configuration used for Grafana links
 type GrafanaConfig struct {
-	Auth           Auth                     `yaml:"auth"`
-	Dashboards     []GrafanaDashboardConfig `yaml:"dashboards"`
-	DatasourceUID  string                   `yaml:"datasource_uid,omitempty"`
-	Enabled        bool                     `yaml:"enabled"`      // Enable or disable Grafana support in Kiali
-	ExternalURL    string                   `yaml:"external_url"` // replaces the old url
-	HealthCheckUrl string                   `yaml:"health_check_url,omitempty"`
-	InternalURL    string                   `yaml:"internal_url"` // replaces the old in_cluster_url
-	IsCore         bool                     `yaml:"is_core,omitempty"`
+	Auth           Auth                     `yaml:"auth" json:"auth"`
+	Dashboards     []GrafanaDashboardConfig `yaml:"dashboards" json:"dashboards"`
+	DatasourceUID  string                   `yaml:"datasource_uid,omitempty" json:"datasourceUID,omitempty"`
+	Enabled        bool                     `yaml:"enabled" json:"enabled"`          // Enable or disable Grafana support in Kiali
+	ExternalURL    string                   `yaml:"external_url" json:"externalURL"` // replaces the old url
+	HealthCheckUrl string                   `yaml:"health_check_url,omitempty" json:"healthCheckUrl,omitempty"`
+	InternalURL    string                   `yaml:"internal_url" json:"internalURL"` // replaces the old in_cluster_url
+	IsCore         bool                     `yaml:"is_core,omitempty" json:"isCore,omitempty"`
 	XInClusterURL  string                   `yaml:"in_cluster_url,omitempty" json:"InClusterURL,omitempty"` // DEPRECATED!
 	XURL           string                   `yaml:"url,omitempty" json:"URL,omitempty"`                     // DEPRECATED!
 }
 
 type GrafanaDashboardConfig struct {
-	Name      string                 `yaml:"name"`
-	Variables GrafanaVariablesConfig `yaml:"variables"`
+	Name      string                 `yaml:"name" json:"name"`
+	Variables GrafanaVariablesConfig `yaml:"variables" json:"variables"`
 }
 
 type GrafanaVariablesConfig struct {
@@ -282,31 +282,31 @@ type GrafanaVariablesConfig struct {
 }
 
 type TempoConfig struct {
-	CacheCapacity int    `yaml:"cache_capacity" json:"cache_capacity,omitempty"`
-	CacheEnabled  bool   `yaml:"cache_enabled" json:"cache_enabled,omitempty"`
-	DatasourceUID string `yaml:"datasource_uid" json:"datasource_uid,omitempty"`
-	OrgID         string `yaml:"org_id" json:"org_id,omitempty"`
-	URLFormat     string `yaml:"url_format" json:"url_format,omitempty"`
+	CacheCapacity int    `yaml:"cache_capacity" json:"cacheCapacity,omitempty"`
+	CacheEnabled  bool   `yaml:"cache_enabled" json:"cacheEnabled,omitempty"`
+	DatasourceUID string `yaml:"datasource_uid" json:"datasourceUID,omitempty"`
+	OrgID         string `yaml:"org_id" json:"orgID,omitempty"`
+	URLFormat     string `yaml:"url_format" json:"urlFormat,omitempty"`
 }
 
 // TracingConfig describes configuration used for tracing links
 type TracingConfig struct {
-	Auth                 Auth              `yaml:"auth"`
-	CustomHeaders        map[string]string `yaml:"custom_headers,omitempty"`
-	DisableVersionCheck  bool              `yaml:"disable_version_check,omitempty"`
-	Enabled              bool              `yaml:"enabled"`      // Enable Tracing in Kiali
-	ExternalURL          string            `yaml:"external_url"` // replaces the old url
-	HealthCheckUrl       string            `yaml:"health_check_url,omitempty"`
-	GrpcPort             int               `yaml:"grpc_port,omitempty"`
-	InternalURL          string            `yaml:"internal_url"` // replaces the old in_cluster_url
-	IsCore               bool              `yaml:"is_core,omitempty"`
-	Provider             TracingProvider   `yaml:"provider,omitempty"` // jaeger | tempo
-	TempoConfig          TempoConfig       `yaml:"tempo_config,omitempty"`
-	NamespaceSelector    bool              `yaml:"namespace_selector"`
-	QueryScope           map[string]string `yaml:"query_scope,omitempty"`
-	QueryTimeout         int               `yaml:"query_timeout,omitempty"`
-	UseGRPC              bool              `yaml:"use_grpc"`
-	WhiteListIstioSystem []string          `yaml:"whitelist_istio_system"`
+	Auth                 Auth              `yaml:"auth" json:"auth"`
+	CustomHeaders        map[string]string `yaml:"custom_headers,omitempty" json:"customHeaders,omitempty"`
+	DisableVersionCheck  bool              `yaml:"disable_version_check,omitempty" json:"disableVersionCheck,omitempty"`
+	Enabled              bool              `yaml:"enabled" json:"enabled"`
+	ExternalURL          string            `yaml:"external_url" json:"externalURL"`
+	HealthCheckUrl       string            `yaml:"health_check_url,omitempty" json:"healthCheckUrl,omitempty"`
+	GrpcPort             int               `yaml:"grpc_port,omitempty" json:"grpcPort,omitempty"`
+	InternalURL          string            `yaml:"internal_url" json:"internalURL"`
+	IsCore               bool              `yaml:"is_core,omitempty" json:"isCore,omitempty"`
+	Provider             TracingProvider   `yaml:"provider,omitempty" json:"provider,omitempty"`
+	TempoConfig          TempoConfig       `yaml:"tempo_config,omitempty" json:"tempoConfig,omitempty"`
+	NamespaceSelector    bool              `yaml:"namespace_selector" json:"namespaceSelector"`
+	QueryScope           map[string]string `yaml:"query_scope,omitempty" json:"queryScope,omitempty"`
+	QueryTimeout         int               `yaml:"query_timeout,omitempty" json:"queryTimeout,omitempty"`
+	UseGRPC              bool              `yaml:"use_grpc" json:"useGRPC"`
+	WhiteListIstioSystem []string          `yaml:"whitelist_istio_system" json:"whiteListIstioSystem"`
 	XInClusterURL        string            `yaml:"in_cluster_url,omitempty" json:"InClusterURL,omitempty"` // DEPRECATED!
 	XURL                 string            `yaml:"url,omitempty" json:"URL,omitempty"`                     // DEPRECATED!
 }
@@ -314,47 +314,47 @@ type TracingConfig struct {
 // RegistryConfig contains configuration for connecting to an external istiod.
 // This is used when Kiali should connect to the istiod via a url instead of port forwarding.
 type RegistryConfig struct {
-	IstiodURL string `yaml:"istiod_url"`
+	IstiodURL string `yaml:"istiod_url" json:"istiodUrl"`
 	// TODO: Support auth options
 }
 
 // IstioConfig describes configuration used for istio links
 type IstioConfig struct {
-	ComponentStatuses                 ComponentStatuses `yaml:"component_status,omitempty"`
-	ConfigMapName                     string            `yaml:"config_map_name,omitempty"`
-	EnvoyAdminLocalPort               int               `yaml:"envoy_admin_local_port,omitempty"`
-	GatewayAPIClasses                 []GatewayAPIClass `yaml:"gateway_api_classes,omitempty"`
-	GatewayAPIClassesLabelSelector    string            `yaml:"gateway_api_classes_label_selector,omitempty"`
-	IstioAPIEnabled                   bool              `yaml:"istio_api_enabled"`
-	IstioIdentityDomain               string            `yaml:"istio_identity_domain,omitempty"`
-	IstioInjectionAnnotation          string            `yaml:"istio_injection_annotation,omitempty"`
-	IstioSidecarInjectorConfigMapName string            `yaml:"istio_sidecar_injector_config_map_name,omitempty"`
-	IstioSidecarAnnotation            string            `yaml:"istio_sidecar_annotation,omitempty"`
-	IstiodDeploymentName              string            `yaml:"istiod_deployment_name,omitempty"`
-	IstiodPodMonitoringPort           int               `yaml:"istiod_pod_monitoring_port,omitempty"`
+	ComponentStatuses                 ComponentStatuses `yaml:"component_status,omitempty" json:"componentStatuses,omitempty"`
+	ConfigMapName                     string            `yaml:"config_map_name,omitempty" json:"configMapName,omitempty"`
+	EnvoyAdminLocalPort               int               `yaml:"envoy_admin_local_port,omitempty" json:"envoyAdminLocalPort,omitempty"`
+	GatewayAPIClasses                 []GatewayAPIClass `yaml:"gateway_api_classes,omitempty" json:"gatewayApiClasses,omitempty"`
+	GatewayAPIClassesLabelSelector    string            `yaml:"gateway_api_classes_label_selector,omitempty" json:"gatewayApiClassesLabelSelector,omitempty"`
+	IstioAPIEnabled                   bool              `yaml:"istio_api_enabled" json:"istioApiEnabled"`
+	IstioIdentityDomain               string            `yaml:"istio_identity_domain,omitempty" json:"istioIdentityDomain,omitempty"`
+	IstioInjectionAnnotation          string            `yaml:"istio_injection_annotation,omitempty" json:"istioInjectionAnnotation,omitempty"`
+	IstioSidecarInjectorConfigMapName string            `yaml:"istio_sidecar_injector_config_map_name,omitempty" json:"istioSidecarInjectorConfigMapName,omitempty"`
+	IstioSidecarAnnotation            string            `yaml:"istio_sidecar_annotation,omitempty" json:"istioSidecarAnnotation,omitempty"`
+	IstiodDeploymentName              string            `yaml:"istiod_deployment_name,omitempty" json:"istiodDeploymentName,omitempty"`
+	IstiodPodMonitoringPort           int               `yaml:"istiod_pod_monitoring_port,omitempty" json:"istiodPodMonitoringPort,omitempty"`
 	// IstiodPollingIntervalSeconds is how often in seconds Kiali will poll istiod(s) for
 	// proxy status and registry services. Polling is not performed if IstioAPIEnabled is false.
-	IstiodPollingIntervalSeconds     int             `yaml:"istiod_polling_interval_seconds,omitempty"`
-	Registry                         *RegistryConfig `yaml:"registry,omitempty"`
-	RootNamespace                    string          `yaml:"root_namespace,omitempty"`
-	UrlServiceVersion                string          `yaml:"url_service_version"`
-	ValidationChangeDetectionEnabled bool            `yaml:"validation_change_detection_enabled,omitempty"`
+	IstiodPollingIntervalSeconds     int             `yaml:"istiod_polling_interval_seconds,omitempty" json:"istiodPollingIntervalSeconds,omitempty"`
+	Registry                         *RegistryConfig `yaml:"registry,omitempty" json:"registry,omitempty"`
+	RootNamespace                    string          `yaml:"root_namespace,omitempty" json:"rootNamespace,omitempty"`
+	UrlServiceVersion                string          `yaml:"url_service_version" json:"urlServiceVersion"`
+	ValidationChangeDetectionEnabled bool            `yaml:"validation_change_detection_enabled,omitempty" json:"validationChangeDetectionEnabled,omitempty"`
 	// ValidationReconcileInterval sets how often Kiali will validate Istio configuration.
 	// Validations can be disabled setting the interval to 0
-	ValidationReconcileInterval *time.Duration `yaml:"validation_reconcile_interval,omitempty"`
+	ValidationReconcileInterval *time.Duration `yaml:"validation_reconcile_interval,omitempty" json:"validationReconcileInterval,omitempty"`
 }
 
 type ComponentStatuses struct {
-	Enabled    bool              `yaml:"enabled,omitempty"`
-	Components []ComponentStatus `yaml:"components,omitempty"`
+	Enabled    bool              `yaml:"enabled,omitempty" json:"enabled,omitempty"`
+	Components []ComponentStatus `yaml:"components,omitempty" json:"components,omitempty"`
 }
 
 type ComponentStatus struct {
-	AppLabel       string `yaml:"app_label,omitempty"`
-	IsCore         bool   `yaml:"is_core,omitempty"`
-	IsProxy        bool   `yaml:"is_proxy,omitempty"`
-	IsMultiCluster bool   `yaml:"is_multicluster,omitempty"`
-	Namespace      string `yaml:"namespace,omitempty"`
+	AppLabel       string `yaml:"app_label,omitempty" json:"appLabel,omitempty"`
+	IsCore         bool   `yaml:"is_core,omitempty" json:"isCore,omitempty"`
+	IsProxy        bool   `yaml:"is_proxy,omitempty" json:"isProxy,omitempty"`
+	IsMultiCluster bool   `yaml:"is_multicluster,omitempty" json:"isMulticluster,omitempty"`
+	Namespace      string `yaml:"namespace,omitempty" json:"namespace,omitempty"`
 }
 
 type GatewayAPIClass struct {
@@ -602,6 +602,9 @@ type Aggregation struct {
 type MetricsDefaults struct {
 	Aggregations []Aggregation `yaml:"aggregations,omitempty" json:"aggregations,omitempty"`
 }
+type TracingDefaults struct {
+	Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
+}
 
 // UIDefaults defines default settings configured for the UI
 type UIDefaults struct {
@@ -614,6 +617,7 @@ type UIDefaults struct {
 	MetricsOutbound   MetricsDefaults `yaml:"metrics_outbound,omitempty" json:"metricsOutbound,omitempty"`
 	Namespaces        []string        `yaml:"namespaces,omitempty" json:"namespaces,omitempty"`
 	RefreshInterval   string          `yaml:"refresh_interval,omitempty" json:"refreshInterval,omitempty"`
+	Tracing           TracingDefaults `yaml:"tracing,omitempty" json:"tracing,omitempty"`
 }
 
 // Validations defines default settings configured for the Validations subsystem
@@ -926,6 +930,7 @@ func NewConfig() (c *Config) {
 				MetricsPerRefresh: "1m",
 				Namespaces:        make([]string, 0),
 				RefreshInterval:   "60s",
+				Tracing:           TracingDefaults{Limit: 100},
 			},
 			Validations: Validations{
 				Ignore: make([]string, 0),
@@ -1204,6 +1209,11 @@ func Unmarshal(yamlString string) (conf *Config, err error) {
 		log.Info("DEPRECATION NOTICE: 'external_services.tracing.url' has been deprecated - switch to 'external_services.tracing.external_url'")
 	}
 
+	// Validate tracing min and max values
+	if conf.KialiFeatureFlags.UIDefaults.Tracing.Limit < 10 || conf.KialiFeatureFlags.UIDefaults.Tracing.Limit > 1000 {
+		return nil, fmt.Errorf("KialiFeatureFlags.UIDefaults.Tracing.Limit should be between 10 and 1000")
+	}
+
 	// Some config settings (such as sensitive settings like passwords) are overrideable
 	// via secrets mounted on the file system rather than storing them directly in the config map itself.
 	// The names of the files in /kiali-override-secrets denote which credentials they are.
diff --git a/controller/validations.go b/controller/validations.go
index 6aa01d8e9..360667214 100644
--- a/controller/validations.go
+++ b/controller/validations.go
@@ -103,11 +103,11 @@ type ValidationsReconciler struct {
 
 // Reconcile fetches the VirtualService and prints its name
 func (r *ValidationsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	log.Debug("[ValidationsReconciler] Started reconciling ")
+	log.Trace("[ValidationsReconciler] Started reconciling ")
 	startTime := time.Now()
 	defer func() {
 		totalReconcileTime := time.Since(startTime)
-		log.Debugf("[ValidationsReconciler] Finished reconciling in %.2fs", totalReconcileTime.Seconds())
+		log.Tracef("[ValidationsReconciler] Finished reconciling in %.2fs", totalReconcileTime.Seconds())
 		if totalReconcileTime > r.reconcileInterval {
 			const warningLog = "[ValidationsReconciler] Reconcile took longer than the reconcile interval of [%s]. " +
 				"If this continues, validations will be increasingly stale. You can configure how often Kiali validates " +
diff --git a/frontend/cypress/README.md b/frontend/cypress/README.md
index 334794385..6576bce25 100644
--- a/frontend/cypress/README.md
+++ b/frontend/cypress/README.md
@@ -17,6 +17,7 @@ export CYPRESS_USERNAME=<value>                      # defaults to jenkins, opt.
 export CYPRESS_PASSWD=<value>                        # no defaults
 export CYPRESS_AUTH_PROVIDER=<value>                 # defaults to my_htpasswd_provider
 export CYPRESS_ALLOW_INSECURE_KIALI_API=<true|false> # Useful when running tests locally against an insecure endpoint like crc.
+export CYPRESS_STERN=<true|false>                    # defaults to false, set true for extended stern logging
 ```
 
 When running the Cypress tests for multi-cluster, both contexts for the kubectl/oc command must be specified. 
diff --git a/frontend/cypress/integration/common/hooks.ts b/frontend/cypress/integration/common/hooks.ts
index 9d63afc5c..e58efbaa3 100644
--- a/frontend/cypress/integration/common/hooks.ts
+++ b/frontend/cypress/integration/common/hooks.ts
@@ -101,7 +101,9 @@ Before({ tags: '@gateway-api' }, () => {
     if (result.code !== 0) {
       cy.log('Gateway API not found. Enabling it now.');
 
-      cy.exec('kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | kubectl apply -f -;')
+      cy.exec(
+        'kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | kubectl apply -f -;'
+      )
         .its('code')
         .should('eq', 0);
     }
@@ -169,32 +171,37 @@ metadata:
 `;
 
 Before({ tags: '@shared-mesh-config' }, () => {
-  cy.exec(`echo "${istioSharedMeshConfigMap}" | kubectl apply -f -`);
-  const patch = '{"spec": {"values": {"pilot": {"env": {"SHARED_MESH_CONFIG": "istio-user"}}}}}';
-  cy.exec(`kubectl patch istio default --type='merge' -p '${patch}'`).then(() => {
-    const maxTries = 10;
-    let tries = 0;
-    const doRequest = (): void => {
-      if (tries > maxTries) {
-        throw new Error('Timed out waiting for Kiali to see the Shared Mesh Config');
-      }
-
-      tries++;
-      cy.request({ method: 'GET', url: '/api/mesh/graph' }).then(response => {
-        expect(response.status).to.equal(200);
-        console.log(response.body.elements.nodes.find(node => node.data.infraType === 'istiod'));
-        if (
-          response.body.elements.nodes.find(node => node.data.infraType === 'istiod')?.data?.infraData?.config
-            ?.sharedConfig === undefined
-        ) {
-          cy.log(`Kiali has not seen the Shared Mesh Config yet. Tries: ${tries}. Waiting 3s...`);
-          cy.wait(3000);
-          doRequest();
+  cy.exec(`kubectl get pods -l app=istiod -n istio-system -o jsonpath="{.items[0].metadata.name}"`).then(result => {
+    const podName = result.stdout;
+
+    cy.exec(`echo "${istioSharedMeshConfigMap}" | kubectl apply -f -`);
+    const patch = '{"spec": {"values": {"pilot": {"env": {"SHARED_MESH_CONFIG": "istio-user"}}}}}';
+    cy.exec(`kubectl patch istio default --type='merge' -p '${patch}'`).then(() => {
+      const maxTries = 15;
+      let tries = 0;
+      const doRequest = (): void => {
+        if (tries > maxTries) {
+          throw new Error('Timed out waiting for Kiali to see the Shared Mesh Config');
         }
-      });
-    };
 
-    doRequest();
+        tries++;
+        cy.request({ method: 'GET', url: '/api/mesh/graph' }).then(response => {
+          expect(response.status).to.equal(200);
+          console.log(response.body.elements.nodes.find(node => node.data.infraType === 'istiod'));
+          if (
+            response.body.elements.nodes.find(node => node.data.infraType === 'istiod')?.data?.infraData?.config
+              ?.sharedConfig === undefined
+          ) {
+            cy.log(`Kiali has not seen the Shared Mesh Config yet. Tries: ${tries}. Waiting 3s...`);
+            cy.wait(3000);
+            doRequest();
+          } else {
+            cy.exec(`kubectl wait --for=delete pod/${podName} -n istio-system --timeout=60s`);
+          }
+        });
+      };
+      doRequest();
+    });
   });
 });
 
@@ -203,3 +210,24 @@ After({ tags: '@shared-mesh-config' }, () => {
   const patch = '{"spec": {"values": {"pilot": {"env": {"SHARED_MESH_CONFIG": null}}}}}';
   cy.exec(`kubectl patch istio default --type='merge' -p '${patch}'`);
 });
+
+Before(() => {
+  if (Cypress.env('STERN') === true) {
+    const specName = Cypress.spec && Cypress.spec.name ? Cypress.spec.name : 'unknown-spec';
+    cy.exec(`../hack/stern/run-stern.sh --logfile ${specName}.json`, { failOnNonZeroExit: false }).then(result => {
+      if (result.code !== 0) {
+        cy.log('Failed to start stern. Check if the stern binary is downloaded.');
+      }
+    });
+  }
+});
+
+After(() => {
+  if (Cypress.env('STERN') === true) {
+    cy.exec(`../hack/stern/run-stern.sh --stop true`, { failOnNonZeroExit: false }).then(result => {
+      if (result.code !== 0) {
+        cy.log('Failed to stop stern. Check if the stern binary is downloaded.');
+      }
+    });
+  }
+});
diff --git a/frontend/cypress/integration/common/masthead.ts b/frontend/cypress/integration/common/masthead.ts
index 44d840f50..e583e3c01 100644
--- a/frontend/cypress/integration/common/masthead.ts
+++ b/frontend/cypress/integration/common/masthead.ts
@@ -31,5 +31,5 @@ When(
 
 After({ tags: '@component-health-upscale' }, () => {
   cy.exec(`kubectl scale -n istio-system --replicas=1 deployment/grafana`);
-  cy.exec(`kubectl rollout status deployment prometheus -n istio-system`);
+  cy.exec(`kubectl rollout status deployment grafana -n istio-system`);
 });
diff --git a/frontend/cypress/integration/common/mesh.ts b/frontend/cypress/integration/common/mesh.ts
index 822bf30dd..621e67e64 100644
--- a/frontend/cypress/integration/common/mesh.ts
+++ b/frontend/cypress/integration/common/mesh.ts
@@ -115,16 +115,43 @@ Then('user sees cluster side panel', () => {
 Then('user sees control plane side panel', () => {
   cy.waitForReact();
   cy.get('#loading_kiali_spinner').should('not.exist');
-  cy.get('#target-panel-control-plane')
-    .should('be.visible')
-    .within(() => {
-      cy.contains('istiod');
-      cy.contains('Outbound policy').should('be.visible');
-      cy.get('div[data-test="memory-chart"]').should('exist');
-      cy.get('div[data-test="cpu-chart"]').should('exist');
-      cy.get('div[data-test="control-plane-certificate"]').should('exist');
-      cy.get('[data-test="label-TLS"]').contains('TLSV1_2');
-    });
+
+  // Wait for metrics
+  const maxTries = 15;
+  let tries = 0;
+  const waitForMemoryMetrics = (): void => {
+    if (tries > maxTries) {
+      throw new Error('Timed out waiting for Kiali to see the Shared Mesh Config');
+    }
+    tries++;
+    cy.request({ method: 'GET', url: '/api/namespaces/istio-system/controlplanes/istiod/metrics' }).then(
+      metricsResponse => {
+        expect(metricsResponse.status).to.equal(200);
+        cy.log(metricsResponse.body);
+        if (metricsResponse.body.process_resident_memory_bytes == null) {
+          cy.log(`Istiod hasn't load the Memory metrics yet. Tries: ${tries}. Waiting 3s...`);
+          cy.wait(3000);
+          waitForMemoryMetrics();
+        }
+      }
+    );
+  };
+  waitForMemoryMetrics();
+  it('control pannel should be visible', { retries: 3 }, () => {
+    cy.get('#refresh_button').click();
+    cy.get('#loading_kiali_spinner').should('not.exist');
+
+    cy.get('#target-panel-control-plane')
+      .should('be.visible')
+      .within(() => {
+        cy.contains('istiod');
+        cy.contains('Outbound policy').should('be.visible');
+        cy.get('div[data-test="memory-chart"]').should('exist');
+        cy.get('div[data-test="cpu-chart"]').should('exist');
+        cy.get('div[data-test="control-plane-certificate"]').should('exist');
+        cy.get('[data-test="label-TLS"]').contains('TLSV1_2');
+      });
+  });
 });
 
 Then('user sees data plane side panel', () => {
@@ -252,12 +279,16 @@ Then('user sees tracing node side panel', () => {
 
 Then('user sees {string} icon side panel', (iconType: string) => {
   cy.waitForReact();
-  cy.get('#target-panel-node').get(`[data-test="icon-${iconType}-validation"]`).should('be.visible');
+  it('icon should be visible', { retries: 3 }, () => {
+    cy.get('#target-panel-node').get(`[data-test="icon-${iconType}-validation"]`).should('be.visible');
+  });
 });
 
 Then('user does not see {string} icon side panel', (iconType: string) => {
   cy.waitForReact();
-  cy.get('#target-panel-node').get(`[data-test="icon-${iconType}-validation"]`).should('not.exist');
+  it('icon should be not visible', { retries: 3 }, () => {
+    cy.get('#target-panel-node').get(`[data-test="icon-${iconType}-validation"]`).should('not.exist');
+  });
 });
 
 Then('user sees {string} configuration tabs', (configTabs: string) => {
@@ -277,3 +308,90 @@ Then('user does not see {string} in the {string} configuration tab', (configOpt:
   cy.getBySel(`config-tab-${tab}`).click();
   cy.getBySel(`${tab}-config-editor`).should('not.contain', configOpt);
 });
+
+When('user opens the Trace Configuration modal', () => {
+  cy.waitForReact();
+  cy.contains('Configuration Tester').click();
+});
+
+Then('user sees the Trace Configuration modal', () => {
+  cy.get('.pf-v5-c-modal-box').should('be.visible');
+  cy.contains('Configuration Tester').should('be.visible');
+});
+
+Then('user sees the Discovery and Tester tabs', () => {
+  cy.get('.pf-v5-c-tabs__item').contains('Discovery').should('exist');
+  cy.get('.pf-v5-c-tabs__item').contains('Tester').should('exist');
+});
+
+Then('user sees the action buttons fixed at the bottom', () => {
+  cy.get('.pf-v5-c-modal-box__footer').should('be.visible');
+  cy.get('.pf-v5-c-modal-box__footer').within(() => {
+    cy.contains('Close').should('exist');
+  });
+});
+
+// Note: This is not checking values to be valid in jaeger/tempo setups
+Then('user verifies the Discovery information is correct', () => {
+  cy.get('#discover-spinner').should('not.exist');
+  cy.get('#discovery-tab-content').contains('Possible configuration(s) found');
+  cy.get('#discovery-tab-content').get('#valid-configurations').contains('Provider:');
+  cy.get('#discovery-tab-content').contains('Logs');
+  cy.get('#discovery-tab-content').get('#configuration-logs').contains('Parsed url');
+  cy.get('#discovery-tab-content').get('#configuration-logs').contains('Checking open ports');
+});
+
+When('user clicks the Rediscover button in the Discovery tab', () => {
+  cy.get('.pf-v5-c-modal-box').within(() => {
+    cy.contains('button', 'Rediscover').click();
+  });
+});
+
+When('user switches to the Tester tab', () => {
+  cy.get('div[data-test="modal-configuration-tester"]').within(() => {
+    cy.contains('button', 'Tester').click();
+  });
+});
+
+When('user changes the provider in the Tester tab', () => {
+  cy.get('div[data-test="modal-configuration-tester"]').within(() => {
+    cy.window().then((win: any) => {
+      const editor = win.ace.edit('ace-editor-tester');
+      const session = editor.getSession();
+      const linesCount = session.getLength();
+      const searchText = 'provider';
+      let replacer = 'tempo';
+      let provider = 'jaeger';
+
+      for (let i = 0; i < linesCount; i++) {
+        const line = session.getLine(i);
+        if (line.toString().toLowerCase().includes(searchText)) {
+          if (line.includes('tempo')) {
+            replacer = 'jaeger';
+            provider = 'tempo';
+          }
+          break;
+        }
+      }
+      let val: string = editor.getValue();
+      val = val.replace(`provider: ${provider}`, `provider: ${replacer}`);
+      editor.setValue(val);
+    });
+  });
+});
+
+When('user clicks the Test Configuration button', () => {
+  cy.get('div[data-test="modal-configuration-tester"]').within(() => {
+    cy.contains('Test Configuration').click();
+  });
+});
+
+Then('user sees the Tester result {string}', (result: string) => {
+  cy.get('div[data-test="modal-configuration-tester"]').within(() => {
+    if (result === 'incorrect') {
+      cy.get('span[data-test="icon-error-validation"]').should('exist');
+    } else {
+      cy.get('span[data-test="icon-correct-validation"]').should('exist');
+    }
+  });
+});
diff --git a/frontend/cypress/integration/common/workload_logs.ts b/frontend/cypress/integration/common/workload_logs.ts
index b6d0659db..0bc5c3bc2 100644
--- a/frontend/cypress/integration/common/workload_logs.ts
+++ b/frontend/cypress/integration/common/workload_logs.ts
@@ -96,6 +96,12 @@ Then('the log pane should only show log lines not containing {string}', (filterT
     });
 });
 
+Then('the log pane should only show json log lines', () => {
+  cy.get('#logsText').within(() => {
+    cy.get('button').find('svg.pf-v5-svg').should('exist');
+  });
+});
+
 Then('the log pane should show log lines containing {string}', (filterText: string) => {
   cy.get('#logsText')
     .find('p')
@@ -144,3 +150,29 @@ Then('the log pane should show spans', () => {
       cy.get('#logsText').find('p').should('have.css', 'color', spansColor);
     });
 });
+
+Then('I click a json log line', () => {
+  cy.get('#logsText').within(() => {
+    cy.get('button').find('svg.pf-v5-svg').should('exist');
+    cy.get('button').first().click();
+  });
+});
+
+Then('I click on the parsed json tab', () => {
+  cy.get('[data-test="json-modal"]')
+    .should('be.visible')
+    .within(() => {
+      cy.get('[data-test="json-table-tab"]').click();
+    });
+});
+
+Then('I should see certain values on the parsed object', () => {
+  cy.get('[data-test="parsed-json-table"]').within(() => {
+    cy.get('tr')
+      .first()
+      .within(() => {
+        cy.get('td').eq(0).should('contain.text', 'a');
+        cy.get('td').eq(1).should('contain.text', 'b');
+      });
+  });
+});
diff --git a/frontend/cypress/integration/featureFiles/istio_config.feature b/frontend/cypress/integration/featureFiles/istio_config.feature
index 1680a28c8..6414a5b22 100644
--- a/frontend/cypress/integration/featureFiles/istio_config.feature
+++ b/frontend/cypress/integration/featureFiles/istio_config.feature
@@ -13,7 +13,6 @@ Feature: Kiali Istio Config page
     And user selects the "bookinfo" namespace
 
   @bookinfo-app
-  @selected
   Scenario: See all Istio Config objects in the bookinfo namespace.
     Then user sees all the Istio Config objects in the bookinfo namespace
     And the "Cluster" column "disappears"
@@ -44,7 +43,6 @@ Feature: Kiali Istio Config page
     Then only "Gateway" objects are visible in the "bookinfo" namespace
 
   @bookinfo-app
-  @selected
   Scenario: Filter Istio Config objects by Valid configuration
     When the user filters by "Config" for "Valid"
     Then user sees "bookinfo-gateway"
diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature
index 538e665dc..6ab6c3b4d 100644
--- a/frontend/cypress/integration/featureFiles/mesh.feature
+++ b/frontend/cypress/integration/featureFiles/mesh.feature
@@ -124,3 +124,20 @@ Feature: Kiali Mesh page
     And user sees "mode: REGISTRY_ONLY" in the "effective" configuration tab
     And user sees "mode: REGISTRY_ONLY" in the "shared" configuration tab
     And user does not see "mode: REGISTRY_ONLY" in the "standard" configuration tab
+
+  Scenario: User opens and interacts with the Trace Configuration modal
+    When user selects tracing mesh node
+    And user opens the Trace Configuration modal
+    Then user sees the Trace Configuration modal
+    And user sees the Discovery and Tester tabs
+    And user sees the action buttons fixed at the bottom
+    And user verifies the Discovery information is correct
+    When user clicks the Rediscover button in the Discovery tab
+    And user verifies the Discovery information is correct
+    When user switches to the Tester tab
+    And user changes the provider in the Tester tab
+    And user clicks the Test Configuration button
+    Then user sees the Tester result "incorrect"
+    And user changes the provider in the Tester tab
+    And user clicks the Test Configuration button
+    Then user sees the Tester result "correct"
diff --git a/frontend/cypress/integration/featureFiles/wizard_istio_config.feature b/frontend/cypress/integration/featureFiles/wizard_istio_config.feature
index 8c6d6c113..bd9946e23 100644
--- a/frontend/cypress/integration/featureFiles/wizard_istio_config.feature
+++ b/frontend/cypress/integration/featureFiles/wizard_istio_config.feature
@@ -300,7 +300,6 @@ Feature: Kiali Istio Config wizard
 
   @gateway-api
   @bookinfo-app
-  @selected
   Scenario: Create multiple K8s Gateways with colliding hostnames and port combinations and check for a reference. Then delete one of them and the reference should be gone.
     When user deletes k8sgateway named "gatewayapi-1" and the resource is no longer available
     And user deletes k8sgateway named "gatewayapi-2" and the resource is no longer available
diff --git a/frontend/cypress/integration/featureFiles/workload_logs.feature b/frontend/cypress/integration/featureFiles/workload_logs.feature
index 7c46c0e8b..4cde31bc0 100644
--- a/frontend/cypress/integration/featureFiles/workload_logs.feature
+++ b/frontend/cypress/integration/featureFiles/workload_logs.feature
@@ -33,6 +33,20 @@ Feature: Workload logs tab
     When I type "GET" on the Hide text field
     Then the log pane should only show log lines not containing "GET"
 
+  @loggers-app
+  Scenario: The log pane of the logs tab should show json log lines with a json log indicator
+    Given I am on the logs tab of the "json-logger" workload detail page of the "loggers" namespace
+    When I type "text log format" on the Hide text field
+    Then the log pane should only show json log lines
+  
+  @loggers-app
+  Scenario: The json log should contain certain values on the parsed object
+    Given I am on the logs tab of the "json-logger" workload detail page of the "loggers" namespace
+    When I type "text log format" on the Hide text field
+    Then I click a json log line
+    And I click on the parsed json tab
+    And I should see certain values on the parsed object
+
   @bookinfo-app
   Scenario: The log pane of the logs tab should limit the number of log lines that are fetched
     Given I am on the logs tab of the "ratings-v1" workload detail page of the "bookinfo" namespace
diff --git a/frontend/cypress/integration/featureFiles/workloads_details _graph.feature b/frontend/cypress/integration/featureFiles/workloads_details_graph.feature
similarity index 100%
rename from frontend/cypress/integration/featureFiles/workloads_details _graph.feature
rename to frontend/cypress/integration/featureFiles/workloads_details_graph.feature
diff --git a/frontend/package.json b/frontend/package.json
index 68043713c..710dd98d2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@kiali/kiali-ui",
-  "version": "2.12.0",
+  "version": "2.13.0",
   "description": "React UI for [Kiali](https://github.com/kiali/kiali).",
   "keywords": [
     "istio service mesh",
diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json
index 8f0d83faa..f215da6fc 100644
--- a/frontend/public/locales/es/translation.json
+++ b/frontend/public/locales/es/translation.json
@@ -68,7 +68,7 @@
   "Builds an Authorization Policy based on Rules": "Builds an Authorization Policy based on Rules",
   "CA Certificates": "CA Certificates",
   "Cancel": "Cancel",
-  "Check Status": "Check Status",
+  "Changes here are not saved to the Kiali CR. If happy with changes, update the Kiali CR using your standard change workflow.": "Changes here are not saved to the Kiali CR. If happy with changes, update the Kiali CR using your standard change workflow.",
   "Check the usual ports for the tracing service and provide a subset of the tracing configuration based on the tracing services found for external_services.tracing.": "Check the usual ports for the tracing service and provide a subset of the tracing configuration based on the tracing services found for external_services.tracing.",
   "Choose the traffic rates used to generate the graph. Each supported protocol offers one or more options. Unused protocols can be omitted.": "Choose the traffic rates used to generate the graph. Each supported protocol offers one or more options. Unused protocols can be omitted.",
   "Circuit Breaker": "Circuit Breaker",
@@ -94,6 +94,9 @@
   "Condition Key not supported": "Condition Key not supported",
   "Condition List": "Condition List",
   "config": "config",
+  "Configuration Discovery": "Configuration Discovery",
+  "Configuration returned valid results": "Configuration returned valid results",
+  "Configuration Tester": "Configuration Tester",
   "Configuration:": "Configuration:",
   "Confirm": "Confirm",
   "Confirm Delete Traffic Routing ?": "Confirm Delete Traffic Routing ?",
@@ -138,6 +141,7 @@
   "Direction type is not valid.": "Direction type is not valid.",
   "Disable Auto Injection": "Disable Auto Injection",
   "Disabled": "Disabled",
+  "Discovery": "Discovery",
   "Display": "Display",
   "Display envoy config for the selected pod": "Display envoy config for the selected pod",
   "Display idle nodes": "Display idle nodes",
@@ -152,6 +156,7 @@
   "Duration": "Duration",
   "Edges": "Edges",
   "Edit": "Edit",
+  "Edit and test configuration changes without having to modify the CR.": "Edit and test configuration changes without having to modify the CR.",
   "Edit Annotations": "Edit Annotations",
   "Either all namespaces are being filtered or the user has no permission to access namespaces.": "Either all namespaces are being filtered or the user has no permission to access namespaces.",
   "Empty Graph": "Empty Graph",
@@ -173,6 +178,7 @@
   "Every 5m": "Cada 5m",
   "Expand view": "Vista expandida",
   "Extension": "Extension",
+  "external_services.tracing configuration:": "external_services.tracing configuration:",
   "Failed Request": "Failed Request",
   "Failure": "Failure",
   "Fault Injection": "Fault Injection",
@@ -184,6 +190,7 @@
   "Filter by Namespace Label": "Filtrar por la etiqueta del espacio de nombres",
   "Find and Hide": "Find and Hide",
   "Fixed Delay": "Fixed Delay",
+  "For Performance issues troubleshooting, remove sensitive data before posting:": "For Performance issues troubleshooting, remove sensitive data before posting:",
   "FQDN": "FQDN",
   "From": "From",
   "From List": "From List",
@@ -225,6 +232,8 @@
   "Istio control plane": "Istio control plane",
   "Istio deployment status disabled.": "Istio deployment status disabled.",
   "Istio revision {{version}}": "Istio revision {{version}}",
+  "JSON": "JSON",
+  "JSON Log Entry": "JSON Log Entry",
   "K8s Gateway": "K8s Gateway",
   "K8s Gateway API is not enabled": "K8s Gateway API is not enabled",
   "K8s Gateways": "K8s Gateways",
@@ -251,6 +260,7 @@
   "Loading...": "Loading...",
   "Login to {{cluster}}": "Login to {{cluster}}",
   "Logout": "Logout",
+  "Logs": "Logs",
   "Manual": "Manual",
   "Manual refresh required": "Manual refresh required",
   "Match 'Any request' is defined in a previous rule.": "Match 'Any request' is defined in a previous rule.",
@@ -346,6 +356,7 @@
   "Per Try Timeout": "Per Try Timeout",
   "Percentage of requests on which the delay will be injected.": "Percentage of requests on which the delay will be injected.",
   "Percentage of requests to be aborted with the error code provided.": "Percentage of requests to be aborted with the error code provided.",
+  "Performance Measurements": "Performance Measurements",
   "Please include this information when opening a bug:": "Please include this information when opening a bug:",
   "Pod Name": "Pod Name",
   "Pod Status": "Pod Status",
@@ -359,6 +370,7 @@
   "Private Key must be non empty": "Private Key must be non empty",
   "Protocol": "Protocol",
   "Proxy sync time": "Proxy sync time",
+  "Rediscover": "Rediscover",
   "Refresh": "Refresh",
   "Refresh interval": "Refresh interval",
   "Refresh interval. \"Pause\" refreshes on page change or Refresh button click. \"Manual\" refreshes only on Refresh button click.": "Refresh interval. \"Pause\" refreshes on page change or Refresh button click. \"Manual\" refreshes only on Refresh button click.",
@@ -418,12 +430,16 @@
   "Switch language": "Cambiar idioma",
   "Switch to {{theme}} Mode": "Cambiar a modo {{theme}}",
   "System Error": "System Error",
+  "Table": "Table",
   "Tag: {{tag}}": "Tag: {{tag}}",
   "TCP Connection": "TCP Connection",
   "TCP Traffic": "TCP Traffic",
   "TCP Traffic Shifting": "TCP Traffic Shifting",
+  "Test Configuration": "Test Configuration",
+  "Tester": "Tester",
   "The control plane must sync each proxy when there is a configuration change. This is the average time spent, per proxy,                  to perform the update. N/A means there was no syncing performed for the configured time period.": "The control plane must sync each proxy when there is a configuration change. This is the average time spent, per proxy,                  to perform the update. N/A means there was no syncing performed for the configured time period.",
   "The destination hosts to which traffic is being sent. Enter one or multiple hosts separated by comma.": "The destination hosts to which traffic is being sent. Enter one or multiple hosts separated by comma.",
+  "The JSON Log entry has been copied to your clipboard.": "The JSON Log entry has been copied to your clipboard.",
   "The minimum TLS version for connections among Istio workloads. Defaults to TLSV1_2 if not set. Set by the 'meshConfig.meshMTLS.minProtocolVersion' field.": "The minimum TLS version for connections among Istio workloads. Defaults to TLSV1_2 if not set. Set by the 'meshConfig.meshMTLS.minProtocolVersion' field.",
   "The refresh interval is set to \"Manual\". To render the graph, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.": "The refresh interval is set to \"Manual\". To render the graph, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.",
   "The refresh interval is set to \"Manual\". To render the list, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.": "The refresh interval is set to \"Manual\". To render the list, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.",
@@ -452,7 +468,6 @@
   "To": "To",
   "To List": "To List",
   "To read the FAQ entry about why your graph may be empty, ": "To read the FAQ entry about why your graph may be empty, ",
-  "Tracing Services Check Logs": "Tracing Services Check Logs",
   "Traffic": "Tráfico",
   "Traffic Animation": "Traffic Animation",
   "Traffic Graph": "Tráfico",
@@ -480,12 +495,10 @@
   "Value": "Valor",
   "Values": "Values",
   "Values and NotValues cannot be empty": "Values and NotValues cannot be empty",
-  "Verifying...": "Verifying...",
   "version: {{version}}": "version: {{version}}",
   "Version: {{version}}": "Versión: {{version}}",
   "View Annotations": "View Annotations",
   "View in Tracing": "View in Tracing",
-  "View logs": "View logs",
   "View on Graph": "View on Graph",
   "Virtual Service / Request Routing": "Virtual Service / Request Routing",
   "VirtualService Host '*' wildcard not allowed on mesh gateway.": "VirtualService Host '*' wildcard not allowed on mesh gateway.",
@@ -501,7 +514,7 @@
   "When enabled in an Ambient environment, include waypoints in the mesh topology.": "When enabled in an Ambient environment, include waypoints in the mesh topology.",
   "When enabled, include gateways in the mesh topology.": "When enabled, include gateways in the mesh topology.",
   "When List": "When List",
-  "While the health check is based on whether the URL response returns an HTTP 200, the services check performs a more exhaustive verification by attempting to analyze if the traces response is correct. It is important that internal_url is defined, as it relies on this host to perform the checks. When in_cluster config is false, it will use the external_url": "While the health check is based on whether the URL response returns an HTTP 200, the services check performs a more exhaustive verification by attempting to analyze if the traces response is correct. It is important that internal_url is defined, as it relies on this host to perform the checks. When in_cluster config is false, it will use the external_url",
+  "While the health check is simple and checks only for an HTTP 200 response for the URL, this check is more comprehensive and attempts to validate the returned trace data. It will typically use the internal_url to perform the checks. If Kiali is running outside of the cluster, the external_url will be used.": "While the health check is simple and checks only for an HTTP 200 response for the URL, this check is more comprehensive and attempts to validate the returned trace data. It will typically use the internal_url to perform the checks. If Kiali is running outside of the cluster, the external_url will be used.",
   "Workload": "Carga de trabajo",
   "Workload Entry": "Workload Entry",
   "Workload Selector": "Workload Selector",
diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json
index 573cbf4bd..0c4bb5538 100644
--- a/frontend/public/locales/zh/translation.json
+++ b/frontend/public/locales/zh/translation.json
@@ -58,7 +58,7 @@
   "Builds an Authorization Policy based on Rules": "Builds an Authorization Policy based on Rules",
   "CA Certificates": "CA证书",
   "Cancel": "取消",
-  "Check Status": "Check Status",
+  "Changes here are not saved to the Kiali CR. If happy with changes, update the Kiali CR using your standard change workflow.": "Changes here are not saved to the Kiali CR. If happy with changes, update the Kiali CR using your standard change workflow.",
   "Check the usual ports for the tracing service and provide a subset of the tracing configuration based on the tracing services found for external_services.tracing.": "Check the usual ports for the tracing service and provide a subset of the tracing configuration based on the tracing services found for external_services.tracing.",
   "Choose the traffic rates used to generate the graph. Each supported protocol offers one or more options. Unused protocols can be omitted.": "选择用于生成图表的流量速率。每个支持的协议都提供一个或多个选项。未使用的协议可以省略。",
   "Circuit Breaker": "熔断",
@@ -84,6 +84,9 @@
   "Condition Key not supported": "Condition Key not supported",
   "Condition List": "Condition List",
   "config": "config",
+  "Configuration Discovery": "Configuration Discovery",
+  "Configuration returned valid results": "Configuration returned valid results",
+  "Configuration Tester": "Configuration Tester",
   "Configuration:": "配置:",
   "Confirm": "Confirm",
   "Confirm Delete Traffic Routing ?": "确认删除流量路由 ?",
@@ -126,6 +129,7 @@
   "Direction type is not valid.": "Direction type is not valid.",
   "Disable Auto Injection": "禁用自动注入",
   "Disabled": "禁用",
+  "Discovery": "Discovery",
   "Display": "展示",
   "Display envoy config for the selected pod": "Display envoy config for the selected pod",
   "Display idle nodes": "Display idle nodes",
@@ -140,6 +144,7 @@
   "Duration": "持续时间",
   "Edges": "边",
   "Edit": "Edit",
+  "Edit and test configuration changes without having to modify the CR.": "Edit and test configuration changes without having to modify the CR.",
   "Edit Annotations": "编辑Annotations",
   "Either all namespaces are being filtered or the user has no permission to access namespaces.": "Either all namespaces are being filtered or the user has no permission to access namespaces.",
   "Empty Graph": "Empty Graph",
@@ -161,6 +166,7 @@
   "Every 5m": "每 5m",
   "Expand view": "展开视图",
   "Extension": "Extension",
+  "external_services.tracing configuration:": "external_services.tracing configuration:",
   "Failed Request": "失败的请求",
   "Failure": "Failure",
   "Fault Injection": "故障注入",
@@ -172,6 +178,7 @@
   "Filter by Namespace Label": "Filter by Namespace Label",
   "Find and Hide": "寻找或隐藏",
   "Fixed Delay": "延迟时长",
+  "For Performance issues troubleshooting, remove sensitive data before posting:": "For Performance issues troubleshooting, remove sensitive data before posting:",
   "FQDN": "完全限定域名",
   "From": "From",
   "From List": "From List",
@@ -213,6 +220,8 @@
   "Istio control plane": "Istio控制面板",
   "Istio deployment status disabled.": "Istio deployment status disabled.",
   "Istio revision {{version}}": "Istio revision {{version}}",
+  "JSON": "JSON",
+  "JSON Log Entry": "JSON Log Entry",
   "K8s Gateway": "K8s Gateway",
   "K8s Gateway API is not enabled": "K8s 网关API不可用",
   "K8s Gateways": "K8s 网关",
@@ -239,6 +248,7 @@
   "Loading...": "加载中...",
   "Login to {{cluster}}": "Login to {{cluster}}",
   "Logout": "Logout",
+  "Logs": "Logs",
   "Manual": "Manual",
   "Manual refresh required": "Manual refresh required",
   "Match 'Any request' is defined in a previous rule.": "之前的规则中已经定义了‘所有请求’的匹配项。",
@@ -334,6 +344,7 @@
   "Per Try Timeout": "重试间隔",
   "Percentage of requests on which the delay will be injected.": "注入了延迟的请求的占比。",
   "Percentage of requests to be aborted with the error code provided.": "中止并提供错误状态码的请求的比例。",
+  "Performance Measurements": "Performance Measurements",
   "Please include this information when opening a bug:": "Please include this information when opening a bug:",
   "Pod Name": "Pod Name",
   "Pod Status": "Pod Status",
@@ -347,6 +358,7 @@
   "Private Key must be non empty": "私钥必须非空",
   "Protocol": "Protocol",
   "Proxy sync time": "Proxy sync time",
+  "Rediscover": "Rediscover",
   "Refresh": "Refresh",
   "Refresh interval": "Refresh interval",
   "Refresh interval. \"Pause\" refreshes on page change or Refresh button click. \"Manual\" refreshes only on Refresh button click.": "Refresh interval. \"Pause\" refreshes on page change or Refresh button click. \"Manual\" refreshes only on Refresh button click.",
@@ -406,12 +418,16 @@
   "Switch language": "更换语言",
   "Switch to {{theme}} Mode": "Switch to {{theme}} Mode",
   "System Error": "System Error",
+  "Table": "Table",
   "Tag: {{tag}}": "Tag: {{tag}}",
   "TCP Connection": "TCP Connection",
   "TCP Traffic": "TCP Traffic",
   "TCP Traffic Shifting": "TCP 流量调整",
+  "Test Configuration": "Test Configuration",
+  "Tester": "Tester",
   "The control plane must sync each proxy when there is a configuration change. This is the average time spent, per proxy,                  to perform the update. N/A means there was no syncing performed for the configured time period.": "The control plane must sync each proxy when there is a configuration change. This is the average time spent, per proxy,                  to perform the update. N/A means there was no syncing performed for the configured time period.",
   "The destination hosts to which traffic is being sent. Enter one or multiple hosts separated by comma.": "发送流量时的目标主机，输入一个或多个主机，以逗号分隔。",
+  "The JSON Log entry has been copied to your clipboard.": "The JSON Log entry has been copied to your clipboard.",
   "The minimum TLS version for connections among Istio workloads. Defaults to TLSV1_2 if not set. Set by the 'meshConfig.meshMTLS.minProtocolVersion' field.": "The minimum TLS version for connections among Istio workloads. Defaults to TLSV1_2 if not set. Set by the 'meshConfig.meshMTLS.minProtocolVersion' field.",
   "The refresh interval is set to \"Manual\". To render the graph, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.": "The refresh interval is set to \"Manual\". To render the graph, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.",
   "The refresh interval is set to \"Manual\". To render the list, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.": "The refresh interval is set to \"Manual\". To render the list, select your desired filters and options and then click the Refresh button. Or, if preferred, change the setting to the desired interval.",
@@ -440,7 +456,6 @@
   "To": "To",
   "To List": "To List",
   "To read the FAQ entry about why your graph may be empty, ": "To read the FAQ entry about why your graph may be empty, ",
-  "Tracing Services Check Logs": "Tracing Services Check Logs",
   "Traffic": "流量",
   "Traffic Animation": "Traffic Animation",
   "Traffic Graph": "图",
@@ -468,12 +483,10 @@
   "Value": "Value",
   "Values": "Values",
   "Values and NotValues cannot be empty": "Values and NotValues cannot be empty",
-  "Verifying...": "Verifying...",
   "version: {{version}}": "version: {{version}}",
   "Version: {{version}}": "Version: {{version}}",
   "View Annotations": "查看Annotations",
   "View in Tracing": "View in Tracing",
-  "View logs": "View logs",
   "View on Graph": "View on Graph",
   "Virtual Service / Request Routing": "Virtual Service / Request Routing",
   "VirtualService Host '*' wildcard not allowed on mesh gateway.": "网状网关上不允许虚拟服务主机'*'通配符。",
@@ -489,7 +502,7 @@
   "When enabled in an Ambient environment, include waypoints in the mesh topology.": "When enabled in an Ambient environment, include waypoints in the mesh topology.",
   "When enabled, include gateways in the mesh topology.": "When enabled, include gateways in the mesh topology.",
   "When List": "When List",
-  "While the health check is based on whether the URL response returns an HTTP 200, the services check performs a more exhaustive verification by attempting to analyze if the traces response is correct. It is important that internal_url is defined, as it relies on this host to perform the checks. When in_cluster config is false, it will use the external_url": "While the health check is based on whether the URL response returns an HTTP 200, the services check performs a more exhaustive verification by attempting to analyze if the traces response is correct. It is important that internal_url is defined, as it relies on this host to perform the checks. When in_cluster config is false, it will use the external_url",
+  "While the health check is simple and checks only for an HTTP 200 response for the URL, this check is more comprehensive and attempts to validate the returned trace data. It will typically use the internal_url to perform the checks. If Kiali is running outside of the cluster, the external_url will be used.": "While the health check is simple and checks only for an HTTP 200 response for the URL, this check is more comprehensive and attempts to validate the returned trace data. It will typically use the internal_url to perform the checks. If Kiali is running outside of the cluster, the external_url will be used.",
   "Workload": "Workload",
   "Workload Entry": "Workload Entry",
   "Workload Selector": "Workload Selector",
diff --git a/frontend/src/components/BreadcrumbView/BreadcrumbView.tsx b/frontend/src/components/BreadcrumbView/BreadcrumbView.tsx
index f4f83cf59..93b2eb73f 100644
--- a/frontend/src/components/BreadcrumbView/BreadcrumbView.tsx
+++ b/frontend/src/components/BreadcrumbView/BreadcrumbView.tsx
@@ -6,13 +6,10 @@ import { FilterSelected } from '../Filters/StatefulFilters';
 import { HistoryManager } from '../../app/History';
 import { useKialiTranslation } from 'utils/I18nUtils';
 import { kindToStringIncludeK8s } from '../../utils/IstioConfigUtils';
+import { capitalize } from '../../utils/Common';
 
 const istioName = 'Istio Config';
 
-const capitalize = (str: string): string => {
-  return str.charAt(0).toUpperCase() + str.slice(1);
-};
-
 const cleanFilters = (): void => {
   FilterSelected.resetFilters();
 };
diff --git a/frontend/src/components/DebugInformation/DebugInformation.tsx b/frontend/src/components/DebugInformation/DebugInformation.tsx
index a930cd523..8d2d79b5f 100644
--- a/frontend/src/components/DebugInformation/DebugInformation.tsx
+++ b/frontend/src/components/DebugInformation/DebugInformation.tsx
@@ -30,6 +30,7 @@ import { ConfigTable } from 'components/Table/ConfigTable';
 import { useKialiTranslation } from 'utils/I18nUtils';
 import { download } from 'utils/Common';
 import { dump } from 'js-yaml';
+import { getAllPerfStats } from '../../utils/PerformanceUtils';
 
 enum CopyStatus {
   NOT_COPIED, // We haven't copied the current output
@@ -78,14 +79,15 @@ const defaultTab = 'kialiConfig';
 
 const tabIndex: { [tab: string]: number } = {
   kialiConfig: 0,
-  additionalState: 1
+  additionalState: 1,
+  perfData: 2
 };
 
 const modalStyle = kialiStyle({
   overflowY: 'hidden',
   $nest: {
     '& .pf-v5-c-tab-content': {
-      height: '525px',
+      height: '530px',
       overflowY: 'auto'
     }
   }
@@ -99,6 +101,12 @@ const tabStyle = kialiStyle({
   }
 });
 
+const spanStyle = kialiStyle({
+  display: 'inline-block',
+  marginBottom: '0.25rem',
+  marginTop: '0.25rem'
+});
+
 const DebugInformationComponent: React.FC<DebugInformationProps> = (props: DebugInformationProps) => {
   const [config, setConfig] = React.useState({});
   const [copyStatus, setCopyStatus] = React.useState(CopyStatus.NOT_COPIED);
@@ -148,8 +156,14 @@ const DebugInformationComponent: React.FC<DebugInformationProps> = (props: Debug
     setCopyStatus(result ? CopyStatus.COPIED : CopyStatus.NOT_COPIED);
   };
 
+  const downloadsMap: { [key: string]: string } = {
+    kialiConfig: 'kiali_config.yaml',
+    additionalState: 'additional_state.yaml',
+    perfData: 'perf_data.yaml'
+  };
+
   const downloadFile = (): void => {
-    const fileName = `debug_${currentTab === 'kialiConfig' ? 'kiali_config' : 'additional_state'}.yaml`;
+    const fileName = `debug_${downloadsMap[currentTab]}`;
 
     download(copyText, fileName);
   };
@@ -204,7 +218,20 @@ const DebugInformationComponent: React.FC<DebugInformationProps> = (props: Debug
     ...yamlDumpOptions
   });
 
-  const copyText = currentTab === 'kialiConfig' ? dump(config, yamlDumpOptions) : debugInformationText;
+  const perfMeasurements = dump(getAllPerfStats(), {
+    noRefs: true,
+    replacer: parseConfig,
+    skipInvalid: true,
+    ...yamlDumpOptions
+  });
+
+  const copyTextMap: { [key: string]: string } = {
+    kialiConfig: dump(config, yamlDumpOptions),
+    additionalState: debugInformationText,
+    perfData: perfMeasurements
+  };
+
+  const copyText = copyTextMap[currentTab];
 
   const renderTabs = (): React.ReactNode[] => {
     const kialiConfig = (
@@ -217,7 +244,7 @@ const DebugInformationComponent: React.FC<DebugInformationProps> = (props: Debug
 
     const additionalState = (
       <Tab eventKey={1} title={t('Additional State')} key="additionalState">
-        <span>{t('Please include this information when opening a bug:')}</span>
+        <span className={spanStyle}>{t('Please include this information when opening a bug:')}</span>
         <AceEditor
           ref={aceEditorRef}
           mode="yaml"
@@ -232,7 +259,25 @@ const DebugInformationComponent: React.FC<DebugInformationProps> = (props: Debug
       </Tab>
     );
 
-    return [kialiConfig, additionalState];
+    const perfData = (
+      <Tab eventKey={2} title={t('Performance Measurements')} key="perfData">
+        <span className={spanStyle}>
+          {t('For Performance issues troubleshooting, remove sensitive data before posting:')}
+        </span>
+        <AceEditor
+          ref={aceEditorRef}
+          mode="yaml"
+          theme={theme === Theme.DARK ? 'twilight' : 'eclipse'}
+          width="100%"
+          className={istioAceEditorStyle}
+          wrapEnabled={true}
+          readOnly={true}
+          setOptions={aceOptions ?? { foldStyle: 'markbegin' }}
+          value={perfMeasurements}
+        />
+      </Tab>
+    );
+    return [kialiConfig, additionalState, perfData];
   };
 
   return (
diff --git a/frontend/src/components/IstioStatus/IstioComponentStatus.tsx b/frontend/src/components/IstioStatus/IstioComponentStatus.tsx
index ca13289c3..5dd40f46f 100644
--- a/frontend/src/components/IstioStatus/IstioComponentStatus.tsx
+++ b/frontend/src/components/IstioStatus/IstioComponentStatus.tsx
@@ -70,7 +70,7 @@ export const IstioComponentStatus: React.FC<Props> = (props: Props) => {
 
     return [
       <Split key={`cell-status-icon-${comp.name}`} hasGutter={true} className={splitItemStyle}>
-        <SplitItem>{renderIcon(props.componentStatus.status, props.componentStatus.is_core)}</SplitItem>
+        <SplitItem>{renderIcon(props.componentStatus.status, props.componentStatus.isCore)}</SplitItem>
         <SplitItem isFilled={true}>{comp.name}</SplitItem>
         <SplitItem>{t(statusMsg[comp.status])}</SplitItem>
       </Split>
diff --git a/frontend/src/components/IstioStatus/IstioStatus.tsx b/frontend/src/components/IstioStatus/IstioStatus.tsx
index e1f2f599d..ee4bdddca 100644
--- a/frontend/src/components/IstioStatus/IstioStatus.tsx
+++ b/frontend/src/components/IstioStatus/IstioStatus.tsx
@@ -150,7 +150,7 @@ export const IstioStatusComponent: React.FC<Props> = (props: Props) => {
       ...components.map(
         cs =>
           statusSeverity[cs.status] +
-          (cs.is_core && statusSeverity[cs.status] !== statusSeverity[Status.Healthy] ? 10 : 0)
+          (cs.isCore && statusSeverity[cs.status] !== statusSeverity[Status.Healthy] ? 10 : 0)
       )
     ); // non health core component has much higher severity
 
@@ -197,11 +197,11 @@ export const IstioStatusComponent: React.FC<Props> = (props: Props) => {
     const values = Object.values(props.statusMap).flat();
 
     Object.keys(values ?? {}).forEach((compKey: string) => {
-      const { status, is_core } = values[compKey];
+      const { status, isCore } = values[compKey];
       const isNotReady: boolean = status === Status.NotReady;
       const isUnhealthy: boolean = status !== Status.Healthy && !isNotReady;
 
-      if (is_core) {
+      if (isCore) {
         coreUnhealthy = coreUnhealthy || isUnhealthy;
       } else {
         addonUnhealthy = addonUnhealthy || isUnhealthy;
diff --git a/frontend/src/components/IstioStatus/IstioStatusList.tsx b/frontend/src/components/IstioStatus/IstioStatusList.tsx
index 749fc8b02..7cfbd7cf5 100644
--- a/frontend/src/components/IstioStatus/IstioStatusList.tsx
+++ b/frontend/src/components/IstioStatus/IstioStatusList.tsx
@@ -24,11 +24,11 @@ export const IstioStatusList: React.FC<Props> = (props: Props) => {
   };
 
   const coreComponentsStatus = (): ComponentStatus[] => {
-    return nonhealthyComponents().filter((s: ComponentStatus) => s.is_core);
+    return nonhealthyComponents().filter((s: ComponentStatus) => s.isCore);
   };
 
   const addonComponentsStatus = (): ComponentStatus[] => {
-    return nonhealthyComponents().filter((s: ComponentStatus) => !s.is_core);
+    return nonhealthyComponents().filter((s: ComponentStatus) => !s.isCore);
   };
 
   const renderComponentList = (): React.ReactNode => {
diff --git a/frontend/src/components/IstioStatus/__tests__/IstioComponentStatus.test.tsx b/frontend/src/components/IstioStatus/__tests__/IstioComponentStatus.test.tsx
index cab5775fb..9f8d7318a 100644
--- a/frontend/src/components/IstioStatus/__tests__/IstioComponentStatus.test.tsx
+++ b/frontend/src/components/IstioStatus/__tests__/IstioComponentStatus.test.tsx
@@ -15,7 +15,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'isito-ingress',
       status: Status.Healthy,
-      is_core: true
+      isCore: true
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -27,7 +27,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'isito-ingress',
       status: Status.Unhealthy,
-      is_core: true
+      isCore: true
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -39,7 +39,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'isito-ingress',
       status: Status.NotFound,
-      is_core: true
+      isCore: true
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -51,7 +51,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'prometheus',
       status: Status.Healthy,
-      is_core: false
+      isCore: false
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -63,7 +63,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'prometheus',
       status: Status.Unhealthy,
-      is_core: false
+      isCore: false
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -75,7 +75,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'prometheus',
       status: Status.NotFound,
-      is_core: false
+      isCore: false
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -87,7 +87,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'core',
       status: Status.NotReady,
-      is_core: true
+      isCore: true
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
@@ -99,7 +99,7 @@ describe('IstioComponentStatus renders', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'addon',
       status: Status.NotReady,
-      is_core: false
+      isCore: false
     });
 
     expect(shallowToJson(wrapper)).toBeDefined();
diff --git a/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx b/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx
index 5afc056f1..1c011bcc8 100644
--- a/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx
+++ b/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx
@@ -68,13 +68,13 @@ describe('When core component has a problem', () => {
         cluster: CLUSTER_DEFAULT,
         name: 'grafana',
         status: Status.Healthy,
-        is_core: false
+        isCore: false
       },
       {
         cluster: CLUSTER_DEFAULT,
         name: 'istio-egressgateway',
         status: Status.Unhealthy,
-        is_core: true
+        isCore: true
       }
     ]);
 
@@ -91,13 +91,13 @@ describe('When addon component has a problem', () => {
         cluster: CLUSTER_DEFAULT,
         name: 'grafana',
         status: Status.Unhealthy,
-        is_core: false
+        isCore: false
       },
       {
         cluster: CLUSTER_DEFAULT,
         name: 'istio-egressgateway',
         status: Status.Healthy,
-        is_core: true
+        isCore: true
       }
     ]);
 
@@ -115,13 +115,13 @@ describe('When both core and addon component have problems', () => {
           cluster: CLUSTER_DEFAULT,
           name: 'grafana',
           status: Status.Unhealthy,
-          is_core: false
+          isCore: false
         },
         {
           cluster: CLUSTER_DEFAULT,
           name: 'istio-egressgateway',
           status: Status.Unhealthy,
-          is_core: true
+          isCore: true
         }
       ]);
 
@@ -141,13 +141,13 @@ describe('When there are not-ready components', () => {
             cluster: CLUSTER_DEFAULT,
             name: 'istio-egressgateway',
             status: Status.Unhealthy,
-            is_core: true
+            isCore: true
           },
           {
             cluster: CLUSTER_DEFAULT,
             name: 'istio-ingressgateway',
             status: Status.NotReady,
-            is_core: true
+            isCore: true
           }
         ]);
 
@@ -164,13 +164,13 @@ describe('When there are not-ready components', () => {
             cluster: CLUSTER_DEFAULT,
             name: 'grafana',
             status: Status.Unhealthy,
-            is_core: false
+            isCore: false
           },
           {
             cluster: CLUSTER_DEFAULT,
             name: 'jaeger',
             status: Status.NotReady,
-            is_core: false
+            isCore: false
           }
         ]);
 
@@ -187,25 +187,25 @@ describe('When there are not-ready components', () => {
             cluster: CLUSTER_DEFAULT,
             name: 'grafana',
             status: Status.Unhealthy,
-            is_core: false
+            isCore: false
           },
           {
             cluster: CLUSTER_DEFAULT,
             name: 'jaeger',
             status: Status.NotReady,
-            is_core: false
+            isCore: false
           },
           {
             cluster: CLUSTER_DEFAULT,
             name: 'istio-egressgateway',
             status: Status.Unhealthy,
-            is_core: true
+            isCore: true
           },
           {
             cluster: CLUSTER_DEFAULT,
             name: 'istio-ingressgateway',
             status: Status.NotReady,
-            is_core: true
+            isCore: true
           }
         ]);
 
@@ -224,7 +224,7 @@ describe('When there are not-ready components', () => {
             cluster: CLUSTER_DEFAULT,
             name: 'jaeger',
             status: Status.NotReady,
-            is_core: false
+            isCore: false
           }
         ]);
 
@@ -241,7 +241,7 @@ describe('When there are not-ready components', () => {
             cluster: CLUSTER_DEFAULT,
             name: 'istiod',
             status: Status.NotReady,
-            is_core: true
+            isCore: true
           }
         ]);
 
@@ -260,13 +260,13 @@ describe('When all components are good', () => {
         cluster: CLUSTER_DEFAULT,
         name: 'grafana',
         status: Status.Healthy,
-        is_core: false
+        isCore: false
       },
       {
         cluster: CLUSTER_DEFAULT,
         name: 'istio-egressgateway',
         status: Status.Healthy,
-        is_core: true
+        isCore: true
       }
     ]);
 
diff --git a/frontend/src/components/IstioStatus/__tests__/IstioStatusList.test.tsx b/frontend/src/components/IstioStatus/__tests__/IstioStatusList.test.tsx
index 56b452989..22de40d76 100644
--- a/frontend/src/components/IstioStatus/__tests__/IstioStatusList.test.tsx
+++ b/frontend/src/components/IstioStatus/__tests__/IstioStatusList.test.tsx
@@ -11,25 +11,25 @@ it('lists all the components grouped', () => {
       cluster: CLUSTER_DEFAULT,
       name: 'grafana',
       status: Status.NotFound,
-      is_core: false
+      isCore: false
     },
     {
       cluster: CLUSTER_DEFAULT,
       name: 'prometheus',
       status: Status.Unhealthy,
-      is_core: false
+      isCore: false
     },
     {
       cluster: CLUSTER_DEFAULT,
       name: 'istiod',
       status: Status.NotFound,
-      is_core: true
+      isCore: true
     },
     {
       cluster: CLUSTER_DEFAULT,
       name: 'istio-egressgateway',
       status: Status.Unhealthy,
-      is_core: true
+      isCore: true
     }
   ];
 
diff --git a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap
index 7695f625d..523edfa02 100644
--- a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap
+++ b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap
@@ -49,13 +49,13 @@ exports[`When addon component has a problem the Icon shows is displayed in orang
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "grafana",
                   "status": "Unhealthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-egressgateway",
                   "status": "Healthy",
                 },
@@ -149,13 +149,13 @@ exports[`When both core and addon component have problems any component is in no
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "grafana",
                   "status": "Unhealthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-egressgateway",
                   "status": "Unhealthy",
                 },
@@ -249,13 +249,13 @@ exports[`When core component has a problem the Icon shows is displayed in Red 1`
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "grafana",
                   "status": "Healthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-egressgateway",
                   "status": "Unhealthy",
                 },
@@ -349,13 +349,13 @@ exports[`When there are not-ready components mixed with other not healthy compon
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "grafana",
                   "status": "Unhealthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "jaeger",
                   "status": "NotReady",
                 },
@@ -449,25 +449,25 @@ exports[`When there are not-ready components mixed with other not healthy compon
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "grafana",
                   "status": "Unhealthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "jaeger",
                   "status": "NotReady",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-egressgateway",
                   "status": "Unhealthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-ingressgateway",
                   "status": "NotReady",
                 },
@@ -561,13 +561,13 @@ exports[`When there are not-ready components mixed with other not healthy compon
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-egressgateway",
                   "status": "Unhealthy",
                 },
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istio-ingressgateway",
                   "status": "NotReady",
                 },
@@ -661,7 +661,7 @@ exports[`When there are not-ready components not mixed with other unhealthy comp
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": true,
+                  "isCore": true,
                   "name": "istiod",
                   "status": "NotReady",
                 },
@@ -755,7 +755,7 @@ exports[`When there are not-ready components not mixed with other unhealthy comp
               Array [
                 Object {
                   "cluster": "Kubernetes",
-                  "is_core": false,
+                  "isCore": false,
                   "name": "jaeger",
                   "status": "NotReady",
                 },
diff --git a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatusList.test.tsx.snap b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatusList.test.tsx.snap
index cd13dba6d..78edaa764 100644
--- a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatusList.test.tsx.snap
+++ b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatusList.test.tsx.snap
@@ -10,7 +10,7 @@ exports[`lists all the components grouped 1`] = `
     componentStatus={
       Object {
         "cluster": "Kubernetes",
-        "is_core": true,
+        "isCore": true,
         "name": "istiod",
         "status": "NotFound",
       }
@@ -21,7 +21,7 @@ exports[`lists all the components grouped 1`] = `
     componentStatus={
       Object {
         "cluster": "Kubernetes",
-        "is_core": true,
+        "isCore": true,
         "name": "istio-egressgateway",
         "status": "Unhealthy",
       }
@@ -32,7 +32,7 @@ exports[`lists all the components grouped 1`] = `
     componentStatus={
       Object {
         "cluster": "Kubernetes",
-        "is_core": false,
+        "isCore": false,
         "name": "grafana",
         "status": "NotFound",
       }
@@ -43,7 +43,7 @@ exports[`lists all the components grouped 1`] = `
     componentStatus={
       Object {
         "cluster": "Kubernetes",
-        "is_core": false,
+        "isCore": false,
         "name": "prometheus",
         "status": "Unhealthy",
       }
diff --git a/frontend/src/components/Mesh/CheckerTracingConfig.tsx b/frontend/src/components/Mesh/CheckerTracingConfig.tsx
new file mode 100644
index 000000000..ce628db4e
--- /dev/null
+++ b/frontend/src/components/Mesh/CheckerTracingConfig.tsx
@@ -0,0 +1,140 @@
+import * as React from 'react';
+import { useKialiTranslation } from '../../utils/I18nUtils';
+import { Button, ButtonVariant, Spinner, Tooltip, TooltipPosition } from '@patternfly/react-core';
+import AceEditor from 'react-ace';
+import { Theme } from '../../types/Common';
+import { istioAceEditorStyle } from '../../styles/AceEditorStyle';
+import { aceOptions, yamlDumpOptions } from '../../types/IstioConfigDetails';
+import { getKialiTheme } from '../../utils/ThemeUtils';
+import { dump, loadAll, YAMLException } from 'js-yaml';
+import ReactAce from 'react-ace/lib/ace';
+import { ValidationTypes } from 'types/IstioObjects';
+import { kialiStyle } from '../../styles/StyleUtils';
+import { Validation } from '../Validations/Validation';
+import * as API from '../../services/Api';
+
+type CheckerTracingConfigProps = {
+  configData?: unknown;
+};
+
+const healthStatusStyle = kialiStyle({
+  marginLeft: '0.5rem'
+});
+
+export const CheckerTracingConfig: React.FC<CheckerTracingConfigProps> = (props: CheckerTracingConfigProps) => {
+  const { t } = useKialiTranslation();
+  const [loading, setLoading] = React.useState(false);
+  const [error, setError] = React.useState<string | null>(null);
+  const [isModified, setIsModified] = React.useState(false);
+  const aceEditorRef = React.useRef<ReactAce | null>(null);
+  const [source, setSource] = React.useState<string>(dump(props.configData, yamlDumpOptions));
+
+  const theme = getKialiTheme();
+  const [configResult, setConfigResult] = React.useState<string | null>(null);
+
+  const parseYamlDocumentsSync = (yamlText: string): any => {
+    let document: any;
+    try {
+      loadAll(yamlText, doc => {
+        document = doc;
+      });
+    } catch (e) {
+      if (e instanceof YAMLException) {
+        setLoading(false);
+        setError(e.message);
+      }
+    }
+    return document;
+  };
+
+  const checkConfig = (): void => {
+    const objectModified = parseYamlDocumentsSync(source);
+    if (!objectModified) {
+      return;
+    }
+    setLoading(true);
+    const jsonPatch = JSON.stringify(objectModified).replace(new RegExp('(,null)+]', 'g'), ']');
+    API.checkTracingConfig(jsonPatch)
+      .then(response => {
+        setLoading(false);
+        setIsModified(false);
+        if (response.data.error) {
+          setError(response.data.error);
+        } else {
+          setConfigResult(response.data.message);
+          setError(null);
+        }
+      })
+      .catch(err => {
+        setLoading(false);
+        setError(err.response?.data?.error);
+      });
+  };
+
+  const showResult = (): React.ReactElement => {
+    let healthSeverity: ValidationTypes;
+
+    if (error) {
+      healthSeverity = ValidationTypes.Error;
+    } else {
+      healthSeverity = ValidationTypes.Correct;
+    }
+
+    return (
+      <Tooltip
+        aria-label={t('Health status')}
+        position={TooltipPosition.right}
+        enableFlip={true}
+        content={<>{error ? error : t('Configuration returned valid results')}</>}
+      >
+        <span className={healthStatusStyle}>
+          <Validation severity={healthSeverity} />
+        </span>
+      </Tooltip>
+    );
+  };
+
+  const handleCheckConfig = (): void => {
+    checkConfig();
+  };
+
+  const onEditorChange = (value: string): void => {
+    setSource(value);
+    setIsModified(true);
+    setError(null);
+  };
+
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', height: '600px', paddingTop: '1em' }}>
+      <div style={{ flexGrow: 1, overflowY: 'auto' }}>
+        <span>{t('external_services.tracing configuration:')}</span>
+        <AceEditor
+          name="ace-editor-tester"
+          ref={aceEditorRef}
+          mode="yaml"
+          theme={theme === Theme.DARK ? 'twilight' : 'eclipse'}
+          onChange={onEditorChange}
+          width="100%"
+          className={istioAceEditorStyle}
+          height="95%"
+          wrapEnabled={true}
+          readOnly={false}
+          setOptions={aceOptions}
+          value={source}
+        />
+      </div>
+      <div>
+        <Button
+          style={{ marginTop: '10px', marginRight: '5px' }}
+          variant={ButtonVariant.secondary}
+          onClick={handleCheckConfig}
+          isDisabled={loading || !isModified}
+        >
+          {t('Test Configuration')}
+        </Button>
+        {loading && <Spinner size="sm" />}
+        {(configResult && !isModified) || error ? showResult() : ''}
+      </div>
+    </div>
+  );
+};
diff --git a/frontend/src/components/Mesh/DiscoveryTracingConfig.tsx b/frontend/src/components/Mesh/DiscoveryTracingConfig.tsx
new file mode 100644
index 000000000..34a47214d
--- /dev/null
+++ b/frontend/src/components/Mesh/DiscoveryTracingConfig.tsx
@@ -0,0 +1,219 @@
+import * as React from 'react';
+import { useKialiTranslation } from '../../utils/I18nUtils';
+import { Button, ButtonVariant, Spinner, Title } from '@patternfly/react-core';
+import { validateExternalUrl } from './TraceConfigurationModal';
+import { kialiStyle } from '../../styles/StyleUtils';
+import { TracingCheck, TracingInfo } from '../../types/TracingInfo';
+import { PFColors } from '../Pf/PfColors';
+import * as API from '../../services/Api';
+import { ExternalServiceInfo } from '../../types/StatusState';
+import { KialiDispatch } from '../../types/Redux';
+import { bindActionCreators } from 'redux';
+import { TracingActions } from '../../actions/TracingActions';
+import { connect } from 'react-redux';
+import { KialiAppState } from '../../store/Store';
+import { classes } from 'typestyle';
+
+type ReduxProps = {
+  externalServices: ExternalServiceInfo[];
+  kiosk: string;
+  tracingDiagnose?: TracingCheck;
+  tracingInfo?: TracingInfo;
+};
+
+type ReduxDispatchProps = {
+  setTracingDiagnose: (err?: TracingCheck) => void;
+};
+
+type CheckModalProps = ReduxDispatchProps &
+  ReduxProps & {
+    cluster: string;
+    externalServices: ExternalServiceInfo[];
+    kiosk: string;
+    tracingDiagnose?: TracingCheck;
+    tracingInfo?: TracingInfo;
+  };
+
+const configStyle = kialiStyle({
+  fontFamily: 'Courier New, Courier, monospace',
+  fontSize: 'small',
+  marginBottom: '0.75em'
+});
+
+const blockDisplay = kialiStyle({
+  display: 'block'
+});
+
+const containerStyle = kialiStyle({
+  fontFamily: 'Courier New, Courier, monospace',
+  fontSize: 'small',
+  resize: 'none',
+  whiteSpace: 'pre',
+  width: '100%',
+  overflowX: 'scroll',
+  backgroundColor: PFColors.BackgroundColor100,
+  border: '1px solid #ddd',
+  borderRadius: '6px',
+  padding: '1rem',
+  boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
+});
+
+const blueDisplay = kialiStyle({
+  color: PFColors.LightBlue500
+});
+
+const padding = kialiStyle({
+  padding: '0 0.5em'
+});
+
+const greyDisplay = kialiStyle({
+  padding: '0 0.5em'
+});
+
+export const CheckConfigComp: React.FC<CheckModalProps> = (props: CheckModalProps) => {
+  const { t } = useKialiTranslation();
+  const [loading, setLoading] = React.useState(false);
+  const [error, setError] = React.useState<string | null>(null);
+  const externalUrl = validateExternalUrl(props.externalServices, props.kiosk, props.tracingInfo);
+
+  const labels = {
+    namespaceSelector: 'NamespaceSelector',
+    provider: 'Provider',
+    url: 'InternalUrl',
+    useGRPC: 'UseGRPC'
+  };
+
+  React.useEffect(() => {
+    if (!props.tracingDiagnose) {
+      fetchCheckService();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const fetchCheckService = async (): Promise<void> => {
+    setLoading(true);
+    setError(null);
+
+    return API.getDiagnoseStatus(props.cluster)
+      .then(response => {
+        props.setTracingDiagnose(response.data);
+        setLoading(false);
+      })
+      .catch(err => {
+        setLoading(false);
+        const errString = err.response.data.error ? err.response.data.error : err;
+        setError(`${errString}`);
+      });
+  };
+
+  const handleCheckService = async (): Promise<void> => {
+    fetchCheckService();
+  };
+
+  return (
+    <div
+      id="discovery-tab-content"
+      style={{ display: 'flex', flexDirection: 'column', height: '600px', paddingTop: '1em' }}
+    >
+      <div style={{ flexGrow: 1, overflowY: 'auto' }}>
+        {props.tracingDiagnose && (
+          <>
+            <span style={{ color: 'green' }}>{props.tracingDiagnose.message}</span>
+            <div style={{ marginTop: '1em' }}>
+              <span>{t('Possible configuration(s) found for external_services.tracing')}:</span>
+            </div>
+          </>
+        )}
+        {error && (
+          <div>
+            <span style={{ color: 'red' }}>{error}</span>
+          </div>
+        )}
+        {props.tracingDiagnose?.validConfig && (
+          <>
+            <div style={{ margin: '0.5em 0', display: 'flex' }}>
+              <span>
+                {props.tracingDiagnose?.validConfig.length === 0 && <>No configurations found. See logs for details</>}
+              </span>
+            </div>
+            <div id="valid-configurations">
+              {props.tracingDiagnose?.validConfig?.map((item, i) => (
+                <>
+                  <div className={configStyle}>
+                    {Object.keys(item).map(key => {
+                      if (labels[key] !== undefined && item[key] != null) {
+                        return (
+                          <span className={blockDisplay}>
+                            <span className={blueDisplay}>{labels[key]}:</span> {item[key].toString()}
+                          </span>
+                        );
+                      }
+                      return null;
+                    })}
+                    {item.warning && <span style={{ color: 'red' }}>{item.warning}</span>}
+                  </div>
+                  {props.tracingDiagnose?.validConfig && i < props.tracingDiagnose?.validConfig?.length - 1 && (
+                    <div style={{ padding: '0 0 10px 5px' }}>---</div>
+                  )}
+                </>
+              ))}
+            </div>
+          </>
+        )}
+        {props.tracingDiagnose?.validConfig && externalUrl && (
+          <div>
+            <span className={configStyle}>{externalUrl}</span>
+          </div>
+        )}
+        {props.tracingDiagnose?.logLine && (
+          <>
+            <div style={{ margin: '1em 0' }}>
+              <Title headingLevel="h4" size="lg" style={{ paddingBottom: '10px' }}>
+                {t('Logs')}:
+              </Title>
+              <div className={containerStyle} id={'configuration-logs'}>
+                {props.tracingDiagnose.logLine.map(log => (
+                  <>
+                    <div>
+                      <span>
+                        <span className={greyDisplay}>{log.time.substring(0, 19)}</span>
+                        <span className={classes(blueDisplay, padding)}>[{log.test}]</span>
+                        {log.result}
+                      </span>
+                    </div>
+                  </>
+                ))}
+              </div>
+            </div>
+          </>
+        )}
+      </div>
+      <div>
+        <Button
+          onClick={handleCheckService}
+          disabled={loading}
+          variant={ButtonVariant.secondary}
+          style={{ marginRight: '5px' }}
+        >
+          {t('Rediscover')}
+        </Button>
+        {loading && <Spinner id="discover-spinner" size="sm" />}
+      </div>
+    </div>
+  );
+};
+
+const mapStateToProps = (state: KialiAppState): ReduxProps => {
+  return {
+    externalServices: state.statusState.externalServices,
+    kiosk: state.globalState.kiosk,
+    tracingDiagnose: state.tracingState.diagnose,
+    tracingInfo: state.tracingState.info
+  };
+};
+
+const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({
+  setTracingDiagnose: bindActionCreators(TracingActions.setDiagnose, dispatch)
+});
+
+export const DiscoveryTracingConfig = connect(mapStateToProps, mapDispatchToProps)(CheckConfigComp);
diff --git a/frontend/src/components/Mesh/LogsModal.tsx b/frontend/src/components/Mesh/LogsModal.tsx
deleted file mode 100644
index e9674bc65..000000000
--- a/frontend/src/components/Mesh/LogsModal.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as React from 'react';
-import { LogLine } from '../../types/TracingInfo';
-import { Button, Modal, ModalVariant } from '@patternfly/react-core';
-import { kialiStyle } from '../../styles/StyleUtils';
-import { useKialiTranslation } from '../../utils/I18nUtils';
-import { PFColors } from '../Pf/PfColors';
-
-type LogsModalProps = {
-  isOpen: boolean;
-  logs: LogLine[];
-  onClose: () => void;
-};
-
-const modalStyle = kialiStyle({
-  overflowY: 'hidden'
-});
-
-const containerStyle = kialiStyle({
-  backgroundColor: PFColors.Black1000,
-  color: PFColors.Blue100,
-  fontFamily: 'Courier New, Courier, monospace',
-  margin: 0,
-  padding: '0.5em',
-  resize: 'none',
-  whiteSpace: 'pre',
-  width: '100%',
-  overflowX: 'scroll'
-});
-
-const blueDisplay = kialiStyle({
-  color: PFColors.Blue400
-});
-
-const blueDarkDisplay = kialiStyle({
-  color: PFColors.Blue200,
-  padding: '0 0.5em'
-});
-
-export const LogsModal: React.FC<LogsModalProps> = (props: LogsModalProps) => {
-  const { t } = useKialiTranslation();
-
-  if (!props.isOpen) {
-    return null;
-  }
-
-  return (
-    <Modal
-      className={modalStyle}
-      variant={ModalVariant.medium}
-      isOpen={props.isOpen}
-      onClose={props.onClose}
-      title={t('Tracing Services Check Logs')}
-      actions={[
-        <Button key="close" onClick={props.onClose}>
-          {t('Close')}
-        </Button>
-      ]}
-    >
-      <div className={containerStyle}>
-        {props.logs.map(log => (
-          <>
-            <div>
-              <span>
-                <span className={blueDisplay}>{log.time.substring(0, 19)}</span>
-                <span className={blueDarkDisplay}>[{log.test}]</span>
-                {log.result}
-              </span>
-            </div>
-          </>
-        ))}
-      </div>
-    </Modal>
-  );
-};
diff --git a/frontend/src/components/Mesh/TraceConfigurationModal.tsx b/frontend/src/components/Mesh/TraceConfigurationModal.tsx
new file mode 100644
index 000000000..49d629abb
--- /dev/null
+++ b/frontend/src/components/Mesh/TraceConfigurationModal.tsx
@@ -0,0 +1,199 @@
+import * as React from 'react';
+import { TracingCheck, TracingInfo } from '../../types/TracingInfo';
+import { Button, Modal, ModalVariant, Tab, TabAction } from '@patternfly/react-core';
+import { useKialiTranslation } from '../../utils/I18nUtils';
+import { ExternalServiceInfo, TempoUrlFormat } from '../../types/StatusState';
+import { isParentKiosk } from '../Kiosk/KioskActions';
+import { isTempoService, TempoUrlProvider } from '../../utils/tracing/UrlProviders/Tempo';
+import { isJaegerService, JaegerUrlProvider } from '../../utils/tracing/UrlProviders/Jaeger';
+import { KialiAppState } from '../../store/Store';
+import { KialiDispatch } from '../../types/Redux';
+import { bindActionCreators } from 'redux';
+import { TracingActions } from '../../actions/TracingActions';
+import { connect } from 'react-redux';
+import { ParameterizedTabs } from '../Tab/Tabs';
+import { DiscoveryTracingConfig } from './DiscoveryTracingConfig';
+import { CheckerTracingConfig } from './CheckerTracingConfig';
+import { HelpIcon } from '@patternfly/react-icons';
+import { helpPopover } from '../../pages/Mesh/target/TargetPanelControlPlane';
+
+type ReduxProps = {
+  externalServices: ExternalServiceInfo[];
+  kiosk: string;
+  tracingCheck?: TracingCheck;
+  tracingInfo?: TracingInfo;
+};
+
+type ReduxDispatchProps = {
+  setTracingDiagnose: (err?: TracingCheck) => void;
+};
+
+type TraceConfigurationModalProps = ReduxProps &
+  ReduxDispatchProps & {
+    cluster: string;
+    configData: unknown;
+    isOpen: boolean;
+    onClose: () => void;
+  };
+
+const defaultTab = 'checkConfig';
+
+const tabIndex: { [tab: string]: number } = {
+  checkConfig: 0,
+  testConfig: 1
+};
+
+export const validateExternalUrl = (
+  externalServices: ExternalServiceInfo[],
+  kiosk: string,
+  info?: TracingInfo
+): string | undefined => {
+  const svc = externalServices.find(s => s.name === info?.provider);
+  if (!svc) return `"View in Tracing" link is hidden because external_url is not defined (No service found)`;
+
+  if (isParentKiosk(kiosk)) {
+    return 'kiosk mode detected. kiosk will try to use the Distributed Tracing UI link. In case the configuration is not found, it will use the external_url.';
+  }
+
+  if (isTempoService(svc)) {
+    if (svc.tempoConfig?.urlFormat === TempoUrlFormat.JAEGER) {
+      const urlProvider = new JaegerUrlProvider(svc);
+      if (!urlProvider.HomeUrl() || !urlProvider.valid) {
+        return '"View in Tracing" is hidden because external_url is empty';
+      }
+    } else {
+      const urlProvider = new TempoUrlProvider(svc, externalServices);
+      if (!urlProvider.HomeUrl() || !urlProvider.valid) {
+        return "\"View in Tracing\" link is hidden because Grafana is not enabled. To use external_url as 'View in tracing' link, tempo_config.url_format must be set to 'jaeger'";
+      }
+    }
+  }
+  if (isJaegerService(svc)) {
+    const urlProvider = new JaegerUrlProvider(svc);
+    if (!urlProvider.HomeUrl() || !urlProvider.valid) {
+      return '"View in Tracing" is hidden because external_url is empty';
+    }
+  }
+
+  return undefined;
+};
+
+export const TraceConfigurationModalComp: React.FC<TraceConfigurationModalProps> = (
+  props: TraceConfigurationModalProps
+) => {
+  const { t } = useKialiTranslation();
+
+  const [currentTab, setCurrentTab] = React.useState(defaultTab);
+
+  const checkConfigHelp = (
+    <>
+      {t(
+        'Check the usual ports for the tracing service and provide a subset of the tracing configuration based on the tracing services found for external_services.tracing.'
+      )}
+      <br />
+      {t(
+        'While the health check is simple and checks only for an HTTP 200 response for the URL, this check is more comprehensive and attempts to validate the returned trace data. It will typically use the internal_url to perform the checks. If Kiali is running outside of the cluster, the external_url will be used.'
+      )}
+    </>
+  );
+
+  const testConfigHelp = (
+    <>
+      {t('Edit and test configuration changes without having to modify the CR.')}
+      <br />
+      {t(
+        'Changes here are not saved to the Kiali CR. If happy with changes, update the Kiali CR using your standard change workflow.'
+      )}
+    </>
+  );
+
+  const renderTabs = (): React.ReactNode[] => {
+    const ref = React.createRef<HTMLElement>();
+    const refTest = React.createRef<HTMLElement>();
+    const checkConfig = (
+      <Tab
+        eventKey={0}
+        title={t('Discovery')}
+        key="checkConfig"
+        actions={
+          <>
+            <TabAction aria-label={`Help action for Check config`} ref={ref}>
+              <HelpIcon />
+            </TabAction>
+            {helpPopover(t('Configuration Discovery'), checkConfigHelp, ref)}
+          </>
+        }
+      >
+        <DiscoveryTracingConfig cluster={props.cluster} />
+      </Tab>
+    );
+
+    const testConfig = (
+      <Tab
+        eventKey={1}
+        title={t('Tester')}
+        key="testConfig"
+        actions={
+          <>
+            <TabAction aria-label={`Help action for configuration test`} ref={refTest}>
+              <HelpIcon />
+            </TabAction>
+            {helpPopover(t('Configuration Tester'), testConfigHelp, refTest)}
+          </>
+        }
+      >
+        <CheckerTracingConfig configData={props.configData} />
+      </Tab>
+    );
+
+    return [checkConfig, testConfig];
+  };
+
+  if (!props.isOpen) {
+    return null;
+  }
+
+  return (
+    <Modal
+      variant={ModalVariant.medium}
+      isOpen={props.isOpen}
+      onClose={props.onClose}
+      data-test="modal-configuration-tester"
+      title={t('Configuration Tester')}
+      actions={[
+        <Button key="close" onClick={props.onClose}>
+          {t('Close')}
+        </Button>
+      ]}
+    >
+      <ParameterizedTabs
+        id="basic-tabs"
+        onSelect={tabValue => {
+          setCurrentTab(tabValue);
+        }}
+        tabMap={tabIndex}
+        defaultTab={defaultTab}
+        activeTab={currentTab}
+        mountOnEnter={false}
+        unmountOnExit={false}
+      >
+        {renderTabs()}
+      </ParameterizedTabs>
+    </Modal>
+  );
+};
+
+const mapStateToProps = (state: KialiAppState): ReduxProps => {
+  return {
+    externalServices: state.statusState.externalServices,
+    kiosk: state.globalState.kiosk,
+    tracingCheck: state.tracingState.diagnose,
+    tracingInfo: state.tracingState.info
+  };
+};
+
+const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({
+  setTracingDiagnose: bindActionCreators(TracingActions.setDiagnose, dispatch)
+});
+
+export const TraceConfigurationModal = connect(mapStateToProps, mapDispatchToProps)(TraceConfigurationModalComp);
diff --git a/frontend/src/components/Mesh/TraceDiagnose.tsx b/frontend/src/components/Mesh/TraceDiagnose.tsx
deleted file mode 100644
index 2d74d1a8a..000000000
--- a/frontend/src/components/Mesh/TraceDiagnose.tsx
+++ /dev/null
@@ -1,258 +0,0 @@
-import * as React from 'react';
-import { useKialiTranslation } from '../../utils/I18nUtils';
-import { TracingCheck, TracingInfo } from '../../types/TracingInfo';
-import * as API from '../../services/Api';
-import { kialiStyle } from 'styles/StyleUtils';
-import { LogsModal } from './LogsModal';
-import { KialiAppState } from '../../store/Store';
-import { connect } from 'react-redux';
-import { ExternalServiceInfo, TempoUrlFormat } from '../../types/StatusState';
-import { isJaegerService, JaegerUrlProvider } from '../../utils/tracing/UrlProviders/Jaeger';
-import { isTempoService, TempoUrlProvider } from '../../utils/tracing/UrlProviders/Tempo';
-import { isParentKiosk } from '../Kiosk/KioskActions';
-import { MeshNodeData } from '../../types/Mesh';
-import { KialiIcon } from 'config/KialiIcon';
-import { Button, ButtonVariant, Popover, PopoverPosition } from '@patternfly/react-core';
-import { PFColors } from '../Pf/PfColors';
-import { KialiDispatch } from '../../types/Redux';
-import { bindActionCreators } from 'redux';
-import { TracingActions } from '../../actions/TracingActions';
-
-type ReduxProps = {
-  externalServices: ExternalServiceInfo[];
-  kiosk: string;
-  tracingDiagnose?: TracingCheck;
-  tracingInfo?: TracingInfo;
-};
-
-type ReduxDispatchProps = {
-  setTracingDiagnose: (err?: TracingCheck) => void;
-};
-
-type TracingDiagnoseProps = ReduxProps &
-  ReduxDispatchProps & {
-    cluster: string;
-    config: MeshNodeData;
-  };
-
-const configStyle = kialiStyle({
-  fontFamily: 'Courier New, Courier, monospace',
-  fontSize: 'small',
-  margin: '1.25em'
-});
-
-const blockDisplay = kialiStyle({
-  display: 'block'
-});
-
-const blueDisplay = kialiStyle({
-  color: 'rgb(25 116 116);'
-});
-
-const helpStyle = kialiStyle({
-  marginBottom: '0.6em',
-  marginLeft: '0.375rem',
-  $nest: {
-    '&:hover': {
-      color: PFColors.Color200,
-      cursor: 'pointer'
-    }
-  }
-});
-
-const validateExternalUrl = (
-  externalServices: ExternalServiceInfo[],
-  kiosk: string,
-  info?: TracingInfo
-): string | undefined => {
-  const svc = externalServices.find(s => s.name === info?.provider);
-  if (!svc) return `"View in Tracing" link is hidden because external_url is not defined (No service found)`;
-
-  if (isParentKiosk(kiosk)) {
-    return 'kiosk mode detected. kiosk will try to use the Distributed Tracing UI link. In case the configuration is not found, it will use the external_url.';
-  }
-
-  if (isTempoService(svc)) {
-    if (svc.tempoConfig?.url_format === TempoUrlFormat.JAEGER) {
-      const urlProvider = new JaegerUrlProvider(svc);
-      if (!urlProvider.HomeUrl() || !urlProvider.valid) {
-        return '"View in Tracing" is hidden because external_url is empty';
-      }
-    } else {
-      const urlProvider = new TempoUrlProvider(svc, externalServices);
-      if (!urlProvider.HomeUrl() || !urlProvider.valid) {
-        return "\"View in Tracing\" link is hidden because Grafana is not enabled. To use external_url as 'View in tracing' link, tempo_config.url_format must be set to 'jaeger'";
-      }
-    }
-  }
-  if (isJaegerService(svc)) {
-    const urlProvider = new JaegerUrlProvider(svc);
-    if (!urlProvider.HomeUrl() || !urlProvider.valid) {
-      return '"View in Tracing" is hidden because external_url is empty';
-    }
-  }
-
-  return undefined;
-};
-
-export const TracingDiagnoseComp: React.FC<TracingDiagnoseProps> = (props: TracingDiagnoseProps) => {
-  const fetchCheckService = async (): Promise<void> => {
-    setLoading(true);
-    props.setTracingDiagnose();
-    setError(null);
-
-    return API.getDiagnoseStatus(props.cluster)
-      .then(response => {
-        props.setTracingDiagnose(response.data);
-        setLoading(false);
-      })
-      .catch(err => {
-        setLoading(false);
-        const errString = err.response.data.error ? err.response.data.error : err;
-        setError(`Could not fetch diagnose info ${errString}`);
-      });
-  };
-
-  const handleCheckService = async (): Promise<void> => {
-    fetchCheckService();
-  };
-
-  const { t } = useKialiTranslation();
-  const [loading, setLoading] = React.useState(false);
-  //const [diagnostic, setDiagnostic] = React.useState<StatusError | null>(null);
-  const [error, setError] = React.useState<string | null>(null);
-  const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
-  const externalUrl = validateExternalUrl(props.externalServices, props.kiosk, props.tracingInfo);
-  const labels = {
-    authType: 'auth.type',
-    namespaceSelector: 'namespace_selector',
-    provider: 'provider',
-    url: 'internal_url',
-    useGRPC: 'use_grpc'
-  };
-  return (
-    <>
-      <div style={{ paddingTop: '0.25em' }}>
-        <div>
-          <span style={{ paddingTop: '0.25em' }}>Status</span>
-          <Popover
-            data-test="check-status-help"
-            position={PopoverPosition.auto}
-            headerContent={
-              <div>
-                <span>Check Status</span>
-              </div>
-            }
-            bodyContent={
-              <>
-                {t(
-                  'Check the usual ports for the tracing service and provide a subset of the tracing configuration based on the tracing services found for external_services.tracing.'
-                )}
-                <br />
-                {t(
-                  'While the health check is based on whether the URL response returns an HTTP 200, the services check performs a more exhaustive verification by attempting to analyze if the traces response is correct. It is important that internal_url is defined, as it relies on this host to perform the checks. When in_cluster config is false, it will use the external_url'
-                )}
-              </>
-            }
-          >
-            <KialiIcon.Help className={helpStyle} />
-          </Popover>
-        </div>
-        <div style={{ display: 'flex' }}>
-          <Button onClick={handleCheckService} disabled={loading} variant={ButtonVariant.secondary}>
-            {loading ? t('Verifying...') : t('Check Status')}
-          </Button>
-          {props.tracingDiagnose && (
-            <span style={{ position: 'absolute', right: '1em' }}>
-              <Button
-                variant={ButtonVariant.link}
-                aria-label={t('Close')}
-                isInline
-                onClick={() => props.setTracingDiagnose()}
-              >
-                <KialiIcon.Close />
-                <span>{t('Close')}</span>
-              </Button>
-            </span>
-          )}
-        </div>
-        {props.tracingDiagnose && <span style={{ color: 'green' }}>{props.tracingDiagnose.message}</span>}
-        {error && (
-          <div>
-            <span style={{ color: 'red' }}>{props.tracingDiagnose?.error ? props.tracingDiagnose?.error : error}</span>
-          </div>
-        )}
-        {props.tracingDiagnose?.validConfig && (
-          <>
-            <div style={{ margin: '0.5em 0', display: 'flex' }}>
-              <span>
-                {props.tracingDiagnose?.validConfig.length > 0 && (
-                  <>{t('Possible configuration(s) found for external_services.tracing')}:</>
-                )}
-                {props.tracingDiagnose?.validConfig.length === 0 && <>No configurations found. See logs for details</>}
-              </span>
-            </div>
-            <div>
-              {props.tracingDiagnose?.validConfig?.map((item, i) => (
-                <>
-                  <div className={configStyle}>
-                    {Object.keys(item).map(key => {
-                      if (labels[key] !== undefined && item[key] !== null) {
-                        return (
-                          <span className={blockDisplay}>
-                            <span className={blueDisplay}>{labels[key]}:</span> {item[key].toString()}
-                          </span>
-                        );
-                      }
-                      return null;
-                    })}
-                    {item.warning && <span style={{ color: 'red' }}>{item.warning}</span>}
-                  </div>
-                  {props.tracingDiagnose?.validConfig && i < props.tracingDiagnose?.validConfig?.length - 1 && <hr />}
-                </>
-              ))}
-            </div>
-          </>
-        )}
-        {props.tracingDiagnose?.logLine && externalUrl && (
-          <div>
-            <span className={configStyle}>{externalUrl}</span>
-          </div>
-        )}
-        {props.tracingDiagnose?.logLine && (
-          <div style={{ paddingTop: '1em' }}>
-            <a
-              href="#"
-              onClick={e => {
-                e.preventDefault();
-                setIsModalOpen(true);
-              }}
-            >
-              {t('View logs')}
-            </a>
-            <LogsModal
-              isOpen={isModalOpen}
-              onClose={() => setIsModalOpen(false)}
-              logs={props.tracingDiagnose?.logLine}
-            />
-          </div>
-        )}
-      </div>
-    </>
-  );
-};
-
-const mapStateToProps = (state: KialiAppState): ReduxProps => {
-  return {
-    externalServices: state.statusState.externalServices,
-    kiosk: state.globalState.kiosk,
-    tracingDiagnose: state.tracingState.diagnose,
-    tracingInfo: state.tracingState.info
-  };
-};
-
-const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({
-  setTracingDiagnose: bindActionCreators(TracingActions.setDiagnose, dispatch)
-});
-
-export const TracingDiagnose = connect(mapStateToProps, mapDispatchToProps)(TracingDiagnoseComp);
diff --git a/frontend/src/components/Mesh/TracingDiagnose.tsx b/frontend/src/components/Mesh/TracingDiagnose.tsx
new file mode 100644
index 000000000..601ea96fe
--- /dev/null
+++ b/frontend/src/components/Mesh/TracingDiagnose.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react';
+import { TraceConfigurationModal } from './TraceConfigurationModal';
+import { Button, ButtonVariant } from '@patternfly/react-core';
+import { useKialiTranslation } from '../../utils/I18nUtils';
+
+type TracingDiagnoseProps = {
+  cluster: string;
+  configData: unknown;
+};
+
+export const TracingDiagnose: React.FC<TracingDiagnoseProps> = (props: TracingDiagnoseProps) => {
+  const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
+  const { t } = useKialiTranslation();
+
+  return (
+    <>
+      <div style={{ marginLeft: 'auto' }}>
+        <Button style={{ marginLeft: '5px' }} onClick={() => setIsModalOpen(true)} variant={ButtonVariant.secondary}>
+          {t('Configuration Tester')}
+        </Button>
+        <TraceConfigurationModal
+          isOpen={isModalOpen}
+          onClose={() => setIsModalOpen(false)}
+          cluster={props.cluster}
+          configData={props.configData}
+        />
+      </div>
+    </>
+  );
+};
diff --git a/frontend/src/components/Metrics/TraceLimit.tsx b/frontend/src/components/Metrics/TraceLimit.tsx
index 220815022..fd28ec320 100644
--- a/frontend/src/components/Metrics/TraceLimit.tsx
+++ b/frontend/src/components/Metrics/TraceLimit.tsx
@@ -1,7 +1,6 @@
 import * as React from 'react';
-import { Radio, Tooltip, TooltipPosition } from '@patternfly/react-core';
+import { Slider, SliderOnChangeEvent, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { KialiIcon } from 'config/KialiIcon';
-import { itemStyleWithoutInfo } from 'styles/DropdownStyles';
 import { ToolbarDropdown } from 'components/Dropdown/ToolbarDropdown';
 import { infoStyle } from 'styles/IconStyle';
 
@@ -19,13 +18,7 @@ export type TraceLimitOption = 20 | 100 | 500 | 1000;
 export const TraceLimit: React.FC<TraceLimitProps> = (props: TraceLimitProps) => {
   const initialLimit = props.initialLimit ?? TRACE_LIMIT_DEFAULT;
   const [limit, setLimit] = React.useState<number>(initialLimit);
-
-  const onLimitChangeRadio = (limit: number, checked: boolean): void => {
-    if (checked) {
-      setLimit(limit);
-      props.onLimitChange(limit);
-    }
-  };
+  const [value, setValue] = React.useState<number>(initialLimit);
 
   const onLimitChange = (limitStr: string): void => {
     const limit = parseInt(limitStr);
@@ -59,34 +52,33 @@ export const TraceLimit: React.FC<TraceLimitProps> = (props: TraceLimitProps) =>
     1000: '1000 traces'
   };
 
+  const handleRelease = (): void => {
+    if (value !== limit) {
+      props.onLimitChange(value);
+      setLimit(value);
+    }
+  };
+
   const traceLimitComponent = (
-    <span id="trace-limit">
+    <div>
       <div style={{ marginTop: '0.5rem' }}>
         <span className={props.titleClassName} style={{ paddingRight: 0 }}>
-          {props.title}
+          {props.title} ({value})
         </span>
         {tooltip}
       </div>
-
-      {Object.keys(traceLimits).map(key => {
-        const lim = parseInt(key);
-        return (
-          <div key={`limit-${lim}`}>
-            <label key={`limit-${lim}`} className={itemStyleWithoutInfo}>
-              <Radio
-                id={`limit-${lim}`}
-                data-test={`limit-${lim}`}
-                name={`limit-${lim}`}
-                isChecked={lim === limit}
-                label={lim}
-                onChange={(_event, checked) => onLimitChangeRadio(lim, checked)}
-                value={lim}
-              />
-            </label>
-          </div>
-        );
-      })}
-    </span>
+      <div id="trace-limit" onMouseUp={handleRelease} style={{ padding: '0 10px' }}>
+        <Slider
+          value={value}
+          onChange={(_event: SliderOnChangeEvent, value: number) => {
+            setValue(value);
+          }}
+          min={10}
+          max={1000}
+          step={10}
+        />
+      </div>
+    </div>
   );
 
   const traceLimitDropdownComponent = (
diff --git a/frontend/src/components/Pf/PfColors.tsx b/frontend/src/components/Pf/PfColors.tsx
index b3e676b5c..1e383d490 100644
--- a/frontend/src/components/Pf/PfColors.tsx
+++ b/frontend/src/components/Pf/PfColors.tsx
@@ -33,6 +33,7 @@ export enum PFColors {
   Green500 = 'var(--pf-v5-global--palette--green-500)',
   Green600 = 'var(--pf-v5-global--palette--green-600)',
   LightBlue400 = 'var(--pf-v5-global--palette--light-blue-400)',
+  LightBlue500 = 'var(--pf-v5-global--palette--light-blue-500)',
   LightGreen400 = 'var(--pf-v5-global--palette--light-green-400)',
   LightGreen500 = 'var(--pf-v5-global--palette--light-green-500)',
   Orange50 = 'var(--pf-v5-global--palette--orange-50)',
diff --git a/frontend/src/components/TracingIntegration/TracesDisplayOptions.tsx b/frontend/src/components/TracingIntegration/TracesDisplayOptions.tsx
index 7c9df588a..5343fb73a 100644
--- a/frontend/src/components/TracingIntegration/TracesDisplayOptions.tsx
+++ b/frontend/src/components/TracingIntegration/TracesDisplayOptions.tsx
@@ -14,6 +14,7 @@ import { HistoryManager, URLParam } from 'app/History';
 import { KialiIcon } from 'config/KialiIcon';
 import { TraceLimit } from 'components/Metrics/TraceLimit';
 import { infoStyle } from 'styles/IconStyle';
+import { serverConfig } from '../../config';
 
 export interface QuerySettings {
   errorsOnly: boolean;
@@ -76,7 +77,7 @@ export class TracesDisplayOptions extends React.Component<Props, State> {
     const limit =
       HistoryManager.getParam(URLParam.TRACING_LIMIT_TRACES) ||
       sessionStorage.getItem(URLParam.TRACING_LIMIT_TRACES) ||
-      '100';
+      serverConfig.kialiFeatureFlags.uiDefaults.tracing.limit;
 
     const errorsOnly =
       HistoryManager.getParam(URLParam.TRACING_ERRORS_ONLY) ||
diff --git a/frontend/src/components/TracingIntegration/TracesFetcher.ts b/frontend/src/components/TracingIntegration/TracesFetcher.ts
index 54e2c18d5..07941fe68 100644
--- a/frontend/src/components/TracingIntegration/TracesFetcher.ts
+++ b/frontend/src/components/TracingIntegration/TracesFetcher.ts
@@ -5,6 +5,7 @@ import { TracingQuery } from 'types/Tracing';
 import { TargetKind } from 'types/Common';
 import { getTimeRangeMicros } from 'utils/tracing/TracingHelper';
 import { transformTraceData } from 'utils/tracing/TraceTransform';
+import { endPerfTimer, startPerfTimer } from '../../utils/PerformanceUtils';
 
 export type FetchOptions = {
   cluster?: string;
@@ -41,14 +42,27 @@ export class TracesFetcher {
       limit: o.spanLimit,
       minDuration: o.minDuration ? Math.floor(1000 * o.minDuration) : undefined
     };
-    const apiCall =
-      o.targetKind === 'app'
-        ? API.getAppTraces
-        : o.targetKind === 'service'
-        ? API.getServiceTraces
-        : API.getWorkloadTraces;
+    let perfKey: string;
+    let apiCall: typeof API.getAppTraces | typeof API.getServiceTraces | typeof API.getWorkloadTraces;
+    switch (o.targetKind) {
+      case 'app':
+        perfKey = 'AppTraces';
+        apiCall = API.getAppTraces;
+        break;
+      case 'service':
+        perfKey = 'ServiceTraces';
+        apiCall = API.getServiceTraces;
+        break;
+      default:
+        perfKey = 'WorkloadTraces';
+        apiCall = API.getWorkloadTraces;
+        break;
+    }
+
+    startPerfTimer(perfKey);
     apiCall(o.namespace, o.target, q, o.cluster)
       .then(response => {
+        endPerfTimer(perfKey);
         const newTraces = response.data.data
           ? (response.data.data
               .map(trace => transformTraceData(trace, o.cluster ? o.cluster : ''))
diff --git a/frontend/src/config/Config.ts b/frontend/src/config/Config.ts
index f181a864e..72ca4e244 100644
--- a/frontend/src/config/Config.ts
+++ b/frontend/src/config/Config.ts
@@ -187,6 +187,7 @@ const conf = {
       tracing: 'api/tracing',
       tracingDiagnose: 'api/tracing/diagnose',
       tracingErrorTraces: (namespace: string, app: string) => `api/namespaces/${namespace}/apps/${app}/errortraces`,
+      tracingTestConfig: 'api/tracing/check',
       tracingTrace: (idTrace: string) => `api/traces/${idTrace}`,
       workloadSpans: (namespace: string, workload: string) => `api/namespaces/${namespace}/workloads/${workload}/spans`,
       workloadTraces: (namespace: string, wkd: string) => `api/namespaces/${namespace}/workloads/${wkd}/traces`,
diff --git a/frontend/src/config/ServerConfig.ts b/frontend/src/config/ServerConfig.ts
index dafb55b0e..c816c31a6 100644
--- a/frontend/src/config/ServerConfig.ts
+++ b/frontend/src/config/ServerConfig.ts
@@ -120,7 +120,8 @@ const defaultServerConfig: ComputedServerConfig = {
       mesh: {
         findOptions: [],
         hideOptions: []
-      }
+      },
+      tracing: { limit: 100 }
     }
   },
   prometheus: {
diff --git a/frontend/src/pages/AppList/AppListPage.tsx b/frontend/src/pages/AppList/AppListPage.tsx
index 82c6ef4db..c184244a9 100644
--- a/frontend/src/pages/AppList/AppListPage.tsx
+++ b/frontend/src/pages/AppList/AppListPage.tsx
@@ -24,6 +24,7 @@ import { RefreshIntervalManual, RefreshIntervalPause } from 'config/Config';
 import { connectRefresh } from 'components/Refresh/connectRefresh';
 import { EmptyVirtualList } from 'components/VirtualList/EmptyVirtualList';
 import { HistoryManager } from 'app/History';
+import { startPerfTimer, endPerfTimer } from '../../utils/PerformanceUtils';
 
 type AppListPageState = FilterComponent.State<AppListItem> & {
   loaded: boolean;
@@ -118,10 +119,11 @@ class AppListPageComponent extends FilterComponent.Component<AppListPageProps, A
   }
 
   fetchApps(clusters: string[], filters: ActiveFiltersInfo, toggles: ActiveTogglesInfo, rateInterval: number): void {
+    const perfKey = 'ClustersApps';
     const appsPromises = clusters.map(cluster => {
       const health = toggles.get('health') ? 'true' : 'false';
       const istioResources = toggles.get('istioResources') ? 'true' : 'false';
-
+      startPerfTimer(perfKey);
       return API.getClustersApps(
         this.props.activeNamespaces.map(ns => ns.name).join(','),
         {
@@ -139,6 +141,7 @@ class AppListPageComponent extends FilterComponent.Component<AppListPageProps, A
         let appListItems: AppListItem[] = [];
 
         responses.forEach(response => {
+          endPerfTimer(perfKey);
           appListItems = appListItems.concat(AppListClass.getAppItems(response.data, rateInterval));
         });
 
diff --git a/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx b/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
index 20bc7b1a1..1c9d6a943 100644
--- a/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
@@ -8,7 +8,7 @@ import { TracingThunkActions } from 'actions/TracingThunkActions';
 import { router, URLParam } from '../../app/History';
 import * as API from '../../services/Api';
 import * as AlertUtils from '../../utils/AlertUtils';
-import { JaegerTrace } from 'types/TracingInfo';
+import { JaegerTrace, TracingResponse } from 'types/TracingInfo';
 import { PromisesRegistry } from 'utils/CancelablePromises';
 import { TracingQuery } from 'types/Tracing';
 import { TimeInSeconds } from 'types/Common';
@@ -21,6 +21,8 @@ import { KialiDispatch } from '../../types/Redux';
 import { isMultiCluster } from '../../config';
 import { KialiIcon } from 'config/KialiIcon';
 import { TraceLimit, TraceLimitOption } from 'components/Metrics/TraceLimit';
+import { endPerfTimer, startPerfTimer } from '../../utils/PerformanceUtils';
+import { ApiResponse } from '../../types/Api';
 
 type ReduxStateProps = {
   kiosk: string;
@@ -174,18 +176,25 @@ class SummaryPanelNodeTracesComponent extends React.Component<Props, State> {
     };
 
     const d = this.props.nodeData;
-
-    const promise = d.workload
-      ? API.getWorkloadTraces(d.namespace, d.workload, params, d.cluster)
-      : d.service
-      ? API.getServiceTraces(d.namespace, d.service, params, d.cluster)
-      : API.getAppTraces(d.namespace, d.app!, params, d.cluster);
+    let perfKey: string;
+    let promise: Promise<ApiResponse<TracingResponse>>;
+    if (d.workload) {
+      perfKey = 'WorkloadTraces';
+      promise = API.getWorkloadTraces(d.namespace, d.workload, params, d.cluster);
+    } else if (d.service) {
+      perfKey = 'ServiceTraces';
+      promise = API.getServiceTraces(d.namespace, d.service, params, d.cluster);
+    } else {
+      perfKey = 'AppTraces';
+      promise = API.getAppTraces(d.namespace, d.app!, params, d.cluster);
+    }
 
     this.promises.cancelAll();
-
+    startPerfTimer(perfKey);
     this.promises
       .register('traces', promise)
       .then(response => {
+        endPerfTimer(perfKey);
         const traces = response.data.data
           ? (response.data.data
               .map(trace => transformTraceData(trace, this.props.nodeData.cluster))
diff --git a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx
index 8716bef79..ffc376877 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx
@@ -78,7 +78,7 @@ export const isRemoteCluster = (annotations?: { [key: string]: string }): boolea
 // TODO: Should these remain fixed values?
 const direction: DirectionType = 'outbound';
 
-const helpPopover = (
+export const helpPopover = (
   header: string,
   configDescription: React.ReactNode,
   popoverRef: React.RefObject<any>
diff --git a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx
index 6ad96706e..723f8403a 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx
@@ -12,7 +12,7 @@ import { panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle';
 import { useKialiTranslation } from 'utils/I18nUtils';
 import { UNKNOWN } from 'types/Graph';
 import { TargetPanelEditor } from './TargetPanelEditor';
-import { TracingDiagnose } from '../../../components/Mesh/TraceDiagnose';
+import { TracingDiagnose } from '../../../components/Mesh/TracingDiagnose';
 
 type TargetPanelNodeProps<T extends MeshNodeData> = TargetPanelCommonProps & {
   target: NodeTarget<T>;
@@ -35,10 +35,12 @@ export const TargetPanelNode: React.FC<TargetPanelNodeProps<MeshNodeData>> = (
     <div id="target-panel-node" className={classes(panelStyle, targetPanelStyle)}>
       <div className={panelHeadingStyle}>{renderNodeHeader(data, { nameOnly: isExternal(data.cluster) })}</div>
       <div className={targetBodyStyle}>
-        <span>{t('Version: {{version}}', { version: data.version || t(UNKNOWN) })}</span>
-        {data.infraType === MeshInfraType.TRACE_STORE && (
-          <TracingDiagnose cluster={data.cluster} config={data.infraData} />
-        )}
+        <div style={{ display: 'flex' }}>
+          <span>{t('Version: {{version}}', { version: data.version || t(UNKNOWN) })}</span>
+          {data.infraType === MeshInfraType.TRACE_STORE && (
+            <TracingDiagnose cluster={data.cluster} configData={data.infraData} />
+          )}
+        </div>
         {targetPanelHR}
 
         <TargetPanelEditor configData={data.infraData} targetName={data.infraName}></TargetPanelEditor>
diff --git a/frontend/src/pages/ServiceList/ServiceListPage.tsx b/frontend/src/pages/ServiceList/ServiceListPage.tsx
index 336088acc..fb20f8be7 100644
--- a/frontend/src/pages/ServiceList/ServiceListPage.tsx
+++ b/frontend/src/pages/ServiceList/ServiceListPage.tsx
@@ -27,6 +27,7 @@ import { connectRefresh } from 'components/Refresh/connectRefresh';
 import { RefreshIntervalManual, RefreshIntervalPause } from 'config/Config';
 import { EmptyVirtualList } from 'components/VirtualList/EmptyVirtualList';
 import { HistoryManager } from 'app/History';
+import { endPerfTimer, startPerfTimer } from '../../utils/PerformanceUtils';
 
 type ServiceListPageState = FilterComponent.State<ServiceListItem> & {
   loaded: boolean;
@@ -163,8 +164,10 @@ class ServiceListPageComponent extends FilterComponent.Component<
     toggles: ActiveTogglesInfo,
     rateInterval: number
   ): void {
-    const servicesPromises = clusters.map(cluster =>
-      API.getClustersServices(
+    const perfKey = 'ClustersServices';
+    const servicesPromises = clusters.map(cluster => {
+      startPerfTimer(perfKey);
+      return API.getClustersServices(
         this.props.activeNamespaces.map(ns => ns.name).join(','),
         {
           health: toggles.get('health') ?? true,
@@ -173,8 +176,8 @@ class ServiceListPageComponent extends FilterComponent.Component<
           onlyDefinitions: toggles.get('configuration') !== undefined ? !toggles.get('configuration') : false // !configuration => onlyDefinitions
         },
         cluster
-      )
-    );
+      );
+    });
 
     this.promises
       .registerAll('services', servicesPromises)
@@ -182,6 +185,7 @@ class ServiceListPageComponent extends FilterComponent.Component<
         let serviceListItems: ServiceListItem[] = [];
 
         responses.forEach(response => {
+          endPerfTimer(perfKey);
           serviceListItems = serviceListItems.concat(this.getServiceItem(response.data, rateInterval));
         });
 
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx b/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
index 7e411fc83..5da019907 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
@@ -1,6 +1,8 @@
 import * as React from 'react';
 import {
   Alert,
+  AlertActionCloseButton,
+  AlertVariant,
   Button,
   ButtonVariant,
   Card,
@@ -17,6 +19,8 @@ import {
   MenuGroup,
   MenuToggle,
   MenuToggleElement,
+  Modal,
+  Tab,
   TextInput,
   Toolbar,
   ToolbarGroup,
@@ -32,7 +36,14 @@ import { AccessLog, LogEntry, LogType, Pod, PodLogs } from '../../types/IstioObj
 import { getPodLogs, getWorkloadSpans, setPodEnvoyProxyLogLevel } from '../../services/Api';
 import { PromisesRegistry } from '../../utils/CancelablePromises';
 import { ToolbarDropdown } from '../../components/Dropdown/ToolbarDropdown';
-import { evalTimeRange, isEqualTimeRange, TimeInMilliseconds, TimeInSeconds, TimeRange } from '../../types/Common';
+import {
+  evalTimeRange,
+  isEqualTimeRange,
+  Theme,
+  TimeInMilliseconds,
+  TimeInSeconds,
+  TimeRange
+} from '../../types/Common';
 import { RenderComponentScroll } from '../../components/Nav/Page';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { KialiIcon } from '../../config/KialiIcon';
@@ -48,7 +59,7 @@ import { Span, TracingQuery } from 'types/Tracing';
 import moment from 'moment';
 import { formatDuration } from 'utils/tracing/TracingHelper';
 import { kebabToggleStyle } from 'styles/DropdownStyles';
-import { isValid } from 'utils/Common';
+import { download, isValid } from 'utils/Common';
 import { KioskElement } from '../../components/Kiosk/KioskElement';
 import { TimeDurationModal } from '../../components/Time/TimeDurationModal';
 import { TimeDurationIndicator } from '../../components/Time/TimeDurationIndicator';
@@ -60,6 +71,13 @@ import { TraceSpansLimit } from 'components/Metrics/TraceSpansLimit';
 import { infoStyle } from 'styles/IconStyle';
 import { WaypointInfo } from '../../types/Workload';
 import { istioProxyName } from './WorkloadDetailsPage';
+import AceEditor from 'react-ace';
+import { t } from 'i18next';
+import { ParameterizedTabs } from 'components/Tab/Tabs';
+import { basicTabStyle } from 'styles/TabStyles';
+import { classes } from 'typestyle';
+import { istioAceEditorStyle } from 'styles/AceEditorStyle';
+import 'ace-builds/src-noconflict/mode-json';
 
 const appContainerColors = [PFColors.Blue200, PFColors.Blue300, PFColors.Blue400, PFColors.Blue100];
 const proxyContainerColor = PFColors.Gold300;
@@ -68,10 +86,18 @@ const spanColor = PFColors.Cyan300;
 
 type ReduxProps = {
   kiosk: string;
+  theme: string;
   timeRange: TimeRange;
   tracingIntegration: boolean;
 };
 
+const defaultTab = 'json';
+
+const tabIndex: { [tab: string]: number } = {
+  json: 0,
+  table: 1
+};
+
 export type WorkloadPodLogsProps = ReduxProps & {
   cluster?: string;
   lastRefreshAt: TimeInMilliseconds;
@@ -101,11 +127,14 @@ type Entry = {
 interface WorkloadPodLogsState {
   accessLogModals: Map<string, AccessLog>;
   containerOptions?: ContainerOption[];
+  currentTab: string;
   entries: Entry[];
   fullscreen: boolean;
   hideError?: string;
   hideLogValue: string;
+  isJSONModalOpen: boolean;
   isTimeOptionsOpen: boolean;
+  jsonModalContent: string | undefined;
   kebabOpen: boolean;
   linesTruncatedContainers: string[];
   loadingLogs: boolean;
@@ -115,6 +144,7 @@ interface WorkloadPodLogsState {
   podValue?: number;
   showClearHideLogButton: boolean;
   showClearShowLogButton: boolean;
+  showCopyMessage?: boolean;
   showError?: string;
   showLogValue: string;
   showSpans: boolean;
@@ -137,6 +167,8 @@ enum LogLevel {
   Critical = 'critical'
 }
 
+const defaultJSONIndentation = 2;
+
 const NoLogsFoundMessage = 'No container logs found for the time period.';
 
 const MaxLinesDefault = 3000;
@@ -151,6 +183,32 @@ const MaxLinesOptions = {
   '25000': '25000 lines'
 };
 
+const modalStyle = kialiStyle({
+  display: 'flex',
+  flexDirection: 'column',
+  width: '50%',
+  height: '70%',
+  $nest: {
+    '& .pf-v5-c-tab-content': {
+      height: '100%',
+      overflowY: 'auto'
+    }
+  }
+});
+
+const previewLogLineStyle = kialiStyle({
+  fontFamily: 'monospace',
+  fontSize: 'var(--kiali-global--font-size)',
+  backgroundColor: PFColors.Black1000,
+  color: PFColors.Blue200,
+  marginBottom: '1rem',
+  overflowX: 'auto',
+  overflowY: 'hidden',
+  whiteSpace: 'nowrap',
+  resize: 'none',
+  padding: '0.75rem'
+});
+
 const alInfoIcon = kialiStyle({
   display: 'flex',
   width: '0.75rem'
@@ -239,6 +297,12 @@ const spansLabelStyle = kialiStyle({
   fontWeight: 'bold'
 });
 
+const jsonTableStyle = kialiStyle({
+  padding: '0.5rem',
+  verticalAlign: 'top',
+  borderBottom: '1px solid #eee'
+});
+
 const colorCheck = (color: string): string =>
   kialiStyle({
     accentColor: color
@@ -259,6 +323,14 @@ const logsHeight = (showToolbar: boolean, fullscreen: boolean, showMaxLinesWarni
   };
 };
 
+const tabStyle = kialiStyle({
+  $nest: {
+    '&& .pf-v5-c-tabs__list': {
+      marginLeft: 0
+    }
+  }
+});
+
 export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsProps, WorkloadPodLogsState> {
   private promises: PromisesRegistry = new PromisesRegistry();
   private podOptions: string[] = [];
@@ -276,10 +348,13 @@ export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsPro
 
     const defaultState = {
       accessLogModals: new Map<string, AccessLog>(),
+      currentTab: defaultTab,
       entries: [],
       fullscreen: false,
       hideLogValue: '',
+      isJSONModalOpen: false,
       isTimeOptionsOpen: false,
+      jsonModalContent: undefined,
       kebabOpen: false,
       linesTruncatedContainers: [],
       loadingLogs: false,
@@ -506,6 +581,7 @@ export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsPro
                     )}
                     {this.getLogsDiv()}
                     {this.getAccessLogModals()}
+                    {this.getJSONLogModal()}
                   </CardBody>
                 </Card>
               </GridItem>
@@ -636,6 +712,12 @@ export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsPro
     );
   };
 
+  private downloadFile = (): void => {
+    if (this.state.jsonModalContent) {
+      download(this.state.jsonModalContent, `${this.props.workload}.json`);
+    }
+  };
+
   private toggleSelected = (c: ContainerOption): void => {
     c.isSelected = !c.isSelected;
     this.setState({ containerOptions: [...this.state.containerOptions!] });
@@ -706,6 +788,23 @@ export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsPro
 
     return !le.accessLog ? (
       <div key={`le-d-${index}`} className={logLineStyle} style={{ ...style }}>
+        {this.isJSON(e) && (
+          <Tooltip
+            key={`jod-tt-${index}`}
+            position={TooltipPosition.auto}
+            entryDelay={1000}
+            content="Click for JSON object details"
+          >
+            <Button
+              key={`jod-b-${index}`}
+              variant={ButtonVariant.plain}
+              className={logInfoStyle}
+              onClick={() => this.openJSONModal(e)}
+            >
+              <KialiIcon.Info key={`jod-i-${index}`} className={alInfoIcon} color={messageColor} />
+            </Button>
+          </Tooltip>
+        )}
         <p key={`le-${index}`} className={logMessageStyle} style={{ color: messageColor }}>
           {this.entryToString(e)}
         </p>
@@ -912,6 +1011,98 @@ export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsPro
     );
   };
 
+  private renderTabs = (): React.ReactNode[] => {
+    const theme = this.props.theme;
+    const jsonTab = (
+      <Tab eventKey={0} title={t('JSON')} key="json">
+        <AceEditor
+          mode="json"
+          theme={theme === Theme.DARK ? 'twilight' : 'eclipse'}
+          value={this.state.jsonModalContent}
+          className={istioAceEditorStyle}
+          name="json-details-viewer"
+          readOnly={true}
+          width="100%"
+          height="100%"
+          setOptions={{
+            useWorker: false,
+            showLineNumbers: true,
+            tabSize: 2,
+            showFoldWidgets: true
+          }}
+          editorProps={{ $blockScrolling: true }}
+        />
+      </Tab>
+    );
+
+    const tableTab = (
+      <Tab eventKey={1} title={t('Table')} key="table" data-test="json-table-tab">
+        {this.state.jsonModalContent && this.renderTableFromJson(JSON.parse(this.state.jsonModalContent))}
+      </Tab>
+    );
+
+    return [jsonTab, tableTab];
+  };
+
+  private getJSONLogModal = (): React.ReactNode => {
+    return (
+      <Modal
+        data-test="json-modal"
+        className={modalStyle}
+        disableFocusTrap={true}
+        title={t('JSON Log Entry')}
+        isOpen={this.state.isJSONModalOpen}
+        onClose={this.closeJSONModal}
+        actions={[
+          <Button key="close" onClick={this.closeJSONModal}>
+            {t('Close')}
+          </Button>,
+          this.state.jsonModalContent && (
+            <CopyToClipboard
+              key="copy"
+              onCopy={() => this.setState({ showCopyMessage: !this.state.showCopyMessage })}
+              text={this.state.jsonModalContent}
+            >
+              <Button variant={ButtonVariant.secondary}>{t('Copy')}</Button>
+            </CopyToClipboard>
+          ),
+          <Button key="download" variant={ButtonVariant.secondary} onClick={this.downloadFile}>
+            {t('Download')}
+          </Button>
+        ]}
+      >
+        {this.state.showCopyMessage && (
+          <Alert
+            style={{ marginBottom: '20px' }}
+            title={t('The JSON Log entry has been copied to your clipboard.')}
+            variant={AlertVariant.success}
+            isInline={true}
+            actionClose={
+              <AlertActionCloseButton onClose={() => this.setState({ showCopyMessage: !this.state.showCopyMessage })} />
+            }
+          />
+        )}
+        <p className={previewLogLineStyle}>{this.state.jsonModalContent}</p>
+        <div style={{ height: 'calc(100% - 120px)' }}>
+          <ParameterizedTabs
+            id="json-log-details-tabs"
+            className={classes(basicTabStyle, tabStyle)}
+            onSelect={tabValue => {
+              this.setState({ currentTab: tabValue });
+            }}
+            tabMap={tabIndex}
+            defaultTab={defaultTab}
+            activeTab={this.state.currentTab}
+            mountOnEnter={true}
+            unmountOnExit={true}
+          >
+            {this.renderTabs()}
+          </ParameterizedTabs>
+        </div>
+      </Modal>
+    );
+  };
+
   private getAccessLogModals = (): React.ReactNode[] => {
     const modals: React.ReactNode[] = [];
     let i = 0;
@@ -1362,6 +1553,59 @@ export class WorkloadPodLogsComponent extends React.Component<WorkloadPodLogsPro
     return `duration: ${formatDuration(duration)}, operationName: ${operationName}`;
   };
 
+  private entryToJSON = (entry: Entry): string | undefined => {
+    if (entry.logEntry) {
+      return JSON.stringify(JSON.parse(entry.logEntry.message), null, defaultJSONIndentation);
+    }
+    return undefined;
+  };
+
+  private isJSON = (entry: Entry): boolean => {
+    try {
+      if (entry.logEntry) {
+        const parsed = JSON.parse(entry.logEntry.message);
+        return typeof parsed === 'object' && parsed !== null;
+      }
+      return false;
+    } catch (e) {
+      return false;
+    }
+  };
+
+  private renderTableFromJson = (data: unknown): JSX.Element | null => {
+    // Limited to objects, not arrays of objects
+    if (typeof data === 'object' && data !== null) {
+      return (
+        <table data-test="parsed-json-table" style={{ width: '100%', tableLayout: 'fixed' }}>
+          <colgroup>
+            <col style={{ width: '30%' }} />
+            <col style={{ width: '70%' }} />
+          </colgroup>
+          <tbody>
+            {Object.entries(data as Record<string, unknown>).map(([key, value]) => (
+              <tr key={key}>
+                <td className={jsonTableStyle} style={{ fontWeight: 'bold' }}>
+                  {key}
+                </td>
+                <td className={jsonTableStyle}>{typeof value === 'object' ? JSON.stringify(value) : String(value)}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    return null;
+  };
+
+  openJSONModal = (jsonEntry: Entry): void => {
+    this.setState({ isJSONModalOpen: true, jsonModalContent: this.entryToJSON(jsonEntry) });
+  };
+
+  closeJSONModal = (): void => {
+    this.setState({ isJSONModalOpen: false, jsonModalContent: undefined });
+  };
+
   private hasEntries = (entries: Entry[]): boolean => !!entries && entries.length > 0;
 }
 
@@ -1375,7 +1619,8 @@ const mapStateToProps = (state: KialiAppState): ReduxProps => {
   return {
     kiosk: state.globalState.kiosk,
     timeRange: timeRangeSelector(state),
-    tracingIntegration: state.tracingState.info?.integration ?? false
+    tracingIntegration: state.tracingState.info?.integration ?? false,
+    theme: state.globalState.theme
   };
 };
 
diff --git a/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx b/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx
index 482ce4af2..2a4bfe2a1 100644
--- a/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx
+++ b/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx
@@ -11,6 +11,7 @@ import { Dropdown, DropdownItem } from '@patternfly/react-core';
 import { KialiIcon } from 'config/KialiIcon';
 
 const defaultProps = (): WorkloadPodLogsProps => ({
+  theme: '',
   kiosk: '',
   lastRefreshAt: 200,
   timeRange: {},
diff --git a/frontend/src/pages/WorkloadList/WorkloadListPage.tsx b/frontend/src/pages/WorkloadList/WorkloadListPage.tsx
index 294c8cfaa..37b189369 100644
--- a/frontend/src/pages/WorkloadList/WorkloadListPage.tsx
+++ b/frontend/src/pages/WorkloadList/WorkloadListPage.tsx
@@ -27,6 +27,7 @@ import { connectRefresh } from 'components/Refresh/connectRefresh';
 import { RefreshIntervalManual, RefreshIntervalPause } from 'config/Config';
 import { EmptyVirtualList } from 'components/VirtualList/EmptyVirtualList';
 import { HistoryManager } from 'app/History';
+import { endPerfTimer, startPerfTimer } from '../../utils/PerformanceUtils';
 
 type WorkloadListPageState = FilterComponent.State<WorkloadListItem> & {
   loaded: boolean;
@@ -167,10 +168,11 @@ class WorkloadListPageComponent extends FilterComponent.Component<
     toggles: ActiveTogglesInfo,
     rateInterval: number
   ): void {
+    const perfKey = 'ClustersWorkloads';
     const workloadsConfigPromises = clusters.map(cluster => {
       const health = toggles.get('health') ? 'true' : 'false';
       const istioResources = toggles.get('istioResources') ? 'true' : 'false';
-
+      startPerfTimer(perfKey);
       return API.getClustersWorkloads(
         this.props.activeNamespaces.map(ns => ns.name).join(','),
         {
@@ -188,6 +190,7 @@ class WorkloadListPageComponent extends FilterComponent.Component<
         let workloadsItems: WorkloadListItem[] = [];
 
         responses.forEach(response => {
+          endPerfTimer(perfKey);
           workloadsItems = workloadsItems.concat(this.getDeploymentItems(response.data));
         });
 
diff --git a/frontend/src/services/Api.ts b/frontend/src/services/Api.ts
index 412b7b76e..2c75d981e 100644
--- a/frontend/src/services/Api.ts
+++ b/frontend/src/services/Api.ts
@@ -47,7 +47,13 @@ import {
   ZtunnelConfigDump
 } from '../types/IstioObjects';
 import { ComponentStatus, IstiodResourceThresholds } from '../types/IstioStatus';
-import { TracingCheck, TracingInfo, TracingResponse, TracingSingleResponse } from '../types/TracingInfo';
+import {
+  ConfigurationValidation,
+  TracingCheck,
+  TracingInfo,
+  TracingResponse,
+  TracingSingleResponse
+} from '../types/TracingInfo';
 import { ControlPlane, MeshDefinition, MeshQuery } from '../types/Mesh';
 import { DashboardQuery, IstioMetricsOptions, MetricsStatsQuery } from '../types/MetricsOptions';
 import { IstioMetricsMap, MetricsPerNamespace, MetricsStatsResult, ResourceUsageMetricsMap } from '../types/Metrics';
@@ -1409,3 +1415,11 @@ export const getDiagnoseStatus = (cluster?: string): Promise<ApiResponse<Tracing
 
   return newRequest<TracingCheck>(HTTP_VERBS.GET, urls.tracingDiagnose, queryParams, {});
 };
+
+export const checkTracingConfig = (config: string, cluster?: string): Promise<ApiResponse<ConfigurationValidation>> => {
+  const queryParams: ClusterParam = {};
+  if (cluster) {
+    queryParams.clusterName = cluster;
+  }
+  return newRequest<ConfigurationValidation>(HTTP_VERBS.POST, urls.tracingTestConfig, queryParams, config);
+};
diff --git a/frontend/src/services/GraphDataSource.ts b/frontend/src/services/GraphDataSource.ts
index 6293d887c..ef4babc54 100644
--- a/frontend/src/services/GraphDataSource.ts
+++ b/frontend/src/services/GraphDataSource.ts
@@ -20,6 +20,8 @@ import { decorateGraphData } from '../store/Selectors/GraphData';
 import EventEmitter from 'eventemitter3';
 import { createSelector } from 'reselect';
 import { isMultiCluster, serverConfig } from '../config';
+import { startPerfTimer, endPerfTimer } from '../utils/PerformanceUtils';
+import { capitalize } from '../utils/Common';
 
 export const EMPTY_GRAPH_DATA = { nodes: [], edges: [] };
 const PROMISE_KEY = 'CURRENT_REQUEST';
@@ -530,9 +532,11 @@ export class GraphDataSource {
 
   private fetchDataForNamespaces = (restParams: GraphElementsQuery): void => {
     restParams.namespaces = this.fetchParameters.namespaces.map(namespace => namespace.name).join(',');
-
+    const perfKey = 'GraphNamespaces';
+    startPerfTimer(perfKey);
     this.promiseRegistry.register(PROMISE_KEY, API.getGraphElements(restParams)).then(
       response => {
+        endPerfTimer(perfKey);
         const responseData: any = response.data;
         this.graphElements = responseData && responseData.elements ? responseData.elements : EMPTY_GRAPH_DATA;
         this.graphTimestamp = responseData && responseData.timestamp ? responseData.timestamp : 0;
@@ -549,6 +553,7 @@ export class GraphDataSource {
         );
       },
       error => {
+        endPerfTimer(perfKey);
         this._isLoading = false;
 
         if (error.isCanceled) {
@@ -564,10 +569,13 @@ export class GraphDataSource {
   };
 
   private fetchDataForNode = (restParams: GraphElementsQuery, cluster?: string): void => {
+    const perfKey = `Graph${capitalize(this.fetchParameters.node!.nodeType)}`;
+    startPerfTimer(perfKey);
     this.promiseRegistry
       .register(PROMISE_KEY, API.getNodeGraphElements(this.fetchParameters.node!, restParams, cluster))
       .then(
         response => {
+          endPerfTimer(perfKey);
           const responseData: any = response.data;
           this.graphElements = responseData && responseData.elements ? responseData.elements : EMPTY_GRAPH_DATA;
           this.graphTimestamp = responseData && responseData.timestamp ? responseData.timestamp : 0;
@@ -584,6 +592,7 @@ export class GraphDataSource {
           );
         },
         error => {
+          endPerfTimer(perfKey);
           this._isLoading = false;
 
           if (error.isCanceled) {
diff --git a/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts b/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
index a72f00555..ad15b7bd5 100644
--- a/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
+++ b/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
@@ -116,7 +116,8 @@ export const serverRateConfig = {
       },
       metricsPerRefresh: '1m',
       namespaces: [],
-      refreshInterval: '15s'
+      refreshInterval: '15s',
+      tracing: { limit: 100 }
     }
   },
   healthConfig: {
diff --git a/frontend/src/types/IstioStatus.ts b/frontend/src/types/IstioStatus.ts
index 76ba77594..bc2a8300b 100644
--- a/frontend/src/types/IstioStatus.ts
+++ b/frontend/src/types/IstioStatus.ts
@@ -26,7 +26,7 @@ export const statusSeverity: Record<Status, number> = {
 
 export interface ComponentStatus {
   cluster: string;
-  is_core: boolean;
+  isCore: boolean;
   name: string;
   status: Status;
 }
diff --git a/frontend/src/types/ServerConfig.ts b/frontend/src/types/ServerConfig.ts
index bf9454396..5ea51514d 100644
--- a/frontend/src/types/ServerConfig.ts
+++ b/frontend/src/types/ServerConfig.ts
@@ -64,6 +64,10 @@ interface MeshUIDefaults {
   hideOptions: GraphFindOption[];
 }
 
+interface TracingDefaults {
+  limit: number;
+}
+
 interface UIDefaults {
   graph: GraphUIDefaults;
   i18n: I18nUIDefaults;
@@ -72,6 +76,7 @@ interface UIDefaults {
   metricsPerRefresh?: string;
   namespaces?: string[];
   refreshInterval?: string;
+  tracing: TracingDefaults;
 }
 
 interface KialiFeatureFlags {
diff --git a/frontend/src/types/StatusState.ts b/frontend/src/types/StatusState.ts
index d0a5057e0..c62fbb413 100644
--- a/frontend/src/types/StatusState.ts
+++ b/frontend/src/types/StatusState.ts
@@ -14,9 +14,9 @@ export enum TempoUrlFormat {
 export type Status = { [K in StatusKey]?: string };
 
 export type TempoConfig = {
-  datasource_uid: string;
-  org_id: string;
-  url_format: TempoUrlFormat;
+  datasourceUID: string;
+  orgID: string;
+  urlFormat: TempoUrlFormat;
 };
 
 export interface ExternalServiceInfo {
diff --git a/frontend/src/types/TracingInfo.ts b/frontend/src/types/TracingInfo.ts
index 739402209..8c7f79e2b 100644
--- a/frontend/src/types/TracingInfo.ts
+++ b/frontend/src/types/TracingInfo.ts
@@ -153,3 +153,8 @@ export type TracingCheck = {
   message: string;
   validConfig?: ValidConfig[];
 };
+
+export type ConfigurationValidation = {
+  error?: string;
+  message: string;
+};
diff --git a/frontend/src/types/__testData__/HealthConfig.ts b/frontend/src/types/__testData__/HealthConfig.ts
index 0d06c9e3b..445388ae6 100644
--- a/frontend/src/types/__testData__/HealthConfig.ts
+++ b/frontend/src/types/__testData__/HealthConfig.ts
@@ -50,7 +50,8 @@ export const healthConfig = {
       },
       metricsPerRefresh: '1m',
       namespaces: [],
-      refreshInterval: '15s'
+      refreshInterval: '15s',
+      tracing: { limit: 100 }
     }
   },
   healthConfig: {
diff --git a/frontend/src/utils/Common.ts b/frontend/src/utils/Common.ts
index 836cb73aa..e6f4975cb 100644
--- a/frontend/src/utils/Common.ts
+++ b/frontend/src/utils/Common.ts
@@ -71,3 +71,7 @@ export const download = (text: string, fileName: string): void => {
   document.body.appendChild(element); // Required for this to work in FireFox
   element.click();
 };
+
+export const capitalize = (str: string): string => {
+  return str.charAt(0).toUpperCase() + str.slice(1);
+};
diff --git a/frontend/src/utils/PerformanceUtils.ts b/frontend/src/utils/PerformanceUtils.ts
new file mode 100644
index 000000000..72e17ece1
--- /dev/null
+++ b/frontend/src/utils/PerformanceUtils.ts
@@ -0,0 +1,149 @@
+export interface PerformanceTimingsStore {
+  [id: string]: number; // Stores start time (milliseconds) for each ID
+}
+
+export interface PerformanceMetricStats {
+  allTimesMs: number[];
+  avgTimeMs: number;
+  count: number;
+  id: string;
+  maxTimeMs: number;
+  medianTimeMs: number;
+  minTimeMs: number;
+  totalTimeMs: number;
+}
+
+export interface SimplifiedPerfStats {
+  avgTimeMs: number;
+  maxTimeMs: number;
+  medianTimeMs: number;
+  minTimeMs: number;
+}
+
+export interface AllPerformanceStatsStore {
+  [id: string]: PerformanceMetricStats;
+}
+
+const activeTimers: PerformanceTimingsStore = {};
+const collectedStats: AllPerformanceStatsStore = {};
+
+/**
+ * Helper function to calculate the median from an array of numbers.
+ * @param times Array of numbers (durations).
+ * @returns The median value.
+ */
+const calculateMedian = (times: number[]): number => {
+  if (!times || times.length === 0) {
+    return 0;
+  }
+
+  const sortedTimes = [...times].sort((a, b) => a - b);
+  const mid = Math.floor(sortedTimes.length / 2);
+
+  if (sortedTimes.length % 2 === 0) {
+    return (sortedTimes[mid - 1] + sortedTimes[mid]) / 2;
+  }
+  return sortedTimes[mid];
+};
+
+/**
+ * Starts a timer for a given measurement ID.
+ * @param id A unique identifier for this measurement.
+ */
+export const startPerfTimer = (id: string): void => {
+  activeTimers[id] = Date.now();
+};
+
+/**
+ * Ends a timer for a given measurement ID and fixes the duration.
+ * @param id The unique identifier for the measurement used with startPerfTimer.
+ */
+export const endPerfTimer = (id: string): void => {
+  const startTime = activeTimers[id];
+  if (startTime === undefined) {
+    return;
+  }
+  activeTimers[id] = Date.now() - startTime; // Override the value with the duration
+
+  // Initialize stats for this ID if it's the first time
+  if (!collectedStats[id]) {
+    collectedStats[id] = {
+      id: id,
+      count: 0,
+      totalTimeMs: 0,
+      minTimeMs: Infinity, // Initialize min to a very high value
+      maxTimeMs: -Infinity, // Initialize max to a very low value
+      avgTimeMs: 0,
+      medianTimeMs: 0,
+      allTimesMs: []
+    };
+  }
+
+  // Update statistics
+  const stats = collectedStats[id];
+  stats.count++;
+  stats.totalTimeMs += activeTimers[id];
+  stats.minTimeMs = Math.min(stats.minTimeMs, activeTimers[id]);
+  stats.maxTimeMs = Math.max(stats.maxTimeMs, activeTimers[id]);
+  stats.avgTimeMs = Math.round(stats.totalTimeMs / stats.count);
+  stats.allTimesMs.push(activeTimers[id]);
+  stats.medianTimeMs = Math.round(calculateMedian(stats.allTimesMs));
+
+  delete activeTimers[id]; // Clean up the active timer
+};
+
+/**
+ * Retrieves a read-only copy of the statistics for a specific ID.
+ * @param id The request ID.
+ * @returns A copy of the statistics object, or undefined if no stats for that ID.
+ */
+export const getPerfStatsForId = (id: string): { [key: string]: SimplifiedPerfStats } | undefined => {
+  if (collectedStats[id]) {
+    const stats = collectedStats[id];
+    const reportStats: SimplifiedPerfStats = {
+      minTimeMs: stats.minTimeMs,
+      maxTimeMs: stats.maxTimeMs,
+      avgTimeMs: stats.avgTimeMs,
+      medianTimeMs: stats.medianTimeMs
+    };
+    const result: { [key: string]: SimplifiedPerfStats } = {};
+    result[id] = reportStats;
+    return result;
+  }
+  return undefined; // No stats found for this request ID
+};
+
+/**
+ * Retrieves all collected performance statistics, with each ID mapping to its
+ * simplified statistics (min, max, avg, median).
+ * This version calls the modified getPerfStatsForId in a loop.
+ *
+ * @returns An object where keys are request IDs and values are their simplified statistics.
+ */
+export const getAllPerfStats = (): { [key: string]: SimplifiedPerfStats } => {
+  const allSimplifiedStatsResult: { [key: string]: SimplifiedPerfStats } = {};
+
+  for (const id in collectedStats) {
+    Object.assign(allSimplifiedStatsResult, getPerfStatsForId(id));
+  }
+  return allSimplifiedStatsResult;
+};
+
+/**
+ * Resets (clears) the statistics for a specific ID.
+ * @param id The performance timer ID.
+ */
+export const resetPerfStatsForId = (id: string): void => {
+  if (collectedStats[id]) {
+    delete collectedStats[id];
+  }
+};
+
+/**
+ * Resets all collected performance statistics.
+ */
+export const resetAllPerfStats = (): void => {
+  for (const id in collectedStats) {
+    delete collectedStats[id];
+  }
+};
diff --git a/frontend/src/utils/tracing/UrlProviders/Tempo.ts b/frontend/src/utils/tracing/UrlProviders/Tempo.ts
index 51275e137..0ecdcae0d 100644
--- a/frontend/src/utils/tracing/UrlProviders/Tempo.ts
+++ b/frontend/src/utils/tracing/UrlProviders/Tempo.ts
@@ -41,11 +41,11 @@ export class TempoUrlProvider implements TracingUrlProvider {
     let frontendProvider: TracingUrlProvider | undefined = undefined;
     const svc = externalServices.find(s => [GRAFANA, JAEGER].includes(s.name.toLowerCase()));
     if (svc && svc.name.toLowerCase() === GRAFANA && svc.url !== undefined) {
-      if (service.tempoConfig?.datasource_uid !== undefined) {
+      if (service.tempoConfig?.datasourceUID !== undefined) {
         // Grafana 10+
         frontendProvider = new GrafanaUrlProvider(svc.url, {
-          datasource_uid: service.tempoConfig.datasource_uid,
-          orgID: service.tempoConfig.org_id
+          datasource_uid: service.tempoConfig.datasourceUID,
+          orgID: service.tempoConfig.orgID
         });
       } else {
         // Fallback to older Grafana URL schema
diff --git a/frontend/src/utils/tracing/UrlProviders/index.ts b/frontend/src/utils/tracing/UrlProviders/index.ts
index a474e06cc..405268c58 100644
--- a/frontend/src/utils/tracing/UrlProviders/index.ts
+++ b/frontend/src/utils/tracing/UrlProviders/index.ts
@@ -22,7 +22,7 @@ export function GetTracingUrlProvider(
   // the wanted url provider
   let urlProvider: TracingUrlProvider | undefined = undefined;
   if (isTempoService(svc)) {
-    if (svc.tempoConfig?.url_format === TempoUrlFormat.JAEGER) {
+    if (svc.tempoConfig?.urlFormat === TempoUrlFormat.JAEGER) {
       urlProvider = new JaegerUrlProvider(svc);
     } else {
       urlProvider = new TempoUrlProvider(svc, externalServices);
diff --git a/go.mod b/go.mod
index b2476f4ea..2c0c1b0ca 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/kiali/kiali
 
-go 1.23.6
+go 1.24.2
 
 require (
 	github.com/Masterminds/semver/v3 v3.3.1
@@ -9,40 +9,40 @@ require (
 	github.com/go-logr/zerologr v1.2.3
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang/protobuf v1.5.4
-	github.com/google/go-cmp v0.6.0
+	github.com/google/go-cmp v0.7.0
 	github.com/gorilla/mux v1.8.1
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/nitishm/engarde v0.1.1
-	github.com/onsi/ginkgo/v2 v2.21.0
-	github.com/onsi/gomega v1.35.1
+	github.com/onsi/ginkgo/v2 v2.22.0
+	github.com/onsi/gomega v1.36.1
 	github.com/openshift/api v0.0.0-20240109042830-44756aa36879
 	github.com/openshift/client-go v0.0.0-20240109161853-2425b4b6d3b3
 	github.com/prometheus/client_golang v1.19.1
 	github.com/prometheus/common v0.55.0
 	github.com/rs/zerolog v1.34.0
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.10.0
 	go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.47.0
-	go.opentelemetry.io/otel v1.31.0
+	go.opentelemetry.io/otel v1.34.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0
-	go.opentelemetry.io/otel/sdk v1.31.0
-	go.opentelemetry.io/otel/trace v1.31.0
+	go.opentelemetry.io/otel/sdk v1.34.0
+	go.opentelemetry.io/otel/trace v1.34.0
 	go.uber.org/automaxprocs v1.5.3
-	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
-	golang.org/x/oauth2 v0.23.0
-	golang.org/x/sync v0.10.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576
-	google.golang.org/grpc v1.69.0
-	google.golang.org/protobuf v1.36.0
+	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
+	golang.org/x/oauth2 v0.25.0
+	golang.org/x/sync v0.13.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422
+	google.golang.org/grpc v1.71.1
+	google.golang.org/protobuf v1.36.6
 	gopkg.in/yaml.v2 v2.4.0
 	istio.io/api v1.25.0
 	istio.io/client-go v1.25.0
-	k8s.io/api v0.32.1
-	k8s.io/apimachinery v0.32.1
-	k8s.io/client-go v0.32.1
-	sigs.k8s.io/controller-runtime v0.19.1
-	sigs.k8s.io/gateway-api v1.2.1
+	k8s.io/api v0.32.3
+	k8s.io/apimachinery v0.32.3
+	k8s.io/client-go v0.32.3
+	sigs.k8s.io/controller-runtime v0.20.4
+	sigs.k8s.io/gateway-api v1.3.0
 )
 
 require (
@@ -51,7 +51,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
-	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
@@ -62,6 +62,7 @@ require (
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
+	github.com/google/btree v1.1.3 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
@@ -83,32 +84,33 @@ require (
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rs/xid v1.6.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/vjeantet/grok v1.0.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	go.opentelemetry.io/otel/metric v1.31.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/otel/metric v1.34.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.3.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.uber.org/zap v1.26.0 // indirect
-	golang.org/x/crypto v0.31.0 // indirect
-	golang.org/x/net v0.33.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
-	golang.org/x/text v0.21.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/crypto v0.37.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/term v0.31.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
-	golang.org/x/tools v0.26.0 // indirect
+	golang.org/x/tools v0.30.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.31.1 // indirect
+	k8s.io/apiextensions-apiserver v0.32.3 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
 	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
 
diff --git a/go.sum b/go.sum
index 7375aa658..c4e68f0dc 100644
--- a/go.sum
+++ b/go.sum
@@ -17,10 +17,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
 github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
-github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
-github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
+github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
+github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@@ -51,11 +51,13 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
 github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -107,10 +109,10 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/nitishm/engarde v0.1.1 h1:+hT7EGP64L1wKnIv64xzu94oSR3n+SFqcsDHiGDcTVQ=
 github.com/nitishm/engarde v0.1.1/go.mod h1:nlzhv5c2J4WL9e/Y8sebkZ2PJBY790US8d+Z/vS/qqY=
-github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
-github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
-github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
-github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
+github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
+github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/openshift/api v0.0.0-20240109042830-44756aa36879 h1:9JfgDtOi0be+p2HsXwIahS8rXlt4MQRj6Vlpmx1DDg8=
 github.com/openshift/api v0.0.0-20240109042830-44756aa36879/go.mod h1:qNtV0315F+f8ld52TLtPvrfivZpdimOzTi3kn9IVbtU=
 github.com/openshift/client-go v0.0.0-20240109161853-2425b4b6d3b3 h1:XQv/z/1tedDGPeplmowftfUUrR3BtglN6Hv5kLzy1CU=
@@ -130,21 +132,21 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
 github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
 github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/vjeantet/grok v1.0.0 h1:uxMqatJP6MOFXsj6C1tZBnqqAThQEeqnizUZ48gSJQQ=
 github.com/vjeantet/grok v1.0.0/go.mod h1:/FWYEVYekkm+2VjcFmO9PufDU5FgXHUz9oy2EGqmQBo=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -152,24 +154,26 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.47.0 h1:yPWywmjyhn5C64Z7OLdIfjnbwOQF/Xz89HNqSVquC2E=
 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.47.0/go.mod h1:jk2INQzOTr9e27FwMs2JVXXttZc/3bucJX/7l3YVfbw=
-go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
-go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
-go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
-go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
-go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
-go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
-go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
-go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
-go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
-go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
 go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
 go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
@@ -178,17 +182,17 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
-golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -201,17 +205,17 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
+golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -225,23 +229,23 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
 golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -250,22 +254,22 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
-golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
+golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
 gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
-google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
-google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
-google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
-google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
-google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
-google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -281,27 +285,29 @@ istio.io/api v1.25.0 h1:UVQNFUAz4R06NagA+QpEzLmi4sw+m+UuMgD6sIyXawA=
 istio.io/api v1.25.0/go.mod h1:QFzEXv/IT582T0FHZVp1QoolvE4ws0zz/vVO55blmlE=
 istio.io/client-go v1.25.0 h1:MMG8r1g+o2yEzVwi0AGSdB6EQyLEhYz8YMYQQIw8IZw=
 istio.io/client-go v1.25.0/go.mod h1:Y4qIjNIVCkpZMiq/e3C7tMjOm7aYUbxT6ebtmai1TRY=
-k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
-k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
-k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
-k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
-k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
-k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
-k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
+k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
+k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
+k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
+k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss=
+k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
+k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
+k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk=
-sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
-sigs.k8s.io/gateway-api v1.2.1 h1:fZZ/+RyRb+Y5tGkwxFKuYuSRQHu9dZtbjenblleOLHM=
-sigs.k8s.io/gateway-api v1.2.1/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
+sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
+sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
+sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
+sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
-sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
-sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016 h1:kXv6kKdoEtedwuqMmkqhbkgvYKeycVbC8+iPCP9j5kQ=
+sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
+sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/hack/ci-kind-molecule-tests.sh b/hack/ci-kind-molecule-tests.sh
index 9b662d11d..d52f6a6d7 100755
--- a/hack/ci-kind-molecule-tests.sh
+++ b/hack/ci-kind-molecule-tests.sh
@@ -403,10 +403,64 @@ fi
 # if requested, install OLM and the Kiali Operator via OLM
 if [ "${OLM_ENABLED}" == "true" ]; then
   if [ "${OLM_VERSION}" == "latest" ]; then
-    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)"
+    # Try for 2 hours (120 attempts * 60 seconds)
+    curl_output=""
+    OLM_VERSION=""
+    for i in {1..120}; do
+      infomsg "Attempt $i/120: Attempting to get the latest OLM version from GitHub..."
+
+      # Try to get the releases from GitHub
+      curl_output=$(curl -s https://api.github.com/repos/operator-framework/operator-lifecycle-manager/releases 2>/dev/null)
+      curl_exit_code=$?
+
+      # Check if curl succeeded
+      if [ $curl_exit_code -ne 0 ]; then
+        if [ $i -lt 120 ]; then
+          infomsg "Retry $i/120: curl command failed with exit code [$curl_exit_code], retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # Check if we got a non-empty response
+      if [ -z "$curl_output" ]; then
+        if [ $i -lt 120 ]; then
+          infomsg "Retry $i/120: curl returned empty response, retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # Check if the response looks like valid JSON (basic check)
+      if ! echo "$curl_output" | grep -q '"tag_name"'; then
+        if [ $i -lt 120 ]; then
+          infomsg "Retry $i/120: curl response does not contain expected JSON structure, retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # Try to extract the OLM version
+      OLM_VERSION_TEMP="$(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)"
+
+      # Check if version extraction succeeded
+      if [ -z "${OLM_VERSION_TEMP}" ]; then
+        if [ $i -lt 120 ]; then
+          infomsg "Retry $i/120: failed to extract latest OLM version from GitHub response, retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # If we got here, everything worked
+      OLM_VERSION="${OLM_VERSION_TEMP}"
+      infomsg "Successfully obtained latest OLM version from GitHub: ${OLM_VERSION}"
+      break
+    done
+
+    # Final check - if we still don't have a version after all retries, fail
     if [ -z "${OLM_VERSION}" ]; then
-      infomsg "Failed to obtain the latest OLM version from Github."
+      infomsg "Failed to obtain the latest OLM version from GitHub after 120 attempts over 2 hours."
       exit 1
     else
       infomsg "Github reports the latest OLM version is: ${OLM_VERSION}"
@@ -415,13 +469,64 @@ if [ "${OLM_ENABLED}" == "true" ]; then
       infomsg "Using the specified OLM version: ${OLM_VERSION}"
   fi
 
+  # Download the OLM install script with retry logic (2 hours, 60 seconds between retries)
+  infomsg "Downloading OLM install script..."
+  for i in {1..120}; do
+    olm_install_script=""
+    infomsg "Attempt $i/120: Downloading OLM install script from GitHub..."
+
+    # Try to download the OLM install script
+    olm_install_script=$(curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/${OLM_VERSION}/install.sh 2>/dev/null)
+    curl_exit_code=$?
+
+    # Check if curl succeeded
+    if [ $curl_exit_code -ne 0 ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl command failed with exit code [$curl_exit_code], retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if we got a non-empty response
+    if [ -z "$olm_install_script" ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl returned empty response, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if the response looks like a valid shell script (basic check)
+    if ! echo "$olm_install_script" | grep -q "#!/bin/bash\|#!/bin/sh\|bash\|kubectl"; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: downloaded content does not appear to be a valid shell script, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # If we got here, everything worked
+    infomsg "Successfully downloaded OLM install script from GitHub"
+    break
+  done
+
+  # Final check - if we still don't have the script after all retries, fail
+  if [ -z "${olm_install_script:-}" ]; then
+    infomsg "Failed to download OLM install script from GitHub after 120 attempts over 2 hours."
+    exit 1
+  fi
+
   # force the install.sh script to go through our client executable when it executes kubectl commands
   kubectl() {
     ${CLIENT_EXE} "$@"
   }
   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}
+
+  # Run the downloaded script (no retry - fail immediately if it fails)
+  infomsg "Running OLM install script..."
+  echo "$olm_install_script" | bash -s ${OLM_VERSION}
   [ "$?" != "0" ] && infomsg "ERROR: Failed to install OLM" && exit 1
   unset -f kubectl
 
diff --git a/hack/istio/ambient/install-sidecars-ambient.sh b/hack/istio/ambient/install-sidecars-ambient.sh
index e4aeb6fd7..11c9e4cb2 100755
--- a/hack/istio/ambient/install-sidecars-ambient.sh
+++ b/hack/istio/ambient/install-sidecars-ambient.sh
@@ -121,7 +121,7 @@ NAD
 if [ "${WAYPOINT}" == "true" ]; then
   echo "Verifying that Gateway API is installed; if it is not then it will be installed now."
   $CLIENT_EXE get crd gateways.gateway.networking.k8s.io &> /dev/null || \
-    { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | $CLIENT_EXE apply -f -; }
+    { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | $CLIENT_EXE apply -f -; }
   ${ISTIOCTL} waypoint apply -n ${AMBIENT_NS} --enroll-namespace
 fi
 
diff --git a/hack/istio/ambient/install-waypoints.sh b/hack/istio/ambient/install-waypoints.sh
index 8785c62e2..2bc08226d 100755
--- a/hack/istio/ambient/install-waypoints.sh
+++ b/hack/istio/ambient/install-waypoints.sh
@@ -76,7 +76,7 @@ fi
 # Verify Gateway API
 echo "Verifying that Gateway API is installed; if it is not then it will be installed now."
 $CLIENT_EXE get crd gateways.gateway.networking.k8s.io &> /dev/null || \
-  { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | $CLIENT_EXE apply -f -; }
+  { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | $CLIENT_EXE apply -f -; }
 
 ${CLIENT_EXE} create ns waypoint-forservice
 ${CLIENT_EXE} create ns waypoint-forworkload
diff --git a/hack/istio/download-istio.sh b/hack/istio/download-istio.sh
index a3962ea8b..fed86eddc 100755
--- a/hack/istio/download-istio.sh
+++ b/hack/istio/download-istio.sh
@@ -64,16 +64,21 @@ echo "Output Directory: ${OUTPUT_DIR}"
 
 if [ -z "${ISTIO_VERSION}" ]; then
   if [ -z "${DEV_ISTIO_VERSION}" ]; then
-    # get the latest released version
-    while [ -z "${VERSION_WE_WANT:-}" ]; do
-      VERSION_WE_WANT=$(curl -L -s https://api.github.com/repos/istio/istio/releases | \
+    # get the latest released version with retry logic
+    echo "Getting the latest Istio version from GitHub..."
+    for attempt in $(seq 1 120); do
+      VERSION_WE_WANT=$(curl -L -s https://api.github.com/repos/istio/istio/releases 2>/dev/null | \
             grep tag_name | sed -e 's/.*://' -e 's/ *"//' -e 's/",//' | \
             grep -v -E "(snapshot|alpha|beta|rc)\.[0-9]$" | sort -t"." -k 1.2g,1 -k 2g,2 -k 3g | tail -n 1)
-      if [ -z "${VERSION_WE_WANT:-}" ]; then
-        echo "Cannot get the latest Istio version from github. Will try again"
-        sleep 2
+      if [ $? -eq 0 ] && [ -n "${VERSION_WE_WANT}" ] && [ "${VERSION_WE_WANT}" != "null" ]; then
+        echo "Successfully retrieved the latest Istio version: [$VERSION_WE_WANT]"
+        break
+      elif [ ${attempt} -eq 120 ]; then
+        echo "ERROR: Failed to get the latest Istio version from GitHub after 120 attempts (2 hours). Giving up."
+        exit 1
       else
-        echo "Will use the latest Istio version: [$VERSION_WE_WANT]"
+        echo "WARNING: Failed to get the latest Istio version from GitHub. Will retry in 60 seconds... (attempt ${attempt}/120)"
+        sleep 60
       fi
     done
     ISTIO_VERSION="${VERSION_WE_WANT}"
@@ -104,15 +109,29 @@ if [ ! -d "./istio-${VERSION_WE_WANT}" ]; then
        exit 1
     fi
     VERSION_TO_MATCH="${major}.${minor}"
-    LATEST=$(curl -s "https://api.github.com/repos/istio/istio/releases" \
-     | jq -r --arg VERSION_TO_MATCH "$VERSION_TO_MATCH" '.[] | select(.tag_name | startswith($VERSION_TO_MATCH)) | .tag_name' \
-     | sort -V \
-     | tail -n 1)
+    # Add retry logic for the second GitHub API call
+    echo "Getting the latest patch version for [${VERSION_TO_MATCH}]..."
+    for attempt in $(seq 1 120); do
+      LATEST=$(curl -L -s https://api.github.com/repos/istio/istio/releases 2>/dev/null \
+       | jq -r --arg VERSION_TO_MATCH "$VERSION_TO_MATCH" '.[] | select(.tag_name | startswith($VERSION_TO_MATCH)) | .tag_name' \
+       | sort -V \
+       | tail -n 1)
+      if [ $? -eq 0 ] && [ -n "${LATEST}" ] && [ "${LATEST}" != "null" ]; then
+        echo "Successfully retrieved the latest patch version [${LATEST}]"
+        break
+      elif [ ${attempt} -eq 120 ]; then
+        echo "ERROR: Failed to get the latest patch version for [${VERSION_TO_MATCH}] from GitHub after 120 attempts (2 hours). Giving up."
+        exit 1
+      else
+        echo "WARNING: Failed to get the latest patch version from GitHub. Will retry in 60 seconds... (attempt ${attempt}/120)"
+        sleep 60
+      fi
+    done
     if [[ -z ${LATEST} ]]; then
       echo "Couldn't find the latest version"
       exit 1
     fi
-    echo "Will use Istio ${LATEST}"
+    echo "Will use Istio [${LATEST}]"
     VERSION_WE_WANT=${LATEST}
     ISTIO_VERSION=${LATEST}
   fi
diff --git a/hack/istio/install-bookinfo-demo.sh b/hack/istio/install-bookinfo-demo.sh
index b7b60b29b..e576e2516 100755
--- a/hack/istio/install-bookinfo-demo.sh
+++ b/hack/istio/install-bookinfo-demo.sh
@@ -201,7 +201,7 @@ if [ "${AUTO_INJECTION}" == "false" -a "${MANUAL_INJECTION}" == "false" ]; then
       # Verify Gateway API
       echo "Verifying that Gateway API is installed; if it is not then it will be installed now."
       $CLIENT_EXE get crd gateways.gateway.networking.k8s.io &> /dev/null || \
-        { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | $CLIENT_EXE apply -f -; }
+        { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | $CLIENT_EXE apply -f -; }
       break
     fi
   done
diff --git a/hack/istio/install-error-rates-demo.sh b/hack/istio/install-error-rates-demo.sh
index ece0d7e0e..804abdd7e 100755
--- a/hack/istio/install-error-rates-demo.sh
+++ b/hack/istio/install-error-rates-demo.sh
@@ -191,7 +191,7 @@ if [ "${AMBIENT_ENABLED}" == "true" ]; then
     # Verify Gateway API
     echo "Verifying that Gateway API is installed; if it is not then it will be installed now."
     $CLIENT_EXE get crd gateways.gateway.networking.k8s.io &> /dev/null || \
-      { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | $CLIENT_EXE apply -f -; }
+      { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | $CLIENT_EXE apply -f -; }
     # Create Waypoint proxy
     echo "Create Waypoint proxy"
     ${ISTIOCTL} x waypoint apply -n ${NAMESPACE_ALPHA}
diff --git a/hack/istio/install-loggers-demo.sh b/hack/istio/install-loggers-demo.sh
index 57d15040b..07c09dec1 100755
--- a/hack/istio/install-loggers-demo.sh
+++ b/hack/istio/install-loggers-demo.sh
@@ -90,7 +90,7 @@ if [ "${DELETE_DEMO}" == "false" ]; then
       $CLIENT_EXE label namespace ${NAMESPACE} istio.io/dataplane-mode=ambient --overwrite
   fi
 
-  echo "Deploying logger for Bookinfo"
+  echo "Deploying custom logger"
   cat <<EOF | $CLIENT_EXE apply -f -
 apiVersion: apps/v1
 kind: Deployment
@@ -114,6 +114,31 @@ spec:
         args:
           - while true; do echo 'GET'; echo 'DEBUG'; sleep 1; done
 EOF
+
+  echo "Deploying json logger demo"
+  cat <<EOF | $CLIENT_EXE apply -f -
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: json-logger
+  namespace: $NAMESPACE
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: json-logger
+  template:
+    metadata:
+      labels:
+        app: json-logger
+    spec:
+      containers:
+      - name: logger
+        image: busybox
+        command: ["/bin/sh", "-c"]
+        args:
+          - while true; do echo "{\"a\":\"b\", \"c\":{\"d\":\"e\"}}"; sleep 1; echo "text format log"; done
+EOF
   
   sleep 5
 else
diff --git a/hack/istio/install-travel-agency-demo.sh b/hack/istio/install-travel-agency-demo.sh
index 7346f5850..6cee531c4 100755
--- a/hack/istio/install-travel-agency-demo.sh
+++ b/hack/istio/install-travel-agency-demo.sh
@@ -151,7 +151,7 @@ do
      # Verify Gateway API
      echo "Verifying that Gateway API is installed; if it is not then it will be installed now."
      $CLIENT_EXE get crd gateways.gateway.networking.k8s.io &> /dev/null || \
-       { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | $CLIENT_EXE apply -f -; }
+       { $CLIENT_EXE kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | $CLIENT_EXE apply -f -; }
      break
    fi
 done
diff --git a/hack/istio/kustomization/bookinfo-ppc64le.yaml b/hack/istio/kustomization/bookinfo-ppc64le.yaml
index 0a9d47d18..8f3558fa4 100644
--- a/hack/istio/kustomization/bookinfo-ppc64le.yaml
+++ b/hack/istio/kustomization/bookinfo-ppc64le.yaml
@@ -3,27 +3,27 @@ resources:
 images:
 - name: docker.io/istio/examples-bookinfo-details-v1
   newName: quay.io/maistra/examples-bookinfo-details-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-details-2
   newName: quay.io/maistra/examples-bookinfo-details-v2
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-ratings-v1
   newName: quay.io/maistra/examples-bookinfo-ratings-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-ratings-v2
   newName: quay.io/maistra/examples-bookinfo-ratings-v2
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-reviews-v1
   newName: quay.io/maistra/examples-bookinfo-reviews-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-reviews-v2
   newName: quay.io/maistra/examples-bookinfo-reviews-v2
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-reviews-v3
   newName: quay.io/maistra/examples-bookinfo-reviews-v3
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-productpage-v1
   newName: quay.io/maistra/examples-bookinfo-productpage-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 sortOptions:
   order: fifo
diff --git a/hack/istio/kustomization/bookinfo-s390x.yaml b/hack/istio/kustomization/bookinfo-s390x.yaml
index 0a9d47d18..8f3558fa4 100644
--- a/hack/istio/kustomization/bookinfo-s390x.yaml
+++ b/hack/istio/kustomization/bookinfo-s390x.yaml
@@ -3,27 +3,27 @@ resources:
 images:
 - name: docker.io/istio/examples-bookinfo-details-v1
   newName: quay.io/maistra/examples-bookinfo-details-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-details-2
   newName: quay.io/maistra/examples-bookinfo-details-v2
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-ratings-v1
   newName: quay.io/maistra/examples-bookinfo-ratings-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-ratings-v2
   newName: quay.io/maistra/examples-bookinfo-ratings-v2
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-reviews-v1
   newName: quay.io/maistra/examples-bookinfo-reviews-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-reviews-v2
   newName: quay.io/maistra/examples-bookinfo-reviews-v2
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-reviews-v3
   newName: quay.io/maistra/examples-bookinfo-reviews-v3
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 - name: docker.io/istio/examples-bookinfo-productpage-v1
   newName: quay.io/maistra/examples-bookinfo-productpage-v1
-  newTag: 1.24.2-ibm
+  newTag: 1.26.2-ibm
 sortOptions:
   order: fifo
diff --git a/hack/istio/multicluster/env.sh b/hack/istio/multicluster/env.sh
index d3b3f8d89..c48ea820f 100755
--- a/hack/istio/multicluster/env.sh
+++ b/hack/istio/multicluster/env.sh
@@ -442,7 +442,7 @@ Valid command line arguments:
                               (Default: true)
   -kni|--kind-node-image: Image of the kind cluster. Defaults to latest kind image if not specified.
   -kshc|--kiali-server-helm-charts <path>: If specified, must be the path to a Kiali server helm charts tarball. If not
-                                           specified, the latest published helm charts is used. (Default: kiali-server)
+                                           specified, the latest published helm charts is used.
   -kudi|--kiali-use-dev-image: If "true" the local dev image of Kiali will be pushed and used in the Kiali deployment.
                                The local dev image must be tagged as "quay.io/kiali/kiali:dev" prior to running this script;
                                that will be the image pushed to the clusters. You can "make container-build-kiali" to build it.
@@ -606,6 +606,10 @@ if [ "${KIALI_USE_DEV_IMAGE}" == "true" ]; then
     # e.g. /source/helm-charts/_output/charts/kiali-server-1.64.0-SNAPSHOT.tgz
     KIALI_SERVER_HELM_CHARTS="${KIALI_SERVER_HELM_CHARTS:-kiali-server}"
   fi
+else
+  if [ -z "${KIALI_SERVER_HELM_CHARTS:-}" ]; then
+    KIALI_SERVER_HELM_CHARTS="kiali/kiali-server"
+  fi
 fi
 
 # Export all variables so child scripts pick them up
diff --git a/hack/istio/multicluster/install-multi-primary.sh b/hack/istio/multicluster/install-multi-primary.sh
index 3453ae907..f000e1786 100755
--- a/hack/istio/multicluster/install-multi-primary.sh
+++ b/hack/istio/multicluster/install-multi-primary.sh
@@ -249,9 +249,12 @@ printf "%s" "${REMOTE_SECRET}" | ${CLIENT_EXE} apply -f -
 ${CLIENT_EXE} patch svc prometheus -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CONTEXT} -p "{\"spec\": {\"type\": \"LoadBalancer\"}}"
 
 WEST_PROMETHEUS_ADDRESS=$(${CLIENT_EXE} --context=${CLUSTER2_CONTEXT} -n ${ISTIO_NAMESPACE} get svc prometheus -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
-sed -i "s/WEST_PROMETHEUS_ADDRESS/$WEST_PROMETHEUS_ADDRESS/g" ${SCRIPT_DIR}/prometheus.yaml
-${CLIENT_EXE} apply -f ${SCRIPT_DIR}/prometheus.yaml -n ${ISTIO_NAMESPACE} --context ${CLUSTER1_CONTEXT} 
-sed -i "s/$WEST_PROMETHEUS_ADDRESS/WEST_PROMETHEUS_ADDRESS/g" ${SCRIPT_DIR}/prometheus.yaml
+if [ -z "${WEST_PROMETHEUS_ADDRESS}" ]; then
+  echo "WARNING! Prometheus not updated - cannot determine the west prometheus load balancer ingress IP"
+else
+  ## TODO: prometheus.yaml has CLUSTER_NAME in it that should also be searched-replaced, but that is not done here
+  cat ${SCRIPT_DIR}/prometheus.yaml | sed -e "s/WEST_PROMETHEUS_ADDRESS/$WEST_PROMETHEUS_ADDRESS/g" | ${CLIENT_EXE} apply -n ${ISTIO_NAMESPACE} --context ${CLUSTER1_CONTEXT} -f -
+fi
 
 # Configure Tracing "federation"
 source ${SCRIPT_DIR}/setup-tracing.sh
diff --git a/hack/istio/multicluster/install-primary-remote.sh b/hack/istio/multicluster/install-primary-remote.sh
index c8a73e38e..4f963ef87 100755
--- a/hack/istio/multicluster/install-primary-remote.sh
+++ b/hack/istio/multicluster/install-primary-remote.sh
@@ -138,9 +138,12 @@ ${CLIENT_EXE} patch svc prometheus -n ${ISTIO_NAMESPACE} --context ${CLUSTER2_CO
 
 # Configure Prometheus federation
 WEST_PROMETHEUS_ADDRESS=$(${CLIENT_EXE} --context=${CLUSTER2_CONTEXT} -n ${ISTIO_NAMESPACE} get svc prometheus -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
-sed -i "s/WEST_PROMETHEUS_ADDRESS/$WEST_PROMETHEUS_ADDRESS/g" ${SCRIPT_DIR}/prometheus.yaml
-${CLIENT_EXE} apply -f ${SCRIPT_DIR}/prometheus.yaml -n ${ISTIO_NAMESPACE} --context ${CLUSTER1_CONTEXT} 
-sed -i "s/$WEST_PROMETHEUS_ADDRESS/WEST_PROMETHEUS_ADDRESS/g" ${SCRIPT_DIR}/prometheus.yaml
+if [ -z "${WEST_PROMETHEUS_ADDRESS}" ]; then
+  echo "WARNING! Prometheus not updated - cannot determine the west prometheus load balancer ingress IP"
+else
+  ## TODO: prometheus.yaml has CLUSTER_NAME in it that should also be searched-replaced, but that is not done here
+  cat ${SCRIPT_DIR}/prometheus.yaml | sed -e "s/WEST_PROMETHEUS_ADDRESS/$WEST_PROMETHEUS_ADDRESS/g" | ${CLIENT_EXE} apply -n ${ISTIO_NAMESPACE} --context ${CLUSTER1_CONTEXT} -f -
+fi
 
 echo "==== DEPLOY ISTIO INGRESS GATEWAY ON CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
 kubectl --context "${CLUSTER1_CONTEXT}" apply -n "${ISTIO_NAMESPACE}" -f "${SCRIPT_DIR}/../istio-gateway.yaml"
diff --git a/hack/istio/multicluster/setup-external-controlplane.sh b/hack/istio/multicluster/setup-external-controlplane.sh
index bdc95f4a4..2bc3a6b00 100755
--- a/hack/istio/multicluster/setup-external-controlplane.sh
+++ b/hack/istio/multicluster/setup-external-controlplane.sh
@@ -227,7 +227,7 @@ kubectl wait --context="${CTX_REMOTE_CLUSTER}" --for=condition=Ready istios/exte
 
 # There's no istio on the remote cluster so install gateway CRDs. 
 kubectl get crd gateways.gateway.networking.k8s.io --context="${CTX_REMOTE_CLUSTER}" &> /dev/null || \
-  { kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.2.0" | kubectl apply -f - --context="${CTX_REMOTE_CLUSTER}"; }
+  { kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.3.0" | kubectl apply -f - --context="${CTX_REMOTE_CLUSTER}"; }
 
 # Configure Prometheus federation
 # Open up remote prom for scraping by the centralized prom.
diff --git a/hack/istio/sail/func-olm.sh b/hack/istio/sail/func-olm.sh
index c335deffb..135d83d75 100755
--- a/hack/istio/sail/func-olm.sh
+++ b/hack/istio/sail/func-olm.sh
@@ -25,13 +25,58 @@ install_olm() {
   local olm_download_url="https://github.com/operator-framework/operator-lifecycle-manager/releases/download/${OLM_VERSION}/install.sh"
   infomsg "Will install OLM now. Downloading installer from: ${olm_download_url}"
 
-  if ! curl -sL ${olm_download_url} > /tmp/olm-install-script.sh; then
-    errormsg "ERROR: Unable to download the OLM install script from GitHub"
+  # Download the OLM install script with retry logic (2 hours, 60 seconds between retries)
+  infomsg "Downloading OLM install script..."
+  for i in {1..120}; do
+    local olm_install_script=""
+    infomsg "Attempt $i/120: Downloading OLM install script from GitHub..."
+
+    # Try to download the OLM install script
+    olm_install_script=$(curl -sL ${olm_download_url} 2>/dev/null)
+    local curl_exit_code=$?
+
+    # Check if curl succeeded
+    if [ $curl_exit_code -ne 0 ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl command failed with exit code [$curl_exit_code], retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if we got a non-empty response
+    if [ -z "$olm_install_script" ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl returned empty response, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if the response looks like a valid shell script (basic check)
+    if ! echo "$olm_install_script" | grep -q '#!/bin/bash\|#!/bin/sh\|bash\|kubectl'; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: downloaded content does not appear to be a valid shell script, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # If we got here, everything worked
+    infomsg "Successfully downloaded OLM install script from GitHub"
+    break
+  done
+
+  # Final check - if we still don't have the script after all retries, fail
+  if [ -z "${olm_install_script:-}" ]; then
+    errormsg "Failed to download OLM install script from GitHub after 120 attempts over 2 hours."
     exit 1
-  else
-    chmod +x /tmp/olm-install-script.sh
   fi
 
+  # Write the downloaded script to a temporary file
+  echo "$olm_install_script" > /tmp/olm-install-script.sh
+  chmod +x /tmp/olm-install-script.sh
+
   # Run the OLM install script with PATH set so it picks up our kubectl executable.
   if ! PATH="$(dirname "$OC"):$PATH" /tmp/olm-install-script.sh "${OLM_VERSION}"; then
     errormsg "ERROR: Failed to install OLM"
@@ -48,11 +93,67 @@ get_olm_version_we_want() {
     # the env var already tells us the version we want
     return
   fi
-  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)"
-	if [ -z "${OLM_VERSION}" ]; then
-	  errormsg "Failed to obtain the latest OLM version from GitHub. You will need to specify an explicit version via OLM_VERSION environment variable."
-	  exit 1
+
+  # Try for 2 hours (120 attempts * 60 seconds)
+  local curl_output=""
+  OLM_VERSION=""
+  for i in {1..120}; do
+    infomsg "Attempt $i/120: Attempting to get the latest OLM version from GitHub..."
+
+    # Try to get the releases from GitHub
+    curl_output=$(curl -s https://api.github.com/repos/operator-framework/operator-lifecycle-manager/releases 2>/dev/null)
+    local curl_exit_code=$?
+
+    # Check if curl succeeded
+    if [ $curl_exit_code -ne 0 ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl command failed with exit code [$curl_exit_code], retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if we got a non-empty response
+    if [ -z "$curl_output" ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl returned empty response, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if the response looks like valid JSON (basic check)
+    if ! echo "$curl_output" | grep -q '"tag_name"'; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: curl response does not contain expected JSON structure, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Try to extract the OLM version
+    local OLM_VERSION_TEMP="$(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)"
+
+    # Check if version extraction succeeded
+    if [ -z "${OLM_VERSION_TEMP}" ]; then
+      if [ $i -lt 120 ]; then
+        infomsg "Retry $i/120: failed to extract latest OLM version from GitHub response, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # If we got here, everything worked
+    OLM_VERSION="${OLM_VERSION_TEMP}"
+    infomsg "Successfully obtained latest OLM version from GitHub: ${OLM_VERSION}"
+    break
+  done
+
+  # Final check - if we still don't have a version after all retries, fail
+  if [ -z "${OLM_VERSION:-}" ]; then
+    errormsg "Failed to obtain the latest OLM version from GitHub after 120 attempts over 2 hours. You will need to specify an explicit version via OLM_VERSION environment variable."
+    exit 1
   else
-	  infomsg "GitHub reports the latest OLM version is: ${OLM_VERSION}"
-	fi
+    infomsg "GitHub reports the latest OLM version is: ${OLM_VERSION}"
+  fi
 }
diff --git a/hack/k8s-minikube.sh b/hack/k8s-minikube.sh
index ba8708335..ed9ec7c63 100755
--- a/hack/k8s-minikube.sh
+++ b/hack/k8s-minikube.sh
@@ -380,22 +380,128 @@ install_olm() {
   echo 'Installing OLM...'
 
   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)"
-    if [ -z "${OLM_VERSION}" ]; then
-      echo "Failed to obtain the latest OLM version from Github. You will need to specify an explicit version via --olm-version."
+    # Try for 2 hours (120 attempts * 60 seconds)
+    local curl_output=""
+    OLM_VERSION=""
+    for i in {1..120}; do
+      echo "Attempt $i/120: Attempting to get the latest OLM version from GitHub..."
+
+      # Try to get the releases from GitHub
+      curl_output=$(curl -s https://api.github.com/repos/operator-framework/operator-lifecycle-manager/releases 2>/dev/null)
+      local curl_exit_code=$?
+
+      # Check if curl succeeded
+      if [ $curl_exit_code -ne 0 ]; then
+        if [ $i -lt 120 ]; then
+          echo "Retry $i/120: curl command failed with exit code [$curl_exit_code], retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # Check if we got a non-empty response
+      if [ -z "$curl_output" ]; then
+        if [ $i -lt 120 ]; then
+          echo "Retry $i/120: curl returned empty response, retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # Check if the response looks like valid JSON (basic check)
+      if ! echo "$curl_output" | grep -q '"tag_name"'; then
+        if [ $i -lt 120 ]; then
+          echo "Retry $i/120: curl response does not contain expected JSON structure, retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # Try to extract the OLM version
+      local OLM_VERSION_TEMP="$(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)"
+
+      # Check if version extraction succeeded
+      if [ -z "${OLM_VERSION_TEMP}" ]; then
+        if [ $i -lt 120 ]; then
+          echo "Retry $i/120: failed to extract latest OLM version from GitHub response, retrying in 60 seconds..."
+          sleep 60
+        fi
+        continue
+      fi
+
+      # If we got here, everything worked
+      OLM_VERSION="${OLM_VERSION_TEMP}"
+      echo "Successfully obtained latest OLM version from GitHub: ${OLM_VERSION}"
+      break
+    done
+
+    # Final check - if we still don't have a version after all retries, fail
+    if [ -z "${OLM_VERSION:-}" ]; then
+      echo "Failed to obtain the latest OLM version from GitHub after 120 attempts over 2 hours. You will need to specify an explicit version via --olm-version."
       exit 1
     else
       echo "Github reports the latest OLM version is: ${OLM_VERSION}"
     fi
   fi
 
+  # Download the OLM install script with retry logic (2 hours, 60 seconds between retries)
+  echo "Downloading OLM install script..."
+  for i in {1..120}; do
+    local olm_install_script=""
+    echo "Attempt $i/120: Downloading OLM install script from GitHub..."
+
+    # Try to download the OLM install script
+    olm_install_script=$(curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/${OLM_VERSION}/install.sh 2>/dev/null)
+    local curl_exit_code=$?
+
+    # Check if curl succeeded
+    if [ $curl_exit_code -ne 0 ]; then
+      if [ $i -lt 120 ]; then
+        echo "Retry $i/120: curl command failed with exit code [$curl_exit_code], retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if we got a non-empty response
+    if [ -z "$olm_install_script" ]; then
+      if [ $i -lt 120 ]; then
+        echo "Retry $i/120: curl returned empty response, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # Check if the response looks like a valid shell script (basic check)
+    if ! echo "$olm_install_script" | grep -q '#!/bin/bash\|#!/bin/sh\|bash\|kubectl'; then
+      if [ $i -lt 120 ]; then
+        echo "Retry $i/120: downloaded content does not appear to be a valid shell script, retrying in 60 seconds..."
+        sleep 60
+      fi
+      continue
+    fi
+
+    # If we got here, everything worked
+    echo "Successfully downloaded OLM install script from GitHub"
+    break
+  done
+
+  # Final check - if we still don't have the script after all retries, fail
+  if [ -z "${olm_install_script:-}" ]; then
+    echo "Failed to download OLM install script from GitHub after 120 attempts over 2 hours."
+    exit 1
+  fi
+
   # force the install.sh script to go through minikube kubectl when it executes kubectl commands
   kubectl() {
     ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- $@
   }
   export MINIKUBE_EXEC_WITH_PROFILE
   export -f kubectl
-  curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/${OLM_VERSION}/install.sh | bash -s ${OLM_VERSION}
+
+  # Run the downloaded script (no retry - fail immediately if it fails)
+  echo "Running OLM install script..."
+  echo "$olm_install_script" | bash -s ${OLM_VERSION}
   [ "$?" != "0" ] && echo "ERROR: Failed to install OLM" && exit 1
   unset -f kubectl
 
diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh
index 11532eb21..5c5528279 100755
--- a/hack/run-integration-tests.sh
+++ b/hack/run-integration-tests.sh
@@ -12,13 +12,14 @@ FRONTEND_AMBIENT="frontend-ambient"
 FRONTEND_PRIMARY_REMOTE="frontend-primary-remote"
 FRONTEND_MULTI_PRIMARY="frontend-multi-primary"
 FRONTEND_TEMPO="frontend-tempo"
-ISTIO_VERSION=""
 HELM_CHARTS_DIR=""
-TEST_SUITE="${BACKEND}"
+ISTIO_VERSION=""
 SETUP_ONLY="false"
+STERN="false"
+TEMPO="false"
+TEST_SUITE="${BACKEND}"
 TESTS_ONLY="false"
 WITH_VIDEO="false"
-TEMPO="false"
 
 # process command line args
 while [[ $# -gt 0 ]]; do
@@ -40,6 +41,14 @@ while [[ $# -gt 0 ]]; do
       fi
       shift;shift
       ;;
+    -st|--stern)
+      STERN="${2}"
+      if [ "${STERN}" != "true" -a "${STERN}" != "false" ]; then
+        echo "--stern option must be one of 'true' or 'false'"
+        exit 1
+      fi
+      shift;shift
+      ;;
     -t|--tempo)
       TEMPO="${2}"
       shift;shift
@@ -79,6 +88,9 @@ Valid command line arguments:
   -so|--setup-only <true|false>
     If true, only setup the test environment and exit without running the tests.
     Default: false
+  -st|--stern <true|false> 
+    If true, will setup stern logging binary. 
+    Default: false
   -t|--tempo <true|false>
     If true, Tempo will be installed instead of Jaeger. Just for primary-remote suite
     Default: false
@@ -121,6 +133,7 @@ cat <<EOM
 HELM_CHARTS_DIR=$HELM_CHARTS_DIR
 ISTIO_VERSION=$ISTIO_VERSION
 SETUP_ONLY=$SETUP_ONLY
+STERN=$STERN
 TESTS_ONLY=$TESTS_ONLY
 TEST_SUITE=$TEST_SUITE
 WITH_VIDEO=$WITH_VIDEO
@@ -297,6 +310,13 @@ ensureMulticlusterApplicationsAreHealthy() {
   done
 }
 
+if [ "${STERN}" == "true" ]; then
+  infomsg "Downloading stern binary ..."
+  "${SCRIPT_DIR}"/stern/download-stern.sh
+  export CYPRESS_STERN="true"
+  infomsg "CYPRESS_STERN=${CYPRESS_STERN} exported."
+fi
+
 infomsg "Running ${TEST_SUITE} integration tests"
 if [ "${TEST_SUITE}" == "${BACKEND}" ]; then
   if [ "${TESTS_ONLY}" == "false" ]; then
diff --git a/hack/stern/download-stern.sh b/hack/stern/download-stern.sh
new file mode 100755
index 000000000..e469b8b61
--- /dev/null
+++ b/hack/stern/download-stern.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
+cd "$SCRIPT_DIR/"
+
+# specify grep executable
+if [[ "$OS" == "darwin" ]]; then
+  GREP_CMD="ggrep"
+else
+  GREP_CMD="grep"
+fi
+
+# Check if stern binary exists
+if [ -f "stern" ]; then
+  echo "Stern executable found."
+else
+  echo "Downloading stern executable..."
+  # Define variables
+  REPO="stern/stern"
+  ARCH="$(uname -m)"
+  OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
+
+  # Normalize ARCH for stern naming
+  if [[ "$ARCH" == "x86_64" ]]; then
+    ARCH="amd64"
+  elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
+    ARCH="arm64"
+  fi
+
+  # Normalize OS string for stern naming
+  if [[ "$OS" == "darwin" ]]; then
+    OS="darwin"
+  elif [[ "$OS" == "linux" ]]; then
+    OS="linux"
+  fi
+
+  # Get latest version tag from GitHub API
+  VERSION=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | $GREP_CMD -oP '"tag_name": "\K(.*)(?=")' | sed 's/^v//')
+
+  # Define the filename and URL
+  FILENAME="stern_${VERSION}_${OS}_${ARCH}.tar.gz"
+  URL="https://github.com/${REPO}/releases/download/v${VERSION}/${FILENAME}"
+
+  # Download and extract, clean archive
+  echo "Downloading Stern ${VERSION}..."
+  curl -LO "$URL"
+  tar -xzf "$FILENAME"
+  rm LICENSE "$FILENAME"
+
+  chmod +x ./stern
+fi
+
+# Print stern version
+echo "Stern $(./stern --version | $GREP_CMD version ) installed successfully!"
diff --git a/hack/stern/run-stern.sh b/hack/stern/run-stern.sh
new file mode 100755
index 000000000..87b3f1c26
--- /dev/null
+++ b/hack/stern/run-stern.sh
@@ -0,0 +1,80 @@
+#!/bin/bash
+
+
+# Determine where this script is and make it the cwd
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
+cd "$SCRIPT_DIR"
+
+# Default variables for the script
+STOP="false"
+LOGFILE=""
+
+# Help
+helpmsg() {
+cat <<HELPMSG
+This script controls stern logging binary for istio (upstream) and service mesh (downstream) containers + demo apps .
+    Valid command line arguments:
+    -l|--logfile <filename.json>: Required logfile name (default: Cypress uses current .feature file name, i.e. 'apps.feature.json')
+    -s|--stop <true|false>: If true, will stop any instances of the stern spawned by this script (default: false).
+    -h|--help : this message
+HELPMSG
+}
+
+# process command line args
+while [[ $# -gt 0 ]]; do
+  key="$1"
+  case $key in
+    -l|--logfile)
+      LOGFILE="$2"
+      shift;shift
+      ;;
+    -s|--stop)
+      STOP="$2"
+      shift;shift
+      ;;
+    -h|--help)
+      helpmsg
+      exit 1
+      ;;
+    *)
+      echo "Unknown argument [$key]. Aborting."
+      exit 1
+      ;;
+  esac
+done
+
+run_stern() {
+  local resource="$1"
+  local namespace="$2"
+  bash -c "exec -a sternLogs \"$SCRIPT_DIR/stern\" $resource $namespace --tail 0 -o json --only-log-lines --max-log-requests 150 >> $LOGFILE 2>&1" &
+}
+
+if [[ "${STOP}" == "false" && "${LOGFILE}" != "" ]]; then
+  echo '{"message":"---next scenario in the feature file---"}' >> $LOGFILE
+  # core mesh containers
+  run_stern "kiali" "-n istio-system"
+  run_stern "kiali" "-n kiali-operator"
+  run_stern "istiod" "-n istio-system"
+  run_stern "istio-ingressgateway" "-n istio-system"
+
+  # bookinfo application
+  run_stern "details" "-n bookinfo"
+  run_stern "kiali-traffic-generator" "-n bookinfo"
+  run_stern "productpage" "-n bookinfo"
+  run_stern "ratings" "-n bookinfo"
+  run_stern "reviews" "-n bookinfo"
+
+  # check if stern is running, otherwise return non zero exit code, so cypress will notice and 
+  if ! pgrep -f "sternLogs" > /dev/null; then
+    echo "stern is not running. Exiting with error."
+    exit 1
+  fi
+elif [[ "${STOP}" == "true" ]]; then
+    pkill -x stern
+else
+    helpmsg
+fi
+
+
+
+
diff --git a/handlers/tracing.go b/handlers/tracing.go
index f7112fe81..0b1a6bd49 100644
--- a/handlers/tracing.go
+++ b/handlers/tracing.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -433,6 +434,46 @@ func TracingDiagnose(
 	}
 }
 
+// TracingConfigurationCheck is the API handler to test a Tracing configuration
+func TracingConfigurationCheck(
+	conf *config.Config,
+	kialiCache cache.KialiCache,
+	clientFactory kubernetes.ClientFactory,
+	prom prometheus.ClientInterface,
+	cpm business.ControlPlaneMonitor,
+	traceClientLoader func() tracing.ClientInterface,
+	grafana *grafana.Service,
+	discovery *istio.Discovery,
+) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			RespondWithError(w, http.StatusBadRequest, "Update request with bad update patch: "+err.Error())
+		}
+		testConfig := string(body)
+		var tracingConfig config.TracingConfig
+		err = json.Unmarshal([]byte(testConfig), &tracingConfig)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Tracing configuration is not valid: "+err.Error())
+			return
+		}
+
+		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			return
+		}
+		if !isHomeCPAccessible(r.Context(), conf, business.Namespace, clientFactory.GetSAHomeClusterClient().ClusterInfo().Name) {
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			return
+		}
+
+		status := business.Tracing.ValidateConfiguration(r.Context(), conf, &tracingConfig, clientFactory.GetSAHomeClusterClient().GetToken())
+		RespondWithJSON(w, http.StatusOK, status)
+	}
+}
+
 // isHomeCPAccessible Check access to the home istio namespace
 func isHomeCPAccessible(ctx context.Context, conf *config.Config, namespaceService business.NamespaceService, cluster string) bool {
 	_, err := namespaceService.GetClusterNamespace(ctx, conf.IstioNamespace, cluster)
diff --git a/istio/discovery.go b/istio/discovery.go
index 1a4fd7953..c0e4f8ae4 100644
--- a/istio/discovery.go
+++ b/istio/discovery.go
@@ -423,13 +423,13 @@ func (in *Discovery) Mesh(ctx context.Context) (*models.Mesh, error) {
 		istiods := depList.Items
 
 		if len(istiods) == 0 {
-			log.Debugf("Cluster [%s] is a remote cluster. Skipping adding a controlplane.", cluster.Name)
+			log.Tracef("Cluster [%s] is a remote cluster. Skipping adding a controlplane.", cluster.Name)
 			remoteClusters = append(remoteClusters, &cluster)
 			continue
 		}
 
 		for _, istiod := range istiods {
-			log.Debugf("Found controlplane [%s/%s] on cluster [%s].", istiod.Name, istiod.Namespace, cluster.Name)
+			log.Tracef("Found controlplane [%s/%s] on cluster [%s].", istiod.Name, istiod.Namespace, cluster.Name)
 			controlPlane := newControlPlane(istiod, &cluster)
 
 			if containers := istiod.Spec.Template.Spec.Containers; len(containers) > 0 {
@@ -758,7 +758,7 @@ func (in *Discovery) setTags(ctx context.Context, controlPlanes []models.Control
 		}
 
 		for _, webhook := range webhooks.Items {
-			log.Debugf("Found webhook [%s/%s] on cluster: [%s].", webhook.Namespace, webhook.Name, cluster)
+			log.Tracef("Found webhook [%s/%s] on cluster: [%s].", webhook.Namespace, webhook.Name, cluster)
 			tag := models.Tag{
 				Cluster:  cluster,
 				Name:     webhook.Labels[models.IstioTagLabel],
diff --git a/istio/version.go b/istio/version.go
index ef0533150..1e698b23d 100644
--- a/istio/version.go
+++ b/istio/version.go
@@ -119,7 +119,7 @@ func parseRawIstioVersion(rawVersion string) *models.ExternalServiceInfo {
 	// see if it is a released version of Istio
 	istioVersionStringArr = istioVersionExpr.FindStringSubmatch(rawVersion)
 	if istioVersionStringArr != nil {
-		log.Debugf("Detected Istio version [%v]", rawVersion)
+		log.Tracef("Detected Istio version [%v]", rawVersion)
 		if len(istioVersionStringArr) > 1 {
 			product.Name = istioProductNameUpstream
 			product.Version = istioVersionStringArr[1] // get regex group #1 ,which is the "#.#.#" version string
diff --git a/kiali.go b/kiali.go
index 7901ca581..a1be7219b 100644
--- a/kiali.go
+++ b/kiali.go
@@ -185,7 +185,7 @@ func main() {
 
 	if conf.ExternalServices.Tracing.Enabled {
 		go func() {
-			client, err := tracing.NewClient(ctx, conf, clientFactory.GetSAHomeClusterClient().GetToken())
+			client, err := tracing.NewClient(ctx, conf, clientFactory.GetSAHomeClusterClient().GetToken(), true)
 			if err != nil {
 				log.Fatalf("Error creating tracing client: %s", err)
 				return
diff --git a/kubernetes/istio.go b/kubernetes/istio.go
index eedae9112..7f4d87f4d 100644
--- a/kubernetes/istio.go
+++ b/kubernetes/istio.go
@@ -54,7 +54,7 @@ type ComponentStatus struct {
 	//
 	// example:  true
 	// required: true
-	IsCore bool `json:"is_core"`
+	IsCore bool `json:"isCore"`
 }
 
 type IstioComponentStatus []ComponentStatus
diff --git a/make/Makefile.build.mk b/make/Makefile.build.mk
index 1015068d8..b6ca55ba9 100644
--- a/make/Makefile.build.mk
+++ b/make/Makefile.build.mk
@@ -116,7 +116,7 @@ test-integration-controller: .ensure-envtest-bin-dir-exists .ensure-yq-exists
 
 ## lint-install: Installs golangci-lint
 lint-install:
-	curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(${GO} env GOPATH)/bin v1.60.1
+	curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(${GO} env GOPATH)/bin v1.64.8
 
 ## lint: Runs golangci-lint
 # doc.go is ommited for linting, because it generates lots of warnings.
diff --git a/make/Makefile.molecule.mk b/make/Makefile.molecule.mk
index e045f7ac6..d4a3619da 100644
--- a/make/Makefile.molecule.mk
+++ b/make/Makefile.molecule.mk
@@ -224,6 +224,7 @@ endif
 	      ${MOLECULE_DESTROY_NEVER_ARG} \
 	      --scenario-name $${msn}"; \
 	  echo "$$theCmd"; \
+	  success=""; \
 	  while [ $$attempt -le 60 ]; do \
 	    echo "Running molecule test: scenario=$$msn (attempt=$$attempt)"; \
 	    tmpfile=$$(mktemp); \
@@ -235,7 +236,11 @@ endif
 	      rm -f $$tmpfile; \
 	      break; \
 	    elif grep -iq "Skipping Galaxy server" "$$tmpfile"; then \
-	      echo "Molecule test failed but will retry after 60s: $$msn"; \
+	      echo "Molecule test failed! due to galaxy down but will retry after 60s: $$msn"; \
+	      attempt=$$((attempt + 1)); \
+	      sleep 60; \
+	    elif grep -Eiq 'ERROR\s+Starting galaxy' "$$tmpfile"; then \
+	      echo "Molecule test failed due to galaxy down but will retry after 60s: $$msn"; \
 	      attempt=$$((attempt + 1)); \
 	      sleep 60; \
 	    else \
diff --git a/models/metrics.go b/models/metrics.go
index b966b18f2..14925556b 100644
--- a/models/metrics.go
+++ b/models/metrics.go
@@ -232,7 +232,7 @@ func convertSampleStream(from *pmod.SampleStream, name, stat string, conversionP
 		}
 		labelSet[string(k)] = string(v)
 		if conversionParams.LabelPrefix != "" {
-			for i, _ := range labelSet {
+			for i := range labelSet {
 				labelSet[i] = fmt.Sprintf("%s (%s)", conversionParams.LabelPrefix, v)
 			}
 		}
diff --git a/routing/routes.go b/routing/routes.go
index d6082b9b1..33e3b5f9d 100644
--- a/routing/routes.go
+++ b/routing/routes.go
@@ -190,7 +190,7 @@ func NewRoutes(
 			handlers.Root(conf, clientFactory, kialiCache, grafana),
 			true,
 		},
-		// swagger:route GET /tracing/diagnose status getStatus
+		// swagger:route GET /tracing/diagnose tracing tracingDiagnose
 		// ---
 		// Endpoint to get a diagnose for the tracing endpoint
 		//
@@ -210,6 +210,26 @@ func NewRoutes(
 			handlers.TracingDiagnose(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
 			true,
 		},
+		// swagger:route GET /tracing/check tracing check config
+		// ---
+		// Endpoint to test a specific configuration for tracing
+		//
+		//     Produces:
+		//     - application/json
+		//
+		//     Schemes: http, https
+		//
+		// responses:
+		//      500: internalError
+		//      200: configurationValidation
+		{
+			"Tracing check",
+			log.TracingLogName,
+			"POST",
+			"/api/tracing/check",
+			handlers.TracingConfigurationCheck(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
+			true,
+		},
 		// swagger:route GET /config kiali getConfig
 		// ---
 		// Endpoint to get the config of Kiali
diff --git a/tracing/client.go b/tracing/client.go
index 1b77e6756..9db9cf484 100644
--- a/tracing/client.go
+++ b/tracing/client.go
@@ -41,6 +41,7 @@ type ClientInterface interface {
 	GetTraceDetail(ctx context.Context, traceId string) (*model.TracingSingleTrace, error)
 	GetErrorTraces(ctx context.Context, ns, app string, duration time.Duration) (errorTraces int, err error)
 	GetServiceStatus(ctx context.Context) (available bool, err error)
+	GetServices(ctx context.Context) ([]string, error)
 }
 
 // HTTPClientInterface for Mocks, also for Tempo or Jaeger
@@ -48,6 +49,7 @@ type HTTPClientInterface interface {
 	GetAppTracesHTTP(ctx context.Context, client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error)
 	GetTraceDetailHTTP(ctx context.Context, client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error)
 	GetServiceStatusHTTP(ctx context.Context, client http.Client, baseURL *url.URL) (bool, error)
+	GetServices(ctx context.Context, client http.Client, baseURL *url.URL) ([]string, error)
 }
 
 // GRPCClientInterface for Mocks, also for Tempo or Jaeger
@@ -55,6 +57,7 @@ type GRPCClientInterface interface {
 	FindTraces(ctx context.Context, app string, q models.TracingQuery) (response *model.TracingResponse, err error)
 	GetTrace(ctx context.Context, traceID string) (*model.TracingSingleTrace, error)
 	GetServices(ctx context.Context) (bool, error)
+	GetServicesList(ctx context.Context) ([]string, error)
 }
 
 // Client for Tracing API.
@@ -83,7 +86,8 @@ func (c *basicAuth) RequireTransportSecurity() bool {
 
 // NewClient creates a tracing Client. If it fails to create the client for any reason,
 // it will retry indefinitely until the context is cancelled.
-func NewClient(ctx context.Context, conf *config.Config, token string) (*Client, error) {
+// Unless retry is false
+func NewClient(ctx context.Context, conf *config.Config, token string, retry bool) (*Client, error) {
 	var (
 		client *Client
 		err    error
@@ -98,7 +102,9 @@ func NewClient(ctx context.Context, conf *config.Config, token string) (*Client,
 		client, err = newClient(ctx, conf, token)
 		if err != nil {
 			zl.Error().Msgf("Error creating tracing client: [%v]. Retrying in [%s]", err, newClientRetryInterval)
-			return false, nil
+			if retry {
+				return false, nil
+			}
 		}
 
 		return true, nil
@@ -298,6 +304,21 @@ func (in *Client) GetServiceStatus(ctx context.Context) (bool, error) {
 	return in.grpcClient.GetServices(ctx)
 }
 
+func (in *Client) GetServices(ctx context.Context) ([]string, error) {
+	ctx = in.prepareContextForClient(ctx)
+
+	// create a timer to time the tracing query. Note that we will always take the measurement even on failure
+	promtimer := internalmetrics.GetTracingProcessingTimePrometheusTimer("ServiceStatus")
+	defer internalmetrics.ObserveDurationAndLogResults(ctx, config.Get(), promtimer, "TracingProcessingTime", nil, "ServiceStatus")
+
+	// Check Service Status using HTTP when gRPC is not enabled
+	if in.grpcClient == nil {
+		return in.httpTracingClient.GetServices(ctx, in.httpClient, in.baseURL)
+	}
+
+	return in.grpcClient.GetServicesList(ctx)
+}
+
 // BuildTracingServiceName
 func BuildTracingServiceName(namespace, app string) string {
 	conf := config.Get()
@@ -310,7 +331,7 @@ func BuildTracingServiceName(namespace, app string) string {
 // prepareContextForClient puts things in the given context that will be needed by the client to do its job.
 // For example, the custom headers are added to the context so the clients pass them on to the server when making requests.
 func (in *Client) prepareContextForClient(ctx context.Context) context.Context {
-	if len(in.customHeaders) > 0 {
+	if in != nil && len(in.customHeaders) > 0 {
 		log.FromContext(ctx).Trace().Msgf("Adding [%v] custom headers to Tracing client", len(in.customHeaders))
 		ctx = metadata.NewOutgoingContext(ctx, metadata.New(in.customHeaders))
 	}
diff --git a/tracing/jaeger/grpc_client.go b/tracing/jaeger/grpc_client.go
index 540936b76..0ed11a7bd 100644
--- a/tracing/jaeger/grpc_client.go
+++ b/tracing/jaeger/grpc_client.go
@@ -100,6 +100,15 @@ func (jc JaegerGRPCClient) GetTrace(ctx context.Context, strTraceID string) (*mo
 }
 
 // GetServices
+func (jc JaegerGRPCClient) GetServicesList(ctx context.Context) ([]string, error) {
+	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
+	defer cancel()
+
+	services, err := jc.JaegergRPCClient.GetServices(ctx, &model.GetServicesRequest{})
+
+	return services.GetServices(), err
+}
+
 func (jc JaegerGRPCClient) GetServices(ctx context.Context) (bool, error) {
 	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
 	defer cancel()
diff --git a/tracing/jaeger/http_client.go b/tracing/jaeger/http_client.go
index efb3df721..3e903fc62 100644
--- a/tracing/jaeger/http_client.go
+++ b/tracing/jaeger/http_client.go
@@ -74,6 +74,18 @@ func (jc JaegerHTTPClient) GetServiceStatusHTTP(ctx context.Context, client http
 	return reqError == nil, reqError
 }
 
+func (jc JaegerHTTPClient) GetServices(ctx context.Context, client http.Client, baseURL *url.URL) ([]string, error) {
+	url := *baseURL
+	serviceList := model.TracingServices{}
+	url.Path = path.Join(url.Path, "/api/services")
+	r, _, reqError := otel.MakeRequest(ctx, client, url.String(), nil)
+	if errMarshal := json.Unmarshal(r, &serviceList); errMarshal != nil {
+		getLoggerFromContextHTTPJaeger(ctx).Error().Msgf("Error unmarshalling Jaeger response: %s", errMarshal)
+		return nil, errMarshal
+	}
+	return serviceList.Data, reqError
+}
+
 func queryTracesHTTP(ctx context.Context, client http.Client, u *url.URL) (*model.TracingResponse, error) {
 	// HTTP and GRPC requests co-exist, but when minDuration is present, for HTTP it requires a unit (us)
 	// https://github.com/kiali/kiali/issues/3939
diff --git a/tracing/jaeger/model/types.go b/tracing/jaeger/model/types.go
index fd775a4db..52a8ab352 100644
--- a/tracing/jaeger/model/types.go
+++ b/tracing/jaeger/model/types.go
@@ -62,6 +62,11 @@ type TracingDiagnose struct {
 	ValidConfig []ValidConfig `json:"validConfig"`
 }
 
+type ConfigurationValidation struct {
+	Error   string `json:"error"`
+	Message string `json:"message"`
+}
+
 type TracingService struct {
 	Data   []string          `json:"data"`
 	Total  int               `json:"total"`
diff --git a/tracing/tempo/grpc_client.go b/tracing/tempo/grpc_client.go
index 5bb0dd7f5..57d822994 100644
--- a/tracing/tempo/grpc_client.go
+++ b/tracing/tempo/grpc_client.go
@@ -99,6 +99,20 @@ func (jc TempoGRPCClient) GetServices(ctx context.Context) (bool, error) {
 	return err == nil, err
 }
 
+// GetServices Test an empty search to check if the service is available
+func (jc TempoGRPCClient) GetServicesList(ctx context.Context) ([]string, error) {
+	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
+	defer cancel()
+
+	sr := &tempopb.SearchRequest{}
+	result, err := jc.StreamingClient.Search(ctx, sr)
+	if err != nil {
+		return nil, err
+	}
+	services, err := processServices(ctx, result)
+	return services, err
+}
+
 // processStream
 func processStream(ctx context.Context, stream tempopb.StreamingQuerier_SearchClient, serviceName string) ([]jaegerModels.Trace, error) {
 	zl := getLoggerFromContextGRPCTempo(ctx)
@@ -125,3 +139,24 @@ func processStream(ctx context.Context, stream tempopb.StreamingQuerier_SearchCl
 	}
 	return tracesMap, nil
 }
+
+// processServices
+func processServices(ctx context.Context, stream tempopb.StreamingQuerier_SearchClient) ([]string, error) {
+	zl := getLoggerFromContextGRPCTempo(ctx)
+	services := []string{}
+
+	for received, err := stream.Recv(); err != io.EOF; received, err = stream.Recv() {
+		if err != nil {
+			if status.Code(err) == codes.DeadlineExceeded {
+				zl.Trace().Msg("[gRPC Tempo] client timeout")
+				break
+			}
+			zl.Error().Msgf("[gRPC Tempo] stream error: %v", err)
+			return nil, fmt.Errorf("[gRPC Tempo] Tracing gRPC client, stream error: %v", err)
+		}
+		for _, trace := range received.Traces {
+			services = append(services, trace.RootTraceName)
+		}
+	}
+	return services, nil
+}
diff --git a/tracing/tempo/http_client.go b/tracing/tempo/http_client.go
index 32da767b9..04275497d 100644
--- a/tracing/tempo/http_client.go
+++ b/tracing/tempo/http_client.go
@@ -136,6 +136,22 @@ func (oc *OtelHTTPClient) GetServiceStatusHTTP(ctx context.Context, client http.
 	return reqError == nil, reqError
 }
 
+// GetServiceStatusHTTP get service status
+func (oc *OtelHTTPClient) GetServices(ctx context.Context, client http.Client, baseURL *url.URL) (serviceList []string, err error) {
+	var u url.URL
+	servicesList := []string{}
+
+	u = *baseURL
+	u.Path = path.Join(u.Path, "/status/services")
+
+	r, _, reqError := otel.MakeRequest(ctx, client, u.String(), nil)
+	if errMarshal := json.Unmarshal(r, &servicesList); errMarshal != nil {
+		getLoggerFromContextHTTPTempo(ctx).Error().Msgf("Error unmarshalling Jaeger response: %s", errMarshal)
+		return nil, errMarshal
+	}
+	return servicesList, reqError
+}
+
 // queryTracesHTTP
 func (oc *OtelHTTPClient) queryTracesHTTP(ctx context.Context, client http.Client, u *url.URL, error string) (*model.TracingResponse, error) {
 



Test patch:
diff --git a/business/tracing_test.go b/business/tracing_test.go
index 6d58ed7e4..61a5d30c6 100644
--- a/business/tracing_test.go
+++ b/business/tracing_test.go
@@ -5,13 +5,34 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/kubetest"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/tracing/jaeger/model"
 	jaegerModels "github.com/kiali/kiali/tracing/jaeger/model/json"
 )
 
+func getLayer(t *testing.T, conf *config.Config) *Layer {
+
+	config.Set(conf)
+	s1 := kubetest.FakeService("Namespace", "reviews")
+	s2 := kubetest.FakeService("Namespace", "httpbin")
+	objects := []runtime.Object{
+		kubetest.FakeNamespace("Namespace"),
+		&s1,
+		&s2,
+	}
+
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	SetupBusinessLayer(t, k8s, *conf)
+	k8sclients := make(map[string]kubernetes.UserClientInterface)
+	k8sclients[conf.KubernetesConfig.ClusterName] = k8s
+	return NewWithBackends(k8sclients, kubernetes.ConvertFromUserClients(k8sclients), nil, nil)
+}
+
 var trace1 = jaegerModels.Trace{
 	Spans: []jaegerModels.Span{{
 		ProcessID:     "t1_process_1",
@@ -215,3 +236,27 @@ func TestTracesToSpanWaypointWithWorkloadFilter(t *testing.T) {
 	assert.Len(spans, 1)
 	// Process is empty here, because the span we are looking for is the service and the process is the waypoint
 }
+
+func TestValidateConfiguration(t *testing.T) {
+
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	conf.ExternalServices.Tracing.Enabled = true
+	layer := getLayer(t, conf)
+
+	tracingConfig := config.TracingConfig{Enabled: true, InternalURL: "http://localhost", UseGRPC: false}
+
+	validConfig := layer.Tracing.ValidateConfiguration(context.TODO(), config.NewConfig(), &tracingConfig, "")
+
+	assert.NotNil(validConfig)
+	assert.NotNil(validConfig.Error)
+	assert.Contains(validConfig.Error, "connection refused")
+
+	tracingConfig = config.TracingConfig{Enabled: false, InternalURL: "http://localhost", UseGRPC: false}
+	validConfig = layer.Tracing.ValidateConfiguration(context.TODO(), config.NewConfig(), &tracingConfig, "")
+
+	assert.NotNil(validConfig)
+	assert.NotNil(validConfig.Error)
+	assert.Contains(validConfig.Error, "Error creating tracing client")
+
+}
diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected
index b3a9b5cfe..a3577e94a 100644
--- a/mesh/api/testdata/test_mesh_graph.expected
+++ b/mesh/api/testdata/test_mesh_graph.expected
@@ -93,22 +93,19 @@
         "nodeType" : "infra",
         "healthData" : "Healthy",
         "infraData" : {
-          "Auth" : {
-            "CAFile" : "xxx",
-            "InsecureSkipVerify" : false,
-            "Password" : "xxx",
-            "Token" : "xxx",
-            "Type" : "none",
-            "UseKialiToken" : false,
-            "Username" : "xxx"
+          "auth" : {
+            "caFile" : "xxx",
+            "insecureSkipVerify" : false,
+            "password" : "xxx",
+            "token" : "xxx",
+            "type" : "none",
+            "useKialiToken" : false,
+            "username" : "xxx"
           },
-          "Dashboards" : null,
-          "DatasourceUID" : "",
-          "Enabled" : true,
-          "ExternalURL" : "",
-          "HealthCheckUrl" : "",
-          "InternalURL" : "http://grafana.istio-system:3000",
-          "IsCore" : false
+          "dashboards" : null,
+          "enabled" : true,
+          "externalURL" : "",
+          "internalURL" : "http://grafana.istio-system:3000"
         },
         "isExternal" : true,
         "isInaccessible" : true
@@ -124,28 +121,23 @@
         "nodeType" : "infra",
         "healthData" : "Healthy",
         "infraData" : {
-          "Auth" : {
-            "CAFile" : "xxx",
-            "InsecureSkipVerify" : false,
-            "Password" : "xxx",
-            "Token" : "xxx",
-            "Type" : "none",
-            "UseKialiToken" : false,
-            "Username" : "xxx"
+          "auth" : {
+            "caFile" : "xxx",
+            "insecureSkipVerify" : false,
+            "password" : "xxx",
+            "token" : "xxx",
+            "type" : "none",
+            "useKialiToken" : false,
+            "username" : "xxx"
           },
-          "CacheDuration" : 7,
-          "CacheEnabled" : true,
-          "CacheExpiration" : 300,
-          "CustomHeaders" : { },
-          "HealthCheckUrl" : "",
-          "IsCore" : false,
-          "QueryScope" : { },
-          "ThanosProxy" : {
-            "Enabled" : false,
-            "RetentionPeriod" : "7d",
-            "ScrapeInterval" : "30s"
+          "cacheDuration" : 7,
+          "cacheEnabled" : true,
+          "cacheExpiration" : 300,
+          "thanosProxy" : {
+            "retentionPeriod" : "7d",
+            "scrapeInterval" : "30s"
           },
-          "URL" : "http://prometheus.istio-system:9090"
+          "url" : "http://prometheus.istio-system:9090"
         },
         "isExternal" : true,
         "isInaccessible" : true
@@ -161,33 +153,28 @@
         "nodeType" : "infra",
         "healthData" : "Healthy",
         "infraData" : {
-          "Auth" : {
-            "CAFile" : "xxx",
-            "InsecureSkipVerify" : false,
-            "Password" : "xxx",
-            "Token" : "xxx",
-            "Type" : "none",
-            "UseKialiToken" : false,
-            "Username" : "xxx"
+          "auth" : {
+            "caFile" : "xxx",
+            "insecureSkipVerify" : false,
+            "password" : "xxx",
+            "token" : "xxx",
+            "type" : "none",
+            "useKialiToken" : false,
+            "username" : "xxx"
           },
-          "CustomHeaders" : { },
-          "DisableVersionCheck" : false,
-          "Enabled" : true,
-          "ExternalURL" : "",
-          "HealthCheckUrl" : "",
-          "GrpcPort" : 9095,
-          "InternalURL" : "http://tracing.istio-system:16685/jaeger",
-          "IsCore" : false,
-          "Provider" : "jaeger",
-          "TempoConfig" : {
-            "cache_capacity" : 200,
-            "cache_enabled" : true
+          "enabled" : true,
+          "externalURL" : "",
+          "grpcPort" : 9095,
+          "internalURL" : "http://tracing.istio-system:16685/jaeger",
+          "provider" : "jaeger",
+          "tempoConfig" : {
+            "cacheCapacity" : 200,
+            "cacheEnabled" : true
           },
-          "NamespaceSelector" : true,
-          "QueryScope" : { },
-          "QueryTimeout" : 5,
-          "UseGRPC" : true,
-          "WhiteListIstioSystem" : [ "jaeger-query", "istio-ingressgateway" ]
+          "namespaceSelector" : true,
+          "queryTimeout" : 5,
+          "useGRPC" : true,
+          "whiteListIstioSystem" : [ "jaeger-query", "istio-ingressgateway" ]
         },
         "isExternal" : true,
         "isInaccessible" : true
@@ -260,27 +247,20 @@
         "nodeType" : "infra",
         "healthData" : "Healthy",
         "infraData" : {
-          "ComponentStatuses" : {
-            "Enabled" : true,
-            "Components" : [ ]
+          "componentStatuses" : {
+            "enabled" : true
           },
-          "ConfigMapName" : "",
-          "EnvoyAdminLocalPort" : 15000,
-          "GatewayAPIClasses" : [ ],
-          "GatewayAPIClassesLabelSelector" : "",
-          "IstioAPIEnabled" : true,
-          "IstioIdentityDomain" : "svc.cluster.local",
-          "IstioInjectionAnnotation" : "sidecar.istio.io/inject",
-          "IstioSidecarInjectorConfigMapName" : "",
-          "IstioSidecarAnnotation" : "sidecar.istio.io/status",
-          "IstiodDeploymentName" : "",
-          "IstiodPodMonitoringPort" : 15014,
-          "IstiodPollingIntervalSeconds" : 20,
-          "Registry" : null,
-          "RootNamespace" : "istio-system",
-          "UrlServiceVersion" : "",
-          "ValidationChangeDetectionEnabled" : true,
-          "ValidationReconcileInterval" : 60000000000
+          "envoyAdminLocalPort" : 15000,
+          "istioApiEnabled" : true,
+          "istioIdentityDomain" : "svc.cluster.local",
+          "istioInjectionAnnotation" : "sidecar.istio.io/inject",
+          "istioSidecarAnnotation" : "sidecar.istio.io/status",
+          "istiodPodMonitoringPort" : 15014,
+          "istiodPollingIntervalSeconds" : 20,
+          "rootNamespace" : "istio-system",
+          "urlServiceVersion" : "",
+          "validationChangeDetectionEnabled" : true,
+          "validationReconcileInterval" : 60000000000
         }
       }
     }, {
@@ -535,4 +515,4 @@
   },
   "meshName" : "",
   "timestamp" : 1523364075
-}
+}
\ No newline at end of file
diff --git a/tests/data/k8sgateway_data.go b/tests/data/k8sgateway_data.go
index 9f3f2e56f..f13c5632f 100644
--- a/tests/data/k8sgateway_data.go
+++ b/tests/data/k8sgateway_data.go
@@ -143,7 +143,7 @@ func AddListenerToK8sGateway(listener k8s_networking_v1.Listener, gw *k8s_networ
 	return gw
 }
 
-func AddGwAddressToK8sGateway(address k8s_networking_v1.GatewayAddress, gw *k8s_networking_v1.Gateway) *k8s_networking_v1.Gateway {
+func AddGwAddressToK8sGateway(address k8s_networking_v1.GatewaySpecAddress, gw *k8s_networking_v1.Gateway) *k8s_networking_v1.Gateway {
 	gw.Spec.Addresses = append(gw.Spec.Addresses, address)
 	return gw
 }
@@ -196,8 +196,8 @@ func CreateSharedToAllListener(name string, hostname string, port int, protocol
 	return listener
 }
 
-func CreateGWAddress(addrType k8s_networking_v1.AddressType, value string) k8s_networking_v1.GatewayAddress {
-	address := k8s_networking_v1.GatewayAddress{
+func CreateGWAddress(addrType k8s_networking_v1.AddressType, value string) k8s_networking_v1.GatewaySpecAddress {
+	address := k8s_networking_v1.GatewaySpecAddress{
 		Type:  &addrType,
 		Value: value,
 	}
diff --git a/tests/integration/Dockerfile b/tests/integration/Dockerfile
index 9c2ec1613..7de4368dd 100644
--- a/tests/integration/Dockerfile
+++ b/tests/integration/Dockerfile
@@ -8,7 +8,7 @@ ENV HOME=$GOPATH/src/kiali
 
 # install required packages and prepare go dirs
 WORKDIR /bin
-RUN microdnf install -y --nodocs tar gzip make which gettext git \
+RUN microdnf install -y --nodocs tar gzip make which gettext git findutils\
     && curl -Lo ./oc.tar.gz https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable/openshift-client-linux.tar.gz \
     && tar -xf oc.tar.gz \
     && rm -f oc.tar.gz \
diff --git a/tracing/client_test.go b/tracing/client_test.go
index d638b3352..9d989b133 100644
--- a/tracing/client_test.go
+++ b/tracing/client_test.go
@@ -16,7 +16,7 @@ func TestCreateJaegerClient(t *testing.T) {
 	conf.ExternalServices.Tracing.Enabled = true
 	conf.ExternalServices.Tracing.UseGRPC = false
 
-	tracingClient, err := NewClient(context.Background(), conf, token)
+	tracingClient, err := NewClient(context.Background(), conf, token, true)
 
 	assert.Nil(t, err)
 	assert.NotNil(t, tracingClient)
@@ -28,7 +28,7 @@ func TestCreateTempogGRPCClient(t *testing.T) {
 	conf.ExternalServices.Tracing.Provider = "tempo"
 	conf.ExternalServices.Tracing.UseGRPC = true
 
-	tracingClient, err := NewClient(context.Background(), conf, token)
+	tracingClient, err := NewClient(context.Background(), conf, token, true)
 
 	assert.Nil(t, err)
 	assert.NotNil(t, tracingClient)
@@ -40,7 +40,7 @@ func TestCreateTempoHTTPClient(t *testing.T) {
 	conf.ExternalServices.Tracing.Provider = "tempo"
 	conf.ExternalServices.Tracing.UseGRPC = false
 
-	tracingClient, err := NewClient(context.Background(), conf, token)
+	tracingClient, err := NewClient(context.Background(), conf, token, true)
 
 	assert.Nil(t, err)
 	assert.NotNil(t, tracingClient)
diff --git a/tracing/tracingtest/mock.go b/tracing/tracingtest/mock.go
index b43774ee2..5846b3958 100644
--- a/tracing/tracingtest/mock.go
+++ b/tracing/tracingtest/mock.go
@@ -33,3 +33,8 @@ func (j *TracingClientMock) GetServiceStatus(ctx context.Context) (available boo
 	args := j.Called(ctx)
 	return args.Get(0).(bool), args.Error(1)
 }
+
+func (j *TracingClientMock) GetServices(ctx context.Context) (list []string, err error) {
+	args := j.Called(ctx)
+	return args.Get(0).([]string), args.Error(1)
+}