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

Hints from issue comments:
@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/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/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json
index 8f0d83faa..e95aea296 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",
@@ -251,6 +257,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.",
@@ -359,6 +366,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.",
@@ -422,6 +430,8 @@
   "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 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.",
@@ -452,7 +462,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 +489,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 +508,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..57251fc90 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": "故障注入",
@@ -239,6 +245,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.": "之前的规则中已经定义了‘所有请求’的匹配项。",
@@ -347,6 +354,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.",
@@ -410,6 +418,8 @@
   "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 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.",
@@ -440,7 +450,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 +477,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 +496,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/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 4b9037bdc..000000000
--- a/frontend/src/components/Mesh/TraceDiagnose.tsx
+++ /dev/null
@@ -1,257 +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);
-        setError(`Could not fetch diagnose info ${err}`);
-      });
-  };
-
-  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 = {
-    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' }}>{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/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/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/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/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/types/TracingInfo.ts b/frontend/src/types/TracingInfo.ts
index 2c8f49309..8a1647b58 100644
--- a/frontend/src/types/TracingInfo.ts
+++ b/frontend/src/types/TracingInfo.ts
@@ -151,3 +151,8 @@ export type TracingCheck = {
   message: string;
   validConfig?: ValidConfig[];
 };
+
+export type ConfigurationValidation = {
+  error?: string;
+  message: string;
+};
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/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/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 8f0a71d48..6b28eb28c 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 979d2c0e4..866aa904a 100644
--- a/tracing/jaeger/model/types.go
+++ b/tracing/jaeger/model/types.go
@@ -61,6 +61,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/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)
+}