Repository: kiali/kiali

Issue:
Improve tracing tool
### What do you want to improve?
Improve the tracing tool to have a way to test the new configuration with no need to change it in the CR and wait for the deployment to get restarted. 

- Update docs: https://kiali.io/docs/configuration/debugging-kiali/#tracing-integration 

### What is the current behavior?
It is not possible to test the configuration without change it in the CR. 

Some idea: 

![Image](https://github.com/user-attachments/assets/1117e90b-5afe-49e0-bb57-2199bf4c9de5)

![Image](https://github.com/user-attachments/assets/34856c34-3bff-4298-8a0f-9fe5f280e141)

Ref. https://github.com/kiali/kiali/pull/8400

Discussion:
@josunect: ![Image](https://github.com/user-attachments/assets/e2dff2a8-44dd-40fe-90cc-277ba27a2f99)

![Image](https://github.com/user-attachments/assets/8302e9fe-2f01-458b-a5d8-93a3cbcdd7d5)

![Image](https://github.com/user-attachments/assets/56b9e814-ce1b-4ffb-a1d3-67d36671f792)@josunect: https://github.com/user-attachments/assets/874c9e6f-59e6-4e93-b10a-7d719399f6e9

@fkargbo I've added a couple of changes: 

- Move the services check to the modal: I've integrated the logs in this screen because they were shown on a modal before, with an expand field. 
- Create a new tab to modify values to check the config. I've added the Ok button to validate the config, otherwise it could show an error icon. The only question is where to put the specific error in case of incorrect configuration, maybe in a tooltip? @fkargbo: /lgtm@jshaughn: Please backport to v2.11 for inclusion into OSSM 3.1. @josunect: - Backport done: https://github.com/kiali/kiali/pull/8533 

Solution patch:
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..f343122eb 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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/integration/common/hooks.ts b/frontend/cypress/integration/common/hooks.ts
index 9d63afc5c..845a52af5 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();
+    });
   });
 });
 
diff --git a/frontend/cypress/integration/common/mesh.ts b/frontend/cypress/integration/common/mesh.ts
index 822bf30dd..e574e161a 100644
--- a/frontend/cypress/integration/common/mesh.ts
+++ b/frontend/cypress/integration/common/mesh.ts
@@ -115,6 +115,30 @@ Then('user sees cluster side panel', () => {
 Then('user sees control plane side panel', () => {
   cy.waitForReact();
   cy.get('#loading_kiali_spinner').should('not.exist');
+
+  // 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();
+  cy.wait(5000);
+
   cy.get('#target-panel-control-plane')
     .should('be.visible')
     .within(() => {
@@ -252,12 +276,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) => {
diff --git a/frontend/cypress/integration/common/workload_logs.ts b/frontend/cypress/integration/common/workload_logs.ts
index b6d0659db..236bc4da8 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('[role="dialog"]')
+    .should('be.visible')
+    .within(() => {
+      cy.get('[role="tab"]').eq(1).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/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/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/Mesh/CheckerTracingConfig.tsx b/frontend/src/components/Mesh/CheckerTracingConfig.tsx
new file mode 100644
index 000000000..33ce2b34d
--- /dev/null
+++ b/frontend/src/components/Mesh/CheckerTracingConfig.tsx
@@ -0,0 +1,139 @@
+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
+          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..ad506c4c9
--- /dev/null
+++ b/frontend/src/components/Mesh/DiscoveryTracingConfig.tsx
@@ -0,0 +1,211 @@
+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 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>
+              {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}>
+                {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}>
+          {t('Rediscover')}
+        </Button>
+        {loading && <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..0ef6beb3c
--- /dev/null
+++ b/frontend/src/components/Mesh/TraceConfigurationModal.tsx
@@ -0,0 +1,198 @@
+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?.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 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}
+      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..cbdc9d35a 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,97 @@ 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">
+        {this.state.jsonModalContent && this.renderTableFromJson(JSON.parse(this.state.jsonModalContent))}
+      </Tab>
+    );
+
+    return [jsonTab, tableTab];
+  };
+
+  private getJSONLogModal = (): React.ReactNode => {
+    return (
+      <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 +1552,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 +1618,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/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/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/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/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/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/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/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/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/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)
+}