Repository: kiali/kiali

Issue:
Tracing tool to help troubleshooting
### What do you want to improve?

The Tempo configuration introduced some complexity regarding the configuration of the tracing section in Kiali.
Trying to improve the situation with some documentation, this idea from @jshaughn about creating a tool that tries to autodiscover/validate the settings emerged. 

### What is the current behavior?
There is just a health check that it really doesn't help troubleshoot the problems. 

### What is the new behavior?
Create a tracing tool to correct the tracing settings for the Kiali configuration.

It can be an external tool like a hack script or, it can be integrated in Kiali. 

The idea to be integrated in Kiali is because running as an external tool could introduce some additional issues related to the permissions, location, or the Kiali settings. 

If the tool was an on-demand feature integrated in Kiali (For example, from the tracing node in the Mesh page) that based on the Kiali settings tries to perform some tests inside and outside the cluster (From the client side), and gave a report based on these tests. 

For example: 
the jaeger provider is configured but internal_url is pointing to the tempo api. Considerer change:
enternal_services.tracing.internal_url: "internal_tempo_url"
Or: 
The external link is hidden because tempo is configured but Grafana is disabled. Consider change tempo_config.url_format to jaeger.

With some basic data, Kiali could try to connect using the http client or the grpc client, analyze the urls from the configuration, and try to infer the right urls based on some parameters, like http/grpc, the provider, with authentication, ...

Discussion:
@jshaughn: Thinking more on this, maybe think about a similar, or combined effort, to ensure other external_services are correctly configured. But also maybe that if connectivity is established, that can maybe somehow also ensure decent performance.  Certainly just ensuring that users can quickly identify whether config is correct would be really valuable.  I mention perf only because that's typically the next problem.  They are connected but then experience timeouts etc.   

This all ties into stuff that @jmazzitelli and @nrfox are also doing around the improving how users can configure can configure and troubleshoot Kiali itself. 

Solution patch:
diff --git a/business/tracing.go b/business/tracing.go
index 441b3b642..d1d67bb5a 100644
--- a/business/tracing.go
+++ b/business/tracing.go
@@ -135,7 +135,7 @@ func (in *TracingService) GetAppTraces(ctx context.Context, ns, tracingName, app
 		return nil, err
 	}
 	r, err := client.GetAppTraces(ctx, ns, tracingName, query)
-	if tracingName != app {
+	if tracingName != app && r != nil {
 		// Filter by app
 		filter := operationSpanFilter(ctx, ns, app)
 		traces := []jaegerModels.Trace{}
@@ -378,3 +378,7 @@ func tracesToSpans(ctx context.Context, app models.TracingName, r *model.Tracing
 	log.FromContext(ctx).Trace().Msgf("Found [%d] spans in the [%d] traces for app [%s]", len(spans), len(r.Data), app)
 	return spans
 }
+
+func (in *TracingService) TracingDiagnose(ctx context.Context, token string) (trace *model.TracingDiagnose, err error) {
+	return tracing.TestNewClient(ctx, in.conf, token)
+}
diff --git a/business/workloads.go b/business/workloads.go
index dbc47acc0..3faaa433e 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -2713,8 +2713,10 @@ func (in *WorkloadService) StreamPodLogs(ctx context.Context, cluster, namespace
 		for _, pod := range pods {
 			names = append(names, pod.Name)
 		}
-		// They should be all in the same ns
-		return in.streamParsedLogs(cluster, pods[0].Namespace, names, opts, w)
+		if len(pods) > 0 {
+			// They should be all in the same ns
+			return in.streamParsedLogs(cluster, pods[0].Namespace, names, opts, w)
+		}
 	}
 	if opts.LogType == models.LogTypeWaypoint {
 		wk, err := in.GetWorkload(ctx, WorkloadCriteria{Cluster: cluster, Namespace: namespace, WorkloadName: workload, IncludeServices: false})
diff --git a/cache/cache.go b/cache/cache.go
index 9db9d91fe..f25845122 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -285,22 +285,17 @@ func (in *kialiCacheImpl) IsAmbientEnabled(cluster string) bool {
 		}
 
 		daemonSetList := &appsv1.DaemonSetList{}
-		if err := kubeCache.List(context.TODO(), daemonSetList); err != nil {
-			// Don't set the check so we will check again the next time since this error may be transient.
-			in.zl.Debug().Msgf("Error checking for ztunnel in Kiali accessible namespaces in cluster '%s': %s", cluster, err.Error())
-			return false
-		}
 		selector := map[string]string{
-			config.IstioAppLabel: config.Ztunnel,
+			config.KubernetesAppLabel: config.Ztunnel,
 		}
-		daemonsets, err := kubernetes.FilterDaemonSetsBySelector(daemonSetList.Items, selector)
-		if err != nil {
+		listOpts := []client.ListOption{client.MatchingLabels(selector)}
+		if err := kubeCache.List(context.TODO(), daemonSetList, listOpts...); err != nil {
 			// Don't set the check so we will check again the next time since this error may be transient.
 			in.zl.Debug().Msgf("Error checking for ztunnel in Kiali accessible namespaces in cluster '%s': %s", cluster, err.Error())
 			return false
 		}
 
-		if len(daemonsets) == 0 {
+		if len(daemonSetList.Items) == 0 {
 			in.zl.Debug().Msgf("No ztunnel daemonsets found in Kiali accessible namespaces in cluster '%s'", cluster)
 			in.ambientChecksPerCluster.Set(cluster, false)
 			return false
@@ -322,28 +317,23 @@ func (in *kialiCacheImpl) GetZtunnelPods(cluster string) []v1.Pod {
 	}
 
 	daemonSetList := &appsv1.DaemonSetList{}
-	if err := kubeCache.List(context.TODO(), daemonSetList); err != nil {
-		// Don't set the check so we will check again the next time since this error may be transient.
-		in.zl.Debug().Msgf("Error checking for ztunnel in Kiali accessible namespaces in cluster '%s': %s", cluster, err.Error())
-		return nil
-	}
-
 	selector := map[string]string{
-		config.IstioAppLabel: config.Ztunnel,
+		config.KubernetesAppLabel: config.Ztunnel,
 	}
-	daemonsets, err := kubernetes.FilterDaemonSetsBySelector(daemonSetList.Items, selector)
-	if err != nil {
+	listOpts := []client.ListOption{client.MatchingLabels(selector)}
+	if err := kubeCache.List(context.TODO(), daemonSetList, listOpts...); err != nil {
+		// Don't set the check so we will check again the next time since this error may be transient.
 		in.zl.Debug().Msgf("Error checking for ztunnel in Kiali accessible namespaces in cluster '%s': %s", cluster, err.Error())
 		return nil
 	}
 
-	if len(daemonsets) == 0 {
+	if len(daemonSetList.Items) == 0 {
 		in.zl.Debug().Msgf("No ztunnel daemonsets found in Kiali accessible namespaces in cluster '%s'", cluster)
 		return nil
 	}
 
 	podList := &v1.PodList{}
-	if err := kubeCache.List(context.TODO(), podList, client.InNamespace(daemonsets[0].Namespace), client.MatchingLabels{"app": config.Ztunnel}); err != nil {
+	if err := kubeCache.List(context.TODO(), podList, client.InNamespace(daemonSetList.Items[0].Namespace), client.MatchingLabels{"app": config.Ztunnel}); err != nil {
 		in.zl.Error().Msgf("Unable to get ztunnel pods: %s", err)
 		return nil
 	}
diff --git a/config/config.go b/config/config.go
index 21dbe4c8e..159c16060 100644
--- a/config/config.go
+++ b/config/config.go
@@ -80,6 +80,7 @@ const (
 	IstioAppLabel             = "app"                                    // we can assume istio components are labeled with "app"
 	IstioRevisionLabel        = "istio.io/rev"                           // the standard label key used to identify the istio revision.
 	IstioVersionLabel         = "version"                                // we can assume istio components are labeled with "version", if versioned
+	KubernetesAppLabel        = "app.kubernetes.io/name"
 	Waypoint                  = "waypoint"
 	WaypointFor               = "istio.io/waypoint-for"
 	WaypointForAll            = "all"
diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json
index 2bbdf74a8..8f0d83faa 100644
--- a/frontend/public/locales/es/translation.json
+++ b/frontend/public/locales/es/translation.json
@@ -68,6 +68,8 @@
   "Builds an Authorization Policy based on Rules": "Builds an Authorization Policy based on Rules",
   "CA Certificates": "CA Certificates",
   "Cancel": "Cancel",
+  "Check Status": "Check Status",
+  "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",
   "Clear": "Clear",
@@ -349,6 +351,7 @@
   "Pod Status": "Pod Status",
   "Policy": "Policy",
   "Port": "Port",
+  "Possible configuration(s) found for external_services.tracing": "Possible configuration(s) found for external_services.tracing",
   "Present": "Present",
   "Preview": "Preview",
   "Preview new istio objects": "Preview new istio objects",
@@ -449,6 +452,7 @@
   "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",
@@ -476,10 +480,12 @@
   "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.",
@@ -495,6 +501,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",
   "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 859f582f9..573cbf4bd 100644
--- a/frontend/public/locales/zh/translation.json
+++ b/frontend/public/locales/zh/translation.json
@@ -58,6 +58,8 @@
   "Builds an Authorization Policy based on Rules": "Builds an Authorization Policy based on Rules",
   "CA Certificates": "CA证书",
   "Cancel": "取消",
+  "Check Status": "Check Status",
+  "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": "熔断",
   "Clear": "清除",
@@ -337,6 +339,7 @@
   "Pod Status": "Pod Status",
   "Policy": "Policy",
   "Port": "端口",
+  "Possible configuration(s) found for external_services.tracing": "Possible configuration(s) found for external_services.tracing",
   "Present": "Present",
   "Preview": "预览",
   "Preview new istio objects": "Preview new istio objects",
@@ -437,6 +440,7 @@
   "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": "图",
@@ -464,10 +468,12 @@
   "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.": "网状网关上不允许虚拟服务主机'*'通配符。",
@@ -483,6 +489,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",
   "Workload": "Workload",
   "Workload Entry": "Workload Entry",
   "Workload Selector": "Workload Selector",
diff --git a/frontend/src/actions/ActionKeys.ts b/frontend/src/actions/ActionKeys.ts
index 244463d87..fdae50c6e 100644
--- a/frontend/src/actions/ActionKeys.ts
+++ b/frontend/src/actions/ActionKeys.ts
@@ -60,6 +60,7 @@ export enum ActionKeys {
   HELP_STATUS_REFRESH = 'HELP_STATUS_REFRESH',
 
   TRACING_SET_URL = 'TRACING_SET_URL',
+  TRACING_SET_DIAGNOSE = 'TRACING_SET_DIAGNOSE',
   TRACING_SET_ENABLED = 'TRACING_SET_ENABLED',
   TRACING_SET_INFO = 'TRACING_SET_INFO',
   TRACING_SET_TRACE_ID = 'TRACING_SET_TRACE_ID',
diff --git a/frontend/src/actions/TracingActions.ts b/frontend/src/actions/TracingActions.ts
index 1100c4fae..9fb82587f 100644
--- a/frontend/src/actions/TracingActions.ts
+++ b/frontend/src/actions/TracingActions.ts
@@ -1,9 +1,10 @@
 import { ActionType, createAction, createStandardAction } from 'typesafe-actions';
 import { ActionKeys } from './ActionKeys';
-import { TracingInfo, JaegerTrace } from 'types/TracingInfo';
+import { TracingInfo, JaegerTrace, TracingCheck } from 'types/TracingInfo';
 
 // synchronous action creators
 export const TracingActions = {
+  setDiagnose: createStandardAction(ActionKeys.TRACING_SET_DIAGNOSE)<TracingCheck | undefined>(),
   setInfo: createStandardAction(ActionKeys.TRACING_SET_INFO)<TracingInfo | null>(),
   setTrace: createAction(ActionKeys.TRACING_SET_TRACE, resolve => (trace?: JaegerTrace) =>
     resolve({ selectedTrace: trace })
diff --git a/frontend/src/components/Mesh/LogsModal.tsx b/frontend/src/components/Mesh/LogsModal.tsx
new file mode 100644
index 000000000..e9674bc65
--- /dev/null
+++ b/frontend/src/components/Mesh/LogsModal.tsx
@@ -0,0 +1,74 @@
+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/TraceDiagnose.tsx b/frontend/src/components/Mesh/TraceDiagnose.tsx
new file mode 100644
index 000000000..efecc62db
--- /dev/null
+++ b/frontend/src/components/Mesh/TraceDiagnose.tsx
@@ -0,0 +1,257 @@
+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) {
+                        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/config/Config.ts b/frontend/src/config/Config.ts
index e063be2d3..cda3660bf 100644
--- a/frontend/src/config/Config.ts
+++ b/frontend/src/config/Config.ts
@@ -185,6 +185,7 @@ const conf = {
       serviceTraces: (namespace: string, svc: string) => `api/namespaces/${namespace}/services/${svc}/traces`,
       status: 'api/status',
       tracing: 'api/tracing',
+      tracingDiagnose: 'api/tracing/diagnose',
       tracingErrorTraces: (namespace: string, app: string) => `api/namespaces/${namespace}/apps/${app}/errortraces`,
       tracingTrace: (idTrace: string) => `api/traces/${idTrace}`,
       workloadSpans: (namespace: string, workload: string) => `api/namespaces/${namespace}/workloads/${workload}/spans`,
diff --git a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx
index 9aebd8f27..6ad96706e 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx
@@ -6,12 +6,13 @@ import {
   targetPanelHR,
   targetPanelStyle
 } from './TargetPanelCommon';
-import { MeshNodeData, NodeTarget, isExternal } from 'types/Mesh';
+import { MeshNodeData, NodeTarget, isExternal, MeshInfraType } from 'types/Mesh';
 import { classes } from 'typestyle';
 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';
 
 type TargetPanelNodeProps<T extends MeshNodeData> = TargetPanelCommonProps & {
   target: NodeTarget<T>;
@@ -35,7 +36,9 @@ export const TargetPanelNode: React.FC<TargetPanelNodeProps<MeshNodeData>> = (
       <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} />
+        )}
         {targetPanelHR}
 
         <TargetPanelEditor configData={data.infraData} targetName={data.infraName}></TargetPanelEditor>
diff --git a/frontend/src/reducers/TracingState.ts b/frontend/src/reducers/TracingState.ts
index 78c71bdf7..fbf73713d 100644
--- a/frontend/src/reducers/TracingState.ts
+++ b/frontend/src/reducers/TracingState.ts
@@ -2,11 +2,12 @@ import { updateState } from '../utils/Reducer';
 import { KialiAppAction } from '../actions/KialiAppAction';
 import { getType } from 'typesafe-actions';
 import { TracingActions } from '../actions/TracingActions';
-import { TracingInfo, JaegerTrace } from 'types/TracingInfo';
+import { TracingInfo, JaegerTrace, TracingCheck } from 'types/TracingInfo';
 
 export const INITIAL_TRACING_STATE: TracingState = {};
 
 export type TracingState = {
+  diagnose?: TracingCheck;
   info?: TracingInfo;
   selectedTrace?: JaegerTrace;
 };
@@ -16,6 +17,8 @@ export const TracingStateReducer = (
   action: KialiAppAction
 ): TracingState => {
   switch (action.type) {
+    case getType(TracingActions.setDiagnose):
+      return updateState(state, { diagnose: action.payload ? action.payload : undefined });
     case getType(TracingActions.setInfo):
       return updateState(state, { info: action.payload ? action.payload : undefined });
     case getType(TracingActions.setTrace):
diff --git a/frontend/src/services/Api.ts b/frontend/src/services/Api.ts
index 2e523e00d..412b7b76e 100644
--- a/frontend/src/services/Api.ts
+++ b/frontend/src/services/Api.ts
@@ -47,7 +47,7 @@ import {
   ZtunnelConfigDump
 } from '../types/IstioObjects';
 import { ComponentStatus, IstiodResourceThresholds } from '../types/IstioStatus';
-import { TracingInfo, TracingResponse, TracingSingleResponse } from '../types/TracingInfo';
+import { 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';
@@ -1399,3 +1399,13 @@ export const getControlPlanes = (): Promise<ApiResponse<ControlPlane[]>> => {
 export const getMeshGraph = (params: MeshQuery): Promise<ApiResponse<MeshDefinition>> => {
   return newRequest<MeshDefinition>(HTTP_VERBS.GET, urls.meshGraph, params, {});
 };
+
+export const getDiagnoseStatus = (cluster?: string): Promise<ApiResponse<TracingCheck>> => {
+  const queryParams: ClusterParam = {};
+
+  if (cluster) {
+    queryParams.clusterName = cluster;
+  }
+
+  return newRequest<TracingCheck>(HTTP_VERBS.GET, urls.tracingDiagnose, queryParams, {});
+};
diff --git a/frontend/src/types/TracingInfo.ts b/frontend/src/types/TracingInfo.ts
index 68c1e3cf9..3c38364c8 100644
--- a/frontend/src/types/TracingInfo.ts
+++ b/frontend/src/types/TracingInfo.ts
@@ -130,3 +130,24 @@ export type TracingSingleResponse = {
   data: JaegerTrace | null;
   errors: TracingError[];
 };
+
+export type ValidConfig = {
+  namespaceSelector: boolean;
+  provider: string;
+  url: string;
+  useGRPC: boolean;
+  warning?: string;
+};
+
+export type LogLine = {
+  result: string;
+  test: string;
+  time: string;
+};
+
+export type TracingCheck = {
+  code?: number;
+  logLine: LogLine[];
+  message: string;
+  validConfig?: ValidConfig[];
+};
diff --git a/handlers/tracing.go b/handlers/tracing.go
index b5e1fe61b..f7112fe81 100644
--- a/handlers/tracing.go
+++ b/handlers/tracing.go
@@ -1,6 +1,7 @@
 package handlers
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -398,3 +399,42 @@ func readQuery(conf *config.Config, values url.Values) (models.TracingQuery, err
 
 	return q, nil
 }
+
+// TracingDiagnose is the API handler to diagnose Tracing configuration
+func TracingDiagnose(
+	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) {
+
+		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, err := business.Tracing.TracingDiagnose(r.Context(), clientFactory.GetSAHomeClusterClient().GetToken())
+		if err != nil {
+			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
+			return
+		}
+
+		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)
+	return err == nil
+}
diff --git a/kubernetes/filters.go b/kubernetes/filters.go
index 427c8be48..f2ff7b0da 100644
--- a/kubernetes/filters.go
+++ b/kubernetes/filters.go
@@ -688,26 +688,6 @@ func FilterByNamespace[T runtime.Object](objects []T, namespace string) []T {
 	return filtered
 }
 
-func FilterDaemonSetsBySelector(daemonSets []appsv1.DaemonSet, l map[string]string) ([]appsv1.DaemonSet, error) {
-	selector := labels.Set(l)
-	retDS := []appsv1.DaemonSet{}
-	for _, ds := range daemonSets {
-		// TODO: Can we eliminate error?
-		labelMap, err := metav1.LabelSelectorAsMap(ds.Spec.Selector)
-		if err != nil {
-			return nil, err
-		}
-		labelSet := labels.Set(labelMap)
-
-		svcSelector := labelSet.AsSelector()
-		// selector match is done after listing all daemonSets, similar to registry reading
-		if selector.AsSelector().Empty() || (!svcSelector.Empty() && svcSelector.Matches(selector)) {
-			retDS = append(retDS, ds)
-		}
-	}
-	return retDS, nil
-}
-
 func FilterServicesBySelector(services []core_v1.Service, l map[string]string) []core_v1.Service {
 	selector := labels.Set(l)
 	retSvc := []core_v1.Service{}
diff --git a/routing/routes.go b/routing/routes.go
index 44b45ce11..d6082b9b1 100644
--- a/routing/routes.go
+++ b/routing/routes.go
@@ -190,6 +190,26 @@ func NewRoutes(
 			handlers.Root(conf, clientFactory, kialiCache, grafana),
 			true,
 		},
+		// swagger:route GET /tracing/diagnose status getStatus
+		// ---
+		// Endpoint to get a diagnose for the tracing endpoint
+		//
+		//     Produces:
+		//     - application/json
+		//
+		//     Schemes: http, https
+		//
+		// responses:
+		//      500: internalError
+		//      200: tracingDiagnose
+		{
+			"Diagnose",
+			log.TracingLogName,
+			"GET",
+			"/api/tracing/diagnose",
+			handlers.TracingDiagnose(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 e985d2763..8f0a71d48 100644
--- a/tracing/client.go
+++ b/tracing/client.go
@@ -298,6 +298,7 @@ func (in *Client) GetServiceStatus(ctx context.Context) (bool, error) {
 	return in.grpcClient.GetServices(ctx)
 }
 
+// BuildTracingServiceName
 func BuildTracingServiceName(namespace, app string) string {
 	conf := config.Get()
 	if conf.ExternalServices.Tracing.NamespaceSelector {
diff --git a/tracing/discovery_client.go b/tracing/discovery_client.go
new file mode 100644
index 000000000..f76a0712a
--- /dev/null
+++ b/tracing/discovery_client.go
@@ -0,0 +1,444 @@
+package tracing
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/rs/zerolog"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/metadata"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/log"
+	"github.com/kiali/kiali/tracing/jaeger"
+	"github.com/kiali/kiali/tracing/jaeger/model"
+	"github.com/kiali/kiali/tracing/otel"
+	otelModel "github.com/kiali/kiali/tracing/otel/model"
+	"github.com/kiali/kiali/tracing/tempo"
+	"github.com/kiali/kiali/util/grpcutil"
+	"github.com/kiali/kiali/util/httputil"
+)
+
+type DialFunc func(network, address string, timeout time.Duration) (net.Conn, error)
+
+var MakeRequestFunc = otel.MakeRequest
+
+func TestNewClient(ctx context.Context, conf *config.Config, token string) (*model.TracingDiagnose, error) {
+	cfgTracing := conf.ExternalServices.Tracing
+	test := model.TracingDiagnose{}
+	logs := []model.LogLine{}
+	zl := log.WithGroup(log.TracingLogName)
+
+	// Tracing not enabled
+	if !cfgTracing.Enabled {
+		test.Message = "tracing is not enabled"
+		return &test, nil
+	}
+
+	// Internal URL not set
+	url := cfgTracing.InternalURL
+	if !conf.InCluster {
+		url = cfgTracing.ExternalURL
+		logs = append(logs, model.LogLine{Time: time.Now(), Test: fmt.Sprintf("Using external url %s because not in cluster", url)})
+	}
+
+	parsedURL, ll, err := parseUrl(url)
+	if err != nil {
+		return &test, fmt.Errorf("external_services.tracing.internal_url is required and must be a valid URL")
+	}
+	logs = append(logs, ll...)
+
+	// Get Auth
+	auth := cfgTracing.Auth
+	if auth.UseKialiToken {
+		auth.Token = token
+	}
+
+	ports, ll := discoverPortsWithDial(zl, parsedURL.Host, net.DialTimeout)
+	logs = append(logs, ll...)
+
+	validConfig, ll := discoverUrl(ctx, zl, *parsedURL, ports, &auth, cfgTracing)
+	test.ValidConfig = validConfig
+	test.LogLine = append(logs, ll...)
+
+	return &test, nil
+
+}
+
+// Parse URL
+func parseUrl(urlToParse string) (*model.ParsedUrl, []model.LogLine, error) {
+	parsedURL, err := url.Parse(urlToParse)
+	logLines := []model.LogLine{}
+	if err != nil {
+		return nil, logLines, fmt.Errorf("cannot parse url: %s", err.Error())
+	}
+	host, port, err := net.SplitHostPort(parsedURL.Host)
+	if err != nil {
+		host = parsedURL.Host
+		port = ""
+	}
+	baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
+	rest := parsedURL.RequestURI()
+
+	logLines = append(logLines, model.LogLine{Time: time.Now(), Test: "Parsed url", Result: fmt.Sprintf("[Ok] host %s - port %s - baseURL %s - rest %s - prot: %s", host, port, baseURL, rest, parsedURL.Scheme)})
+	return &model.ParsedUrl{BaseUrl: baseURL, Host: host, Port: port, Path: rest, Scheme: parsedURL.Scheme}, logLines, nil
+}
+
+// discoverPorts try to discover open ports
+// dial is just to make it testable
+func discoverPortsWithDial(zl *zerolog.Logger, host string, dial DialFunc) ([]string, []model.LogLine) {
+	portsToScan := []string{"16686", "16685", "80", "3200", "3100", "8080", "9095", "443"}
+	openPorts := []string{}
+	logLines := []model.LogLine{}
+
+	for _, port := range portsToScan {
+		address := fmt.Sprintf("%s:%s", host, port)
+		conn, err := dial("tcp", address, 500*time.Millisecond)
+		if err == nil {
+			logLines = append(logLines, model.LogLine{Time: time.Now(), Test: "Checking open ports", Result: fmt.Sprintf("[Ok] Port %s is open", port)})
+			openPorts = append(openPorts, port)
+			conn.Close()
+		}
+	}
+	zl.Trace().Msgf("[Discovery client] Open ports: %v", openPorts)
+	return openPorts, logLines
+}
+
+// discoverUrl try to discover valid URLs
+func discoverUrl(ctx context.Context, zl *zerolog.Logger, parsedUrl model.ParsedUrl, ports []string, auth *config.Auth, cfgTracing config.TracingConfig) ([]model.ValidConfig, []model.LogLine) {
+	validConfigs := []model.ValidConfig{}
+	var client http.Client
+	logs := []model.LogLine{}
+
+	// Create client
+	timeout := time.Duration(config.Get().ExternalServices.Tracing.QueryTimeout) * time.Second
+	conf := config.Get()
+	transport, err := httputil.CreateTransport(conf, auth, &http.Transport{}, timeout, cfgTracing.CustomHeaders)
+	if err != nil {
+		logs = append(logs, model.LogLine{Time: time.Now(), Test: "Create HTTP client", Result: fmt.Sprintf("[ERROR] Cannot create transport: %s", err.Error())})
+		// TODO: Validate auth?
+		return validConfigs, logs
+	} else {
+		client = http.Client{Transport: transport, Timeout: timeout}
+	}
+
+	for _, port := range ports {
+		switch port {
+		case "16686":
+			{
+				vc, ll := validateJaegerHTTP(ctx, client, zl, parsedUrl, port)
+				validConfigs = append(validConfigs, vc...)
+				logs = append(logs, ll...)
+			}
+		case "8080":
+			{
+				vc, ll := validateTempoHTTP(ctx, client, zl, parsedUrl, port)
+				validConfigs = append(validConfigs, vc...)
+				logs = append(logs, ll...)
+			}
+		case "80", "443":
+			{
+				if port == "443" {
+					parsedUrl.Scheme = "https"
+				}
+				vc, ll := validateJaegerHTTP(ctx, client, zl, parsedUrl, port)
+				validConfigs = append(validConfigs, vc...)
+				logs = append(logs, ll...)
+				vc, ll = validateTempoHTTP(ctx, client, zl, parsedUrl, port)
+				validConfigs = append(validConfigs, vc...)
+				logs = append(logs, ll...)
+				vc, ll = validateSimpleTempoHTTP(ctx, client, zl, parsedUrl, port)
+				validConfigs = append(validConfigs, vc...)
+				logs = append(logs, ll...)
+			}
+		case "3200", "3100":
+			{
+				vc, ll := validateSimpleTempoHTTP(ctx, client, zl, parsedUrl, port)
+				validConfigs = append(validConfigs, vc...)
+				logs = append(logs, ll...)
+			}
+		case "16685":
+			{
+				// Try gRPC Jaeger client
+				opts, err := grpcutil.GetAuthDialOptions(conf, parsedUrl.Scheme == "https", auth)
+				if err == nil {
+					address := parsedUrl.Host + ":" + port
+					logs = append(logs, model.LogLine{Time: time.Now(), Test: "gRPC Client 16685", Result: fmt.Sprintf("%s GRPC client info: address=%s, auth.type=%s", cfgTracing.Provider, address, auth.Type)})
+
+					if len(cfgTracing.CustomHeaders) > 0 {
+						logs = append(logs, model.LogLine{Time: time.Now(), Test: "gRPC Client 16685", Result: fmt.Sprintf("Adding [%v] custom headers to Tracing client", len(cfgTracing.CustomHeaders))})
+						ctx = metadata.NewOutgoingContext(ctx, metadata.New(cfgTracing.CustomHeaders))
+					}
+					conn, err := grpc.NewClient(address, opts...)
+					if err == nil {
+						cc := model.NewQueryServiceClient(conn)
+						clientgRPC, err := jaeger.NewGRPCJaegerClient(cc)
+						if err != nil {
+							logs = append(logs, model.LogLine{Time: time.Now(), Test: "Create gRPC Client 16685", Result: fmt.Sprintf("Error creating gRPC Client: [%s]", err.Error())})
+						} else {
+							ok, err := clientgRPC.GetServices(ctx)
+							if ok {
+								vc := model.ValidConfig{Url: fmt.Sprintf("%s://%s", parsedUrl.Scheme, address), Provider: "jaeger", UseGRPC: true}
+								validConfigs = append(validConfigs, vc)
+								logs = append(logs, model.LogLine{Time: time.Now(), Test: "Create gRPC Client 16685 Ok", Result: "Valid gRPC Client found"})
+							} else {
+								logs = append(logs, model.LogLine{Time: time.Now(), Test: "GetServices gRPC Client 16685", Result: fmt.Sprintf("Error getting gRPC Services: [%s]", err.Error())})
+							}
+						}
+					} else {
+						log.Errorf("Error creating client %s", err.Error())
+						return nil, nil
+					}
+				}
+				if err != nil {
+					logs = append(logs, model.LogLine{Time: time.Now(), Test: "gRPC Client 16685", Result: fmt.Sprintf("Error while building GRPC dial options: %v", err)})
+				}
+			}
+		case "9095":
+			{
+				// Try GRPC Tempo Client
+				// And this also requires HTTP Client
+				var dialOps []grpc.DialOption
+				if cfgTracing.Auth.Type == "basic" {
+					dialOps = append(dialOps, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
+					dialOps = append(dialOps, grpc.WithPerRPCCredentials(&basicAuth{
+						Header: fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(strings.Join([]string{cfgTracing.Auth.Username, cfgTracing.Auth.Password}, ":")))),
+					}))
+				} else {
+					dialOps = append(dialOps, grpc.WithTransportCredentials(insecure.NewCredentials()))
+				}
+				grpcAddress := fmt.Sprintf("%s:%s", parsedUrl.Host, port)
+				clientConn, _ := grpc.NewClient(grpcAddress, dialOps...)
+				streamClient, err := tempo.NewgRPCClient(clientConn)
+				if err != nil {
+					msg := fmt.Sprintf("Error creating gRPC Client %s", err.Error())
+					logs = append(logs, model.LogLine{Time: time.Now(), Test: "Create gRPC Client 9095 error", Result: msg})
+					zl.Error().Msg(msg)
+				} else {
+					ok, err := streamClient.GetServices(ctx)
+					if ok {
+						// TODO: Different config gRPC Port!!!
+						vc := model.ValidConfig{Url: grpcAddress, Provider: "tempo", UseGRPC: true}
+						validConfigs = append(validConfigs, vc)
+						logs = append(logs, model.LogLine{Time: time.Now(), Test: "Create gRPC Tempo Client 9095 Ok", Result: "Valid gRPC Client found. Notice this config also requires any valid HTTP configuration. "})
+					} else {
+						logs = append(logs, model.LogLine{Time: time.Now(), Test: "GetServices gRPC Tempo Client 9095", Result: fmt.Sprintf("Error getting gRPC Services: [%s]", err.Error())})
+					}
+				}
+			}
+		}
+	}
+	return validConfigs, logs
+}
+
+// validateJaegerHTTP validate specific path for Jaeger and HTTP endpoint
+func validateJaegerHTTP(ctx context.Context, client http.Client, zl *zerolog.Logger, parsedUrl model.ParsedUrl, port string) ([]model.ValidConfig, []model.LogLine) {
+	logs := []model.LogLine{}
+	validConfigs := []model.ValidConfig{}
+
+	// We assume it is Jaeger. Try Jaeger URL
+	validEndpoint := fmt.Sprintf("%s://%s:%s/jaeger", parsedUrl.Scheme, parsedUrl.Host, port)
+	endpointJ := fmt.Sprintf("%s/api/services", validEndpoint)
+	vc, ll, err := validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "jaeger")
+	if err == nil {
+		validConfigs = append(validConfigs, *vc)
+	}
+	logs = append(logs, ll...)
+
+	// Try Tempo URL
+	validEndpoint = fmt.Sprintf("%s://%s:%s", parsedUrl.Scheme, parsedUrl.Host, port)
+	endpointJ = fmt.Sprintf("%s/api/services", validEndpoint)
+	vc, ll, err = validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "jaeger")
+	if err == nil {
+		validConfigs = append(validConfigs, *vc)
+	}
+	logs = append(logs, ll...)
+	return validConfigs, logs
+}
+
+// validateTempoHTTP validate specific path for Tempo and HTTP endpoint
+func validateTempoHTTP(ctx context.Context, client http.Client, zl *zerolog.Logger, parsedUrl model.ParsedUrl, port string) ([]model.ValidConfig, []model.LogLine) {
+	logs := []model.LogLine{}
+	validConfigs := []model.ValidConfig{}
+
+	// Tempo from GW, Tempo multi tenant uses security and the gateway
+	if strings.Contains(parsedUrl.Host, "gateway") {
+		splitUrl := strings.Split(parsedUrl.Path, "/")
+		tenant := ""
+		if len(splitUrl) > 4 {
+			tenant = splitUrl[4]
+		} else {
+			logs = append(logs, model.LogLine{Time: time.Now(), Test: "Create http client in port 8080", Result: fmt.Sprintf("tenant name not found: %s", parsedUrl.Path)})
+		}
+		validEndpoint := fmt.Sprintf("%s://%s:%s/api/traces/v1/%s/tempo", parsedUrl.Scheme, parsedUrl.Host, port, tenant)
+		endpointJ := fmt.Sprintf("%s/api/search?q={}", validEndpoint)
+		vc, ll, err := validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "tempo")
+		if err == nil {
+			validConfigs = append(validConfigs, *vc)
+		}
+		logs = append(logs, ll...)
+
+		// Try GW Jaeger Endpoint
+		validEndpoint = fmt.Sprintf("%s://%s:%s/api/traces/v1/%s", parsedUrl.Scheme, parsedUrl.Host, port, tenant)
+		endpointJ = fmt.Sprintf("%s/api/services", validEndpoint)
+		vc, ll, err = validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "jaeger")
+		if err == nil {
+			validConfigs = append(validConfigs, *vc)
+		}
+		logs = append(logs, ll...)
+
+	} else {
+		// Tempo service (No GW)
+		validEndpoint := fmt.Sprintf("%s://%s:%s/api/traces/v1/tempo", parsedUrl.Scheme, parsedUrl.Host, port)
+		endpointJ := fmt.Sprintf("%s/api/search?q={}", validEndpoint)
+		vc, ll, err := validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "tempo")
+		if err == nil {
+			validConfigs = append(validConfigs, *vc)
+		}
+		logs = append(logs, ll...)
+
+		// Try GW Jaeger Endpoint
+		validEndpoint = fmt.Sprintf("%s://%s:%s/api/traces/v1", parsedUrl.Scheme, parsedUrl.Host, port)
+		endpointJ = fmt.Sprintf("%s/api/traces/v1/api/services", validEndpoint)
+		vc, ll, err = validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "jaeger")
+		if err == nil {
+			validConfigs = append(validConfigs, *vc)
+		}
+		logs = append(logs, ll...)
+	}
+
+	return validConfigs, logs
+}
+
+// validateSimpleTempoHTTP validate specific path for Tempo and HTTP endpoint
+func validateSimpleTempoHTTP(ctx context.Context, client http.Client, zl *zerolog.Logger, parsedUrl model.ParsedUrl, port string) ([]model.ValidConfig, []model.LogLine) {
+	logs := []model.LogLine{}
+	validConfigs := []model.ValidConfig{}
+
+	// Try Tempo HTTP client
+	validEndpoint := fmt.Sprintf("%s://%s:%s", parsedUrl.Scheme, parsedUrl.Host, port)
+	endpointJ := fmt.Sprintf("%s/api/search?q={}", validEndpoint)
+	vc, ll, err := validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "tempo")
+	if err == nil {
+		validConfigs = append(validConfigs, *vc)
+	}
+	logs = append(logs, ll...)
+
+	// Try Jaeger?
+	validEndpoint = fmt.Sprintf("%s://%s:%s", parsedUrl.Scheme, parsedUrl.Host, port)
+	endpointJ = fmt.Sprintf("%s/api/services", validEndpoint)
+	vc, ll, err = validateEndpoint(ctx, client, zl, endpointJ, validEndpoint, "jaeger")
+	if err == nil {
+		validConfigs = append(validConfigs, *vc)
+	}
+	logs = append(logs, ll...)
+
+	return validConfigs, logs
+}
+
+// validateEndpoint Given an endpoint, validates it is valid, otherwise returns an error or logLines
+func validateEndpoint(ctx context.Context, client http.Client, zl *zerolog.Logger, endpoint, validEndpoint string, provider string) (*model.ValidConfig, []model.LogLine, error) {
+	logs := []model.LogLine{}
+	resp, code, reqError := MakeRequestFunc(ctx, client, endpoint, nil)
+	if code != 200 {
+		// Try to handle possible known errors
+		// http to https
+		msg := fmt.Sprintf("[Discovery client] Cannot query endpoint: %s. Code [%d].", endpoint, code)
+		if reqError != nil {
+			msg = fmt.Sprintf("%s. Error: %s", msg, reqError.Error())
+		}
+		if resp != nil {
+			msg = fmt.Sprintf("%s. Response: %s", msg, resp)
+			response := string(resp)
+			// HTTPS required. Try HTTPS URL
+			if strings.Contains(response, "Client sent an HTTP request to an HTTPS") && !strings.Contains(endpoint, "https") {
+				endpoint = strings.Replace(endpoint, "http", "https", 1)
+				validEndpoint = strings.Replace(validEndpoint, "http", "https", 1)
+				return validateEndpoint(ctx, client, zl, endpoint, validEndpoint, provider)
+			}
+			// No tenants found
+			if strings.Contains(response, "tenant not found") {
+				vc := model.ValidConfig{Url: validEndpoint, Provider: provider, UseGRPC: false, Warning: "Tenant must be specified."}
+				return &vc, logs, nil
+			}
+		}
+		// Certificate error: Try valid host
+		if reqError != nil && strings.Contains(reqError.Error(), "certificate is valid for") {
+			validRe := regexp.MustCompile(`certificate is valid for ([^,]+)`)
+			notRe := regexp.MustCompile(`not ([^\s]+)`)
+			validMatch := validRe.FindStringSubmatch(reqError.Error())
+			notMatch := notRe.FindStringSubmatch(reqError.Error())
+			if len(validMatch) > 1 && len(notMatch) > 1 {
+				replacedEndpoint := strings.Replace(endpoint, notMatch[1], validMatch[1], 1)
+				replacedValidEndpoint := strings.Replace(validEndpoint, notMatch[1], validMatch[1], 1)
+				if replacedEndpoint != endpoint {
+					return validateEndpoint(ctx, client, zl, replacedEndpoint, replacedValidEndpoint, provider)
+				}
+			}
+		}
+		// Auth issue
+		var unknownAuthErr x509.UnknownAuthorityError
+		if errors.As(reqError, &unknownAuthErr) {
+			vc := model.ValidConfig{Url: validEndpoint, Provider: provider, UseGRPC: false, Warning: "Auth section must be configured properly"}
+			return &vc, logs, nil
+		}
+		logs = append(logs, model.LogLine{Time: time.Now(), Test: endpoint, Result: msg})
+		zl.Trace().Msg(msg)
+		return nil, logs, errors.New(msg)
+	}
+
+	if provider == "jaeger" {
+		var response model.TracingServices
+		if errMarshal := json.Unmarshal(resp, &response); errMarshal != nil {
+			msg := fmt.Sprintf("[Discovery client] Error unmarshalling Jaeger response: %s [URL: %v]", errMarshal, endpoint)
+			logs = append(logs, model.LogLine{Time: time.Now(), Test: endpoint, Result: msg})
+			zl.Trace().Msg(msg)
+			return nil, logs, errors.New(msg)
+		}
+		vc := model.ValidConfig{Url: validEndpoint, Provider: provider, UseGRPC: false, NamespaceSelector: false}
+		for _, rd := range response.Data {
+			parts := strings.Split(rd, ".")
+			if len(parts) > 1 {
+				vc.NamespaceSelector = true
+				break
+			}
+		}
+		msg := fmt.Sprintf("[Discovery client] Found valid Config %v", vc)
+		logs = append(logs, model.LogLine{Time: time.Now(), Test: endpoint, Result: msg})
+		zl.Trace().Msg(msg)
+		return &vc, logs, nil
+	}
+	// Try Tempo
+	var response otelModel.Traces
+	if errMarshal := json.Unmarshal(resp, &response); errMarshal != nil {
+		msg := fmt.Sprintf("[Discovery client] Error unmarshalling Tempo response: %s [URL: %v]", errMarshal, endpoint)
+		logs = append(logs, model.LogLine{Time: time.Now(), Test: endpoint, Result: msg})
+		zl.Trace().Msg(msg)
+		return nil, logs, errors.New(msg)
+	}
+	vc := model.ValidConfig{Url: validEndpoint, Provider: "tempo", UseGRPC: false, NamespaceSelector: false}
+	for _, rd := range response.Traces {
+		parts := strings.Split(rd.RootServiceName, ".")
+		if len(parts) > 1 {
+			vc.NamespaceSelector = true
+			break
+		}
+	}
+	msg := fmt.Sprintf("[Discovery client] Found valid Config %v", vc)
+	logs = append(logs, model.LogLine{Time: time.Now(), Test: endpoint, Result: msg})
+	zl.Trace().Msg(msg)
+	return &vc, logs, nil
+}
diff --git a/tracing/jaeger/http_client.go b/tracing/jaeger/http_client.go
index 3ad8fd651..efb3df721 100644
--- a/tracing/jaeger/http_client.go
+++ b/tracing/jaeger/http_client.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"io"
 	"net/http"
 	"net/url"
 	"path"
@@ -14,6 +13,7 @@ import (
 
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/tracing/jaeger/model"
+	"github.com/kiali/kiali/tracing/otel"
 	"github.com/kiali/kiali/util"
 )
 
@@ -45,7 +45,7 @@ func (jc JaegerHTTPClient) GetTraceDetailHTTP(ctx context.Context, client http.C
 	u := *endpoint
 	// /querier/api/traces/<traceid>?mode=xxxx&blockStart=0000&blockEnd=FFFF&start=<start>&end=<end>
 	u.Path = path.Join(u.Path, "/api/traces/"+traceID)
-	resp, code, reqError := makeRequest(client, u.String(), nil)
+	resp, code, reqError := otel.MakeRequest(ctx, client, u.String(), nil)
 	if reqError != nil {
 		getLoggerFromContextHTTPJaeger(ctx).Error().Msgf("Jaeger query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return nil, reqError
@@ -70,7 +70,7 @@ func (jc JaegerHTTPClient) GetTraceDetailHTTP(ctx context.Context, client http.C
 func (jc JaegerHTTPClient) GetServiceStatusHTTP(ctx context.Context, client http.Client, baseURL *url.URL) (bool, error) {
 	url := *baseURL
 	url.Path = path.Join(url.Path, "/api/services")
-	_, _, reqError := makeRequest(client, url.String(), nil)
+	_, _, reqError := otel.MakeRequest(ctx, client, url.String(), nil)
 	return reqError == nil, reqError
 }
 
@@ -83,7 +83,7 @@ func queryTracesHTTP(ctx context.Context, client http.Client, u *url.URL) (*mode
 		query.Set("minDuration", minDuration+"us")
 		u.RawQuery = query.Encode()
 	}
-	resp, code, reqError := makeRequest(client, u.String(), nil)
+	resp, code, reqError := otel.MakeRequest(ctx, client, u.String(), nil)
 	if reqError != nil {
 		getLoggerFromContextHTTPJaeger(ctx).Error().Msgf("Jaeger query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return &model.TracingResponse{}, reqError
@@ -126,23 +126,3 @@ func prepareQuery(ctx context.Context, u *url.URL, jaegerServiceName string, que
 	u.RawQuery = q.Encode()
 	zl.Debug().Msgf("Prepared Jaeger query: %v", u)
 }
-
-func makeRequest(client http.Client, endpoint string, body io.Reader) (response []byte, status int, err error) {
-	response = nil
-	status = 0
-
-	req, err := http.NewRequest(http.MethodGet, endpoint, body)
-	if err != nil {
-		return
-	}
-	req.Header.Add("Accept", "application/json")
-	req.Header.Add("Content-Type", "application/json")
-	resp, err := client.Do(req)
-	if err != nil {
-		return
-	}
-	defer resp.Body.Close()
-	response, err = io.ReadAll(resp.Body)
-	status = resp.StatusCode
-	return
-}
diff --git a/tracing/jaeger/model/types.go b/tracing/jaeger/model/types.go
index 20d8a76e0..80cb99389 100644
--- a/tracing/jaeger/model/types.go
+++ b/tracing/jaeger/model/types.go
@@ -1,6 +1,9 @@
 package model
 
 import (
+	"net/url"
+	"time"
+
 	jaegerModels "github.com/kiali/kiali/tracing/jaeger/model/json"
 )
 
@@ -33,3 +36,41 @@ type TracingSpan struct {
 type Services struct {
 	Data []string `json:"data"`
 }
+
+type ParsedUrl struct {
+	BaseUrl string   `json:"baseUrl"`
+	Host    string   `json:"host"`
+	Path    string   `json:"path"`
+	Port    string   `json:"port"`
+	Scheme  string   `json:"scheme"`
+	Url     *url.URL `json:"url,omitempty"`
+}
+
+type ValidConfig struct {
+	NamespaceSelector bool   `json:"namespaceSelector"`
+	Provider          string `json:"provider"`
+	Url               string `json:"url"`
+	UseGRPC           bool   `json:"useGRPC"`
+	Warning           string `json:"warning"`
+}
+
+type TracingDiagnose struct {
+	Code        int           `json:"code"`
+	LogLine     []LogLine     `json:"logLine"`
+	Message     string        `json:"message"`
+	ValidConfig []ValidConfig `json:"validConfig"`
+}
+
+type TracingService struct {
+	Data   []string          `json:"data"`
+	Total  int               `json:"total"`
+	Limit  int               `json:"limit"`
+	Offset int               `json:"offset"`
+	Errors []StructuredError `json:"errors"`
+}
+
+type LogLine struct {
+	Result string    `json:"result"`
+	Test   string    `json:"test"`
+	Time   time.Time `json:"time"`
+}
diff --git a/tracing/otel/util.go b/tracing/otel/util.go
new file mode 100644
index 000000000..b97b8834a
--- /dev/null
+++ b/tracing/otel/util.go
@@ -0,0 +1,31 @@
+package otel
+
+import (
+	"context"
+	"io"
+	"net/http"
+)
+
+func MakeRequest(ctx context.Context, client http.Client, endpoint string, body io.Reader) (responseBody []byte, status int, err error) {
+	ctxCancel, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	var req *http.Request
+
+	req, err = http.NewRequestWithContext(ctxCancel, http.MethodGet, endpoint, body)
+	if err != nil {
+		return
+	}
+	req.Header.Add("Accept", "application/json")
+	req.Header.Add("Content-Type", "application/json")
+
+	var resp *http.Response
+	resp, err = client.Do(req)
+	if err != nil {
+		return
+	}
+	defer resp.Body.Close()
+	responseBody, err = io.ReadAll(resp.Body)
+	status = resp.StatusCode
+	return responseBody, resp.StatusCode, err
+}
diff --git a/tracing/tempo/http_client.go b/tracing/tempo/http_client.go
index 0d36ab8e1..32da767b9 100644
--- a/tracing/tempo/http_client.go
+++ b/tracing/tempo/http_client.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
 	"net/http"
 	"net/url"
 	"path"
@@ -18,9 +17,10 @@ import (
 	"github.com/kiali/kiali/store"
 	"github.com/kiali/kiali/tracing/jaeger/model"
 	jaegerModels "github.com/kiali/kiali/tracing/jaeger/model/json"
-	otel "github.com/kiali/kiali/tracing/otel/model"
+	"github.com/kiali/kiali/tracing/otel"
+	otelModel "github.com/kiali/kiali/tracing/otel/model"
 	"github.com/kiali/kiali/tracing/otel/model/converter"
-	otelModels "github.com/kiali/kiali/tracing/otel/model/json"
+	otelJson "github.com/kiali/kiali/tracing/otel/model/json"
 	"github.com/kiali/kiali/util"
 )
 
@@ -76,7 +76,7 @@ func (oc *OtelHTTPClient) GetTraceDetailHTTP(ctx context.Context, client http.Cl
 	zl := getLoggerFromContextHTTPTempo(ctx)
 
 	u.Path = path.Join(u.Path, "/api/traces/", traceID)
-	resp, code, reqError := makeRequest(client, u.String(), nil)
+	resp, code, reqError := otel.MakeRequest(ctx, client, u.String(), nil)
 	if reqError != nil {
 		zl.Error().Msgf("[HTTP Tempo] API Tempo query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return nil, reqError
@@ -129,7 +129,7 @@ func (oc *OtelHTTPClient) GetServiceStatusHTTP(ctx context.Context, client http.
 		u.Path = path.Join(u.Path, "/status/services")
 	}
 
-	_, status, reqError := makeRequest(client, u.String(), nil)
+	_, status, reqError := otel.MakeRequest(ctx, client, u.String(), nil)
 	if status != 200 {
 		return false, fmt.Errorf("[HTTP Tempo] Error %d getting status services", status)
 	}
@@ -149,7 +149,7 @@ func (oc *OtelHTTPClient) queryTracesHTTP(ctx context.Context, client http.Clien
 		query.Set("minDuration", minDuration)
 		u.RawQuery = query.Encode()
 	}
-	resp, code, reqError := makeRequest(client, u.String(), nil)
+	resp, code, reqError := otel.MakeRequest(ctx, client, u.String(), nil)
 	if reqError != nil {
 		zl.Error().Msgf("Tempo API query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return &model.TracingResponse{}, reqError
@@ -169,7 +169,7 @@ func (oc *OtelHTTPClient) queryTracesHTTP(ctx context.Context, client http.Clien
 }
 
 // transformTrace processes every trace ID and make a request to get all the spans for that trace
-func (oc *OtelHTTPClient) transformTrace(ctx context.Context, traces *otel.Traces, error string, limit int) (*model.TracingResponse, error) {
+func (oc *OtelHTTPClient) transformTrace(ctx context.Context, traces *otelModel.Traces, error string, limit int) (*model.TracingResponse, error) {
 	var response model.TracingResponse
 	serviceName := ""
 
@@ -198,8 +198,8 @@ func (oc *OtelHTTPClient) transformTrace(ctx context.Context, traces *otel.Trace
 	return &response, nil
 }
 
-func unmarshal(ctx context.Context, r []byte, u *url.URL) (*otel.Traces, error) {
-	var response otel.Traces
+func unmarshal(ctx context.Context, r []byte, u *url.URL) (*otelModel.Traces, error) {
+	var response otelModel.Traces
 	if errMarshal := json.Unmarshal(r, &response); errMarshal != nil {
 		getLoggerFromContextHTTPTempo(ctx).Error().Msgf("[HTTP Tempo] Error unmarshalling Tempo API response: %s [URL: %v]", errMarshal, u)
 		return nil, errMarshal
@@ -208,8 +208,8 @@ func unmarshal(ctx context.Context, r []byte, u *url.URL) (*otel.Traces, error)
 	return &response, nil
 }
 
-func unmarshalSingleTrace(ctx context.Context, r []byte, u *url.URL) (*otelModels.Data, error) {
-	var response otelModels.Data
+func unmarshalSingleTrace(ctx context.Context, r []byte, u *url.URL) (*otelJson.Data, error) {
+	var response otelJson.Data
 	if errMarshal := json.Unmarshal(r, &response); errMarshal != nil {
 		getLoggerFromContextHTTPTempo(ctx).Error().Msgf("[HTTP Tempo] Error unmarshalling Tempo API Single trace response: %s [URL: %v]", errMarshal, u)
 		return nil, errMarshal
@@ -219,7 +219,7 @@ func unmarshalSingleTrace(ctx context.Context, r []byte, u *url.URL) (*otelModel
 }
 
 // convertBatchTrace Convert a trace returned by TraceQL query into a jaeger Trace
-func convertBatchTrace(trace otel.Trace, serviceName string) (jaegerModels.Trace, error) {
+func convertBatchTrace(trace otelModel.Trace, serviceName string) (jaegerModels.Trace, error) {
 
 	var jaegerModel jaegerModels.Trace
 
@@ -235,7 +235,7 @@ func convertBatchTrace(trace otel.Trace, serviceName string) (jaegerModels.Trace
 }
 
 // convertSingleTrace Convert a single trace returned by the TraceQL search endpoint
-func convertSingleTrace(traces *otelModels.Data, id string) (*model.TracingResponse, error) {
+func convertSingleTrace(traces *otelJson.Data, id string) (*model.TracingResponse, error) {
 	var response model.TracingResponse
 	var jaegerModel jaegerModels.Trace
 	tracingServiceName := ""
@@ -304,27 +304,7 @@ func (oc *OtelHTTPClient) GetTraceQLQuery(ctx context.Context, u *url.URL, traci
 	return u.RawQuery
 }
 
-func makeRequest(client http.Client, endpoint string, body io.Reader) (response []byte, status int, err error) {
-	response = nil
-	status = 0
-
-	req, err := http.NewRequest(http.MethodGet, endpoint, body)
-	if err != nil {
-		return
-	}
-	req.Header.Add("Accept", "application/json")
-	req.Header.Add("Content-Type", "application/json")
-	resp, err := client.Do(req)
-	if err != nil {
-		return
-	}
-	defer resp.Body.Close()
-	response, err = io.ReadAll(resp.Body)
-	status = resp.StatusCode
-	return
-}
-
-func hasErrors(trace otel.Trace) bool {
+func hasErrors(trace otelModel.Trace) bool {
 	for _, span := range trace.SpanSet.Spans {
 		for _, atb := range span.Attributes {
 			if atb.Key == "status" && atb.Value.StringValue == "error" {
@@ -338,7 +318,7 @@ func hasErrors(trace otel.Trace) bool {
 	return false
 }
 
-func getServiceName(attributes []otelModels.Attribute) string {
+func getServiceName(attributes []otelJson.Attribute) string {
 	for _, attb := range attributes {
 		if attb.Key == "service.name" {
 			return attb.Value.StringValue



Test patch:
diff --git a/business/test_util.go b/business/test_util.go
index b10a6395d..b568e3cdc 100644
--- a/business/test_util.go
+++ b/business/test_util.go
@@ -1076,6 +1076,7 @@ func FakeZtunnelDaemonSet(conf *config.Config) []apps_v1.DaemonSet {
 	conf.KubernetesConfig.ExcludeWorkloads = []string{}
 
 	appLabel := conf.IstioLabels.AppLabelName
+	kubernetesLabel := config.KubernetesAppLabel
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
 	return []apps_v1.DaemonSet{
 		{
@@ -1087,6 +1088,7 @@ func FakeZtunnelDaemonSet(conf *config.Config) []apps_v1.DaemonSet {
 				Name:              "ztunnel",
 				Namespace:         "istio-system",
 				CreationTimestamp: meta_v1.NewTime(t1),
+				Labels:            map[string]string{kubernetesLabel: "ztunnel"},
 			},
 			Spec: apps_v1.DaemonSetSpec{
 				Template: core_v1.PodTemplateSpec{
diff --git a/cache/cache_test.go b/cache/cache_test.go
index db68f8642..319f73953 100644
--- a/cache/cache_test.go
+++ b/cache/cache_test.go
@@ -78,6 +78,7 @@ func ztunnelDaemonSet() *apps_v1.DaemonSet {
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "ztunnel",
 			Namespace: "istio-system",
+			Labels:    map[string]string{"app.kubernetes.io/name": "ztunnel"},
 		},
 		Spec: apps_v1.DaemonSetSpec{
 			Selector: &metav1.LabelSelector{
diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected
index 1f5aeab3a..73989b271 100644
--- a/mesh/api/testdata/test_mesh_graph.expected
+++ b/mesh/api/testdata/test_mesh_graph.expected
@@ -1,616 +1,537 @@
 {
-  "elements": {
-    "nodes": [
-      {
-        "data": {
-          "id": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-          "cluster": "_external_",
-          "infraName": "External Deployments",
-          "infraType": "cluster",
-          "namespace": "",
-          "nodeType": "box",
-          "healthData": "Healthy",
-          "isBox": "cluster",
-          "isExternal": true,
-          "isInaccessible": true
-        }
-      },
-      {
-        "data": {
-          "id": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-          "cluster": "cluster-primary",
-          "infraName": "cluster-primary",
-          "infraType": "cluster",
-          "namespace": "",
-          "nodeType": "box",
-          "healthData": "Healthy",
-          "infraData": {
-            "apiEndpoint": "http://127.0.0.2:9443",
-            "isKialiHome": true,
-            "kialiInstances": [
-              {
-                "namespace": "istio-system",
-                "operatorResource": "",
-                "serviceName": "kiali",
-                "url": "",
-                "version": ""
-              }
-            ],
-            "name": "cluster-primary",
-            "secretName": "",
-            "accessible": true
+  "elements" : {
+    "nodes" : [ {
+      "data" : {
+        "id" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+        "cluster" : "_external_",
+        "infraName" : "External Deployments",
+        "infraType" : "cluster",
+        "namespace" : "",
+        "nodeType" : "box",
+        "healthData" : "Healthy",
+        "isBox" : "cluster",
+        "isExternal" : true,
+        "isInaccessible" : true
+      }
+    }, {
+      "data" : {
+        "id" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+        "cluster" : "cluster-primary",
+        "infraName" : "cluster-primary",
+        "infraType" : "cluster",
+        "namespace" : "",
+        "nodeType" : "box",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "apiEndpoint" : "http://127.0.0.2:9443",
+          "isKialiHome" : true,
+          "kialiInstances" : [ {
+            "namespace" : "istio-system",
+            "operatorResource" : "",
+            "serviceName" : "kiali",
+            "url" : "",
+            "version" : ""
+          } ],
+          "name" : "cluster-primary",
+          "secretName" : "",
+          "accessible" : true
+        },
+        "isBox" : "cluster",
+        "version" : "Unknown"
+      }
+    }, {
+      "data" : {
+        "id" : "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
+        "cluster" : "cluster-remote",
+        "infraName" : "cluster-remote",
+        "infraType" : "cluster",
+        "namespace" : "",
+        "nodeType" : "box",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "apiEndpoint" : "",
+          "isKialiHome" : false,
+          "kialiInstances" : null,
+          "name" : "cluster-remote",
+          "secretName" : "",
+          "accessible" : true
+        },
+        "isBox" : "cluster"
+      }
+    }, {
+      "data" : {
+        "id" : "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
+        "parent" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+        "cluster" : "cluster-primary",
+        "infraName" : "data-plane-1",
+        "infraType" : "namespace",
+        "namespace" : "data-plane-1",
+        "nodeType" : "box",
+        "healthData" : null,
+        "isBox" : "namespace"
+      }
+    }, {
+      "data" : {
+        "id" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+        "parent" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+        "cluster" : "cluster-primary",
+        "infraName" : "istio-system",
+        "infraType" : "namespace",
+        "namespace" : "istio-system",
+        "nodeType" : "box",
+        "healthData" : null,
+        "isBox" : "namespace"
+      }
+    }, {
+      "data" : {
+        "id" : "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0",
+        "parent" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+        "cluster" : "_external_",
+        "infraName" : "Grafana",
+        "infraType" : "grafana",
+        "namespace" : "",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "Auth" : {
+            "CAFile" : "xxx",
+            "InsecureSkipVerify" : false,
+            "Password" : "xxx",
+            "Token" : "xxx",
+            "Type" : "none",
+            "UseKialiToken" : false,
+            "Username" : "xxx"
           },
-          "isBox": "cluster",
-          "version": "Unknown"
-        }
-      },
-      {
-        "data": {
-          "id": "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
-          "cluster": "cluster-remote",
-          "infraName": "cluster-remote",
-          "infraType": "cluster",
-          "namespace": "",
-          "nodeType": "box",
-          "healthData": "Healthy",
-          "infraData": {
-            "apiEndpoint": "",
-            "isKialiHome": false,
-            "kialiInstances": null,
-            "name": "cluster-remote",
-            "secretName": "",
-            "accessible": true
+          "Dashboards" : null,
+          "Enabled" : true,
+          "ExternalURL" : "",
+          "HealthCheckUrl" : "",
+          "InternalURL" : "http://grafana.istio-system:3000",
+          "IsCore" : false
+        },
+        "isExternal" : true,
+        "isInaccessible" : true
+      }
+    }, {
+      "data" : {
+        "id" : "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686",
+        "parent" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+        "cluster" : "_external_",
+        "infraName" : "Prometheus",
+        "infraType" : "metricStore",
+        "namespace" : "",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "Auth" : {
+            "CAFile" : "xxx",
+            "InsecureSkipVerify" : false,
+            "Password" : "xxx",
+            "Token" : "xxx",
+            "Type" : "none",
+            "UseKialiToken" : false,
+            "Username" : "xxx"
           },
-          "isBox": "cluster"
-        }
-      },
-      {
-        "data": {
-          "id": "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
-          "parent": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-          "cluster": "cluster-primary",
-          "infraName": "data-plane-1",
-          "infraType": "namespace",
-          "namespace": "data-plane-1",
-          "nodeType": "box",
-          "healthData": null,
-          "isBox": "namespace"
-        }
-      },
-      {
-        "data": {
-          "id": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-          "parent": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-          "cluster": "cluster-primary",
-          "infraName": "istio-system",
-          "infraType": "namespace",
-          "namespace": "istio-system",
-          "nodeType": "box",
-          "healthData": null,
-          "isBox": "namespace"
-        }
-      },
-      {
-        "data": {
-          "id": "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0",
-          "parent": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-          "cluster": "_external_",
-          "infraName": "Grafana",
-          "infraType": "grafana",
-          "namespace": "",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "Auth": {
-              "CAFile": "xxx",
-              "InsecureSkipVerify": false,
-              "Password": "xxx",
-              "Token": "xxx",
-              "Type": "none",
-              "UseKialiToken": false,
-              "Username": "xxx"
-            },
-            "Dashboards": null,
-            "Enabled": true,
-            "ExternalURL": "",
-            "HealthCheckUrl": "",
-            "InternalURL": "http://grafana.istio-system:3000",
-            "IsCore": false
+          "CacheDuration" : 7,
+          "CacheEnabled" : true,
+          "CacheExpiration" : 300,
+          "CustomHeaders" : { },
+          "HealthCheckUrl" : "",
+          "IsCore" : false,
+          "QueryScope" : { },
+          "ThanosProxy" : {
+            "Enabled" : false,
+            "RetentionPeriod" : "7d",
+            "ScrapeInterval" : "30s"
           },
-          "isExternal": true,
-          "isInaccessible": true
-        }
-      },
-      {
-        "data": {
-          "id": "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686",
-          "parent": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-          "cluster": "_external_",
-          "infraName": "Prometheus",
-          "infraType": "metricStore",
-          "namespace": "",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "Auth": {
-              "CAFile": "xxx",
-              "InsecureSkipVerify": false,
-              "Password": "xxx",
-              "Token": "xxx",
-              "Type": "none",
-              "UseKialiToken": false,
-              "Username": "xxx"
-            },
-            "CacheDuration": 7,
-            "CacheEnabled": true,
-            "CacheExpiration": 300,
-            "CustomHeaders": {},
-            "HealthCheckUrl": "",
-            "IsCore": false,
-            "QueryScope": {},
-            "ThanosProxy": {
-              "Enabled": false,
-              "RetentionPeriod": "7d",
-              "ScrapeInterval": "30s"
-            },
-            "URL": "http://prometheus.istio-system:9090"
+          "URL" : "http://prometheus.istio-system:9090"
+        },
+        "isExternal" : true,
+        "isInaccessible" : true
+      }
+    }, {
+      "data" : {
+        "id" : "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f",
+        "parent" : "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
+        "cluster" : "_external_",
+        "infraName" : "jaeger",
+        "infraType" : "traceStore",
+        "namespace" : "",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "Auth" : {
+            "CAFile" : "xxx",
+            "InsecureSkipVerify" : false,
+            "Password" : "xxx",
+            "Token" : "xxx",
+            "Type" : "none",
+            "UseKialiToken" : false,
+            "Username" : "xxx"
           },
-          "isExternal": true,
-          "isInaccessible": true
-        }
-      },
-      {
-        "data": {
-          "id": "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f",
-          "parent": "85774128be9f0db40aeb4c610e9436f63cb0945838e6db0c05ae36b62c6c6c10",
-          "cluster": "_external_",
-          "infraName": "jaeger",
-          "infraType": "traceStore",
-          "namespace": "",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "Auth": {
-              "CAFile": "xxx",
-              "InsecureSkipVerify": false,
-              "Password": "xxx",
-              "Token": "xxx",
-              "Type": "none",
-              "UseKialiToken": false,
-              "Username": "xxx"
-            },
-            "CustomHeaders": {},
-            "DisableVersionCheck": false,
-            "Enabled": true,
-            "ExternalURL": "",
-            "HealthCheckUrl": "",
-            "GrpcPort": 9095,
-            "InternalURL": "http://tracing.istio-system:16685/jaeger",
-            "IsCore": false,
-            "Provider": "jaeger",
-            "TempoConfig": {
-              "cache_capacity": 200,
-              "cache_enabled": true
-            },
-            "NamespaceSelector": true,
-            "QueryScope": {},
-            "QueryTimeout": 5,
-            "UseGRPC": true,
-            "WhiteListIstioSystem": [
-              "jaeger-query",
-              "istio-ingressgateway"
-            ]
+          "CustomHeaders" : { },
+          "DisableVersionCheck" : false,
+          "Enabled" : true,
+          "ExternalURL" : "",
+          "HealthCheckUrl" : "",
+          "GrpcPort" : 9095,
+          "InternalURL" : "http://tracing.istio-system:16685/jaeger",
+          "IsCore" : false,
+          "Provider" : "jaeger",
+          "TempoConfig" : {
+            "cache_capacity" : 200,
+            "cache_enabled" : true
           },
-          "isExternal": true,
-          "isInaccessible": true
-        }
-      },
-      {
-        "data": {
-          "id": "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c",
-          "parent": "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
-          "cluster": "cluster-primary",
-          "infraName": "Data Plane",
-          "infraType": "dataplane",
-          "namespace": "",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": [
-            {
-              "name": "data-plane-1",
-              "cluster": "cluster-primary",
-              "isAmbient": false,
-              "labels": {
-                "istio-injection": "enabled",
-                "kubernetes.io/metadata.name": "data-plane-1"
-              },
-              "annotations": null,
-              "revision": "default"
-            },
-            {
-              "name": "data-plane-2",
-              "cluster": "cluster-primary",
-              "isAmbient": false,
-              "labels": {
-                "istio.io/rev": "default",
-                "kubernetes.io/metadata.name": "data-plane-2"
-              },
-              "annotations": null,
-              "revision": "default"
-            }
-          ],
-          "version": "default"
-        }
-      },
-      {
-        "data": {
-          "id": "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f",
-          "parent": "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
-          "cluster": "cluster-primary",
-          "infraName": "waypoint",
-          "infraType": "waypoint",
-          "namespace": "data-plane-1",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "Annotations": {},
-            "Labels": {
-              "gateway.istio.io/managed": "istio.io-mesh-controller",
-              "gateway.networking.k8s.io/gateway-name": "waypoint"
-            },
-            "TemplateAnnotations": null,
-            "TemplateLabels": {
-              "gateway.istio.io/managed": "istio.io-mesh-controller",
-              "gateway.networking.k8s.io/gateway-name": "waypoint"
-            }
+          "NamespaceSelector" : true,
+          "QueryScope" : { },
+          "QueryTimeout" : 5,
+          "UseGRPC" : true,
+          "WhiteListIstioSystem" : [ "jaeger-query", "istio-ingressgateway" ]
+        },
+        "isExternal" : true,
+        "isInaccessible" : true
+      }
+    }, {
+      "data" : {
+        "id" : "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c",
+        "parent" : "52e2139bb2622c3156273a148bb95c794e674e820ffcf433a3e530e31e5fcd36",
+        "cluster" : "cluster-primary",
+        "infraName" : "Data Plane",
+        "infraType" : "dataplane",
+        "namespace" : "",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : [ {
+          "name" : "data-plane-1",
+          "cluster" : "cluster-primary",
+          "isAmbient" : false,
+          "labels" : {
+            "istio-injection" : "enabled",
+            "kubernetes.io/metadata.name" : "data-plane-1"
           },
-          "version": "default"
-        }
-      },
-      {
-        "data": {
-          "id": "1cf59ab6492289f4c7371ad6c0de54505a037f949855c902b5547fa6f15e3674",
-          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-          "cluster": "cluster-primary",
-          "infraName": "ztunnel",
-          "infraType": "ztunnel",
-          "namespace": "istio-system",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "Annotations": null,
-            "Labels": {
-              "app": "ztunnel"
-            },
-            "TemplateAnnotations": null,
-            "TemplateLabels": {
-              "app": "ztunnel"
-            }
+          "annotations" : null,
+          "revision" : "default"
+        }, {
+          "name" : "data-plane-2",
+          "cluster" : "cluster-primary",
+          "isAmbient" : false,
+          "labels" : {
+            "istio.io/rev" : "default",
+            "kubernetes.io/metadata.name" : "data-plane-2"
           },
-          "version": "default"
-        }
-      },
-      {
-        "data": {
-          "id": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-          "cluster": "cluster-primary",
-          "infraName": "kiali",
-          "infraType": "kiali",
-          "namespace": "istio-system",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "ComponentStatuses": {
-              "Enabled": true,
-              "Components": []
-            },
-            "ConfigMapName": "",
-            "EnvoyAdminLocalPort": 15000,
-            "GatewayAPIClasses": [],
-            "GatewayAPIClassesLabelSelector": "",
-            "IstioAPIEnabled": true,
-            "IstioIdentityDomain": "svc.cluster.local",
-            "IstioInjectionAnnotation": "sidecar.istio.io/inject",
-            "IstioSidecarInjectorConfigMapName": "",
-            "IstioSidecarAnnotation": "sidecar.istio.io/status",
-            "IstiodDeploymentName": "",
-            "IstiodPodMonitoringPort": 15014,
-            "IstiodPollingIntervalSeconds": 20,
-            "Registry": null,
-            "RootNamespace": "istio-system",
-            "UrlServiceVersion": "",
-            "ValidationChangeDetectionEnabled": true,
-            "ValidationReconcileInterval": 60000000000
+          "annotations" : null,
+          "revision" : "default"
+        } ],
+        "version" : "default"
+      }
+    }, {
+      "data" : {
+        "id" : "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f",
+        "parent" : "5d47c4043773968316f9d71a07212a1c363d76a3d9ffe7c52198f864cde8c5f5",
+        "cluster" : "cluster-primary",
+        "infraName" : "waypoint",
+        "infraType" : "waypoint",
+        "namespace" : "data-plane-1",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "Annotations" : { },
+          "Labels" : {
+            "gateway.istio.io/managed" : "istio.io-mesh-controller",
+            "gateway.networking.k8s.io/gateway-name" : "waypoint"
+          },
+          "TemplateAnnotations" : null,
+          "TemplateLabels" : {
+            "gateway.istio.io/managed" : "istio.io-mesh-controller",
+            "gateway.networking.k8s.io/gateway-name" : "waypoint"
           }
-        }
-      },
-      {
-        "data": {
-          "id": "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9",
-          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-          "cluster": "cluster-primary",
-          "infraName": "gateway",
-          "infraType": "gateway",
-          "namespace": "istio-system",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "kind": "Gateway",
-            "apiVersion": "networking.istio.io/v1",
-            "metadata": {
-              "name": "gateway",
-              "namespace": "istio-system",
-              "resourceVersion": "999",
-              "creationTimestamp": null
-            },
-            "spec": {
-              "selector": {
-                "istio": "ingressgateway"
-              }
-            },
-            "status": {}
+        },
+        "version" : "default"
+      }
+    }, {
+      "data" : {
+        "id" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+        "parent" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+        "cluster" : "cluster-primary",
+        "infraName" : "kiali",
+        "infraType" : "kiali",
+        "namespace" : "istio-system",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "ComponentStatuses" : {
+            "Enabled" : true,
+            "Components" : [ ]
           },
-          "version": "default"
+          "ConfigMapName" : "",
+          "EnvoyAdminLocalPort" : 15000,
+          "GatewayAPIClasses" : [ ],
+          "GatewayAPIClassesLabelSelector" : "",
+          "IstioAPIEnabled" : true,
+          "IstioIdentityDomain" : "svc.cluster.local",
+          "IstioInjectionAnnotation" : "sidecar.istio.io/inject",
+          "IstioSidecarInjectorConfigMapName" : "",
+          "IstioSidecarAnnotation" : "sidecar.istio.io/status",
+          "IstiodDeploymentName" : "",
+          "IstiodPodMonitoringPort" : 15014,
+          "IstiodPollingIntervalSeconds" : 20,
+          "Registry" : null,
+          "RootNamespace" : "istio-system",
+          "UrlServiceVersion" : "",
+          "ValidationChangeDetectionEnabled" : true,
+          "ValidationReconcileInterval" : 60000000000
         }
-      },
-      {
-        "data": {
-          "id": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-          "parent": "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
-          "cluster": "cluster-primary",
-          "infraName": "istiod",
-          "infraType": "istiod",
-          "namespace": "istio-system",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": {
-            "cluster": {
-              "apiEndpoint": "http://127.0.0.2:9443",
-              "isKialiHome": true,
-              "kialiInstances": [
-                {
-                  "namespace": "istio-system",
-                  "operatorResource": "",
-                  "serviceName": "kiali",
-                  "url": "",
-                  "version": ""
-                }
-              ],
-              "name": "cluster-primary",
-              "secretName": "",
-              "accessible": true
-            },
-            "config": {
-              "certificates": [
-                {
-                  "dnsNames": null,
-                  "configMapName": "istio-ca-root-cert",
-                  "issuer": "O=cluster.local",
-                  "notBefore": "2021-07-27T14:37:00Z",
-                  "notAfter": "2031-07-25T14:37:00Z",
-                  "error": "",
-                  "accessible": true,
-                  "cluster": ""
-                }
-              ],
-              "effectiveConfig": {
-                "configMap": {
-                  "mesh": {
-                    "accessLogFile": "/dev/stdout",
-                    "enableAutoMtls": true,
-                    "trustDomain": "cluster.local",
-                    "rootNamespace": "istio-system"
-                  }
+      }
+    }, {
+      "data" : {
+        "id" : "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9",
+        "parent" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+        "cluster" : "cluster-primary",
+        "infraName" : "gateway",
+        "infraType" : "gateway",
+        "namespace" : "istio-system",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "kind" : "Gateway",
+          "apiVersion" : "networking.istio.io/v1",
+          "metadata" : {
+            "name" : "gateway",
+            "namespace" : "istio-system",
+            "resourceVersion" : "999",
+            "creationTimestamp" : null
+          },
+          "spec" : {
+            "selector" : {
+              "istio" : "ingressgateway"
+            }
+          },
+          "status" : { }
+        },
+        "version" : "default"
+      }
+    }, {
+      "data" : {
+        "id" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+        "parent" : "d267c54a08bd79c4a07c88b2e0d93b5c93962da0703c7bfd70d1ead571b067f0",
+        "cluster" : "cluster-primary",
+        "infraName" : "istiod",
+        "infraType" : "istiod",
+        "namespace" : "istio-system",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : {
+          "cluster" : {
+            "apiEndpoint" : "http://127.0.0.2:9443",
+            "isKialiHome" : true,
+            "kialiInstances" : [ {
+              "namespace" : "istio-system",
+              "operatorResource" : "",
+              "serviceName" : "kiali",
+              "url" : "",
+              "version" : ""
+            } ],
+            "name" : "cluster-primary",
+            "secretName" : "",
+            "accessible" : true
+          },
+          "config" : {
+            "certificates" : [ {
+              "dnsNames" : null,
+              "configMapName" : "istio-ca-root-cert",
+              "issuer" : "O=cluster.local",
+              "notBefore" : "2021-07-27T14:37:00Z",
+              "notAfter" : "2031-07-25T14:37:00Z",
+              "error" : "",
+              "accessible" : true,
+              "cluster" : ""
+            } ],
+            "effectiveConfig" : {
+              "configMap" : {
+                "mesh" : {
+                  "accessLogFile" : "/dev/stdout",
+                  "enableAutoMtls" : true,
+                  "trustDomain" : "cluster.local",
+                  "rootNamespace" : "istio-system"
                 }
-              },
-              "network": "kialiNetwork",
-              "standardConfig": {
-              "cluster": "cluster-primary",
-                "configMap": {
-                  "mesh": {
-                    "accessLogFile": "/dev/stdout",
-                    "enableAutoMtls": true,
-                    "trustDomain": "cluster.local",
-                    "rootNamespace": "istio-system"
-                  }
-                },
-                "name": "istio",
-                "namespace": "istio-system"
               }
             },
-            "externalControlPlane": false,
-            "id": "cluster-primary",
-            "istiodName": "istiod",
-            "istiodNamespace": "istio-system",
-            "managedClusters": [
-              {
-                "apiEndpoint": "http://127.0.0.2:9443",
-                "isKialiHome": true,
-                "kialiInstances": [
-                  {
-                    "namespace": "istio-system",
-                    "operatorResource": "",
-                    "serviceName": "kiali",
-                    "url": "",
-                    "version": ""
-                  }
-                ],
-                "name": "cluster-primary",
-                "secretName": "",
-                "accessible": true
-              },
-              {
-                "apiEndpoint": "",
-                "isKialiHome": false,
-                "kialiInstances": null,
-                "name": "cluster-remote",
-                "secretName": "",
-                "accessible": true
-              }
-            ],
-            "managesExternal": true,
-            "managedNamespaces": [
-              {
-                "name": "data-plane-1",
-                "cluster": "cluster-primary",
-                "isAmbient": false,
-                "labels": {
-                  "istio-injection": "enabled",
-                  "kubernetes.io/metadata.name": "data-plane-1"
-                },
-                "annotations": null,
-                "revision": "default"
-              },
-              {
-                "name": "data-plane-2",
-                "cluster": "cluster-primary",
-                "isAmbient": false,
-                "labels": {
-                  "istio.io/rev": "default",
-                  "kubernetes.io/metadata.name": "data-plane-2"
-                },
-                "annotations": null,
-                "revision": "default"
-              },
-              {
-                "name": "data-plane-3",
-                "cluster": "cluster-remote",
-                "isAmbient": false,
-                "labels": {
-                  "istio-injection": "enabled",
-                  "kubernetes.io/metadata.name": "data-plane-3"
-                },
-                "annotations": null,
-                "revision": "default"
+            "network" : "kialiNetwork",
+            "standardConfig" : {
+              "cluster" : "cluster-primary",
+              "configMap" : {
+                "mesh" : {
+                  "accessLogFile" : "/dev/stdout",
+                  "enableAutoMtls" : true,
+                  "trustDomain" : "cluster.local",
+                  "rootNamespace" : "istio-system"
+                }
               },
-              {
-                "name": "data-plane-4",
-                "cluster": "cluster-remote",
-                "isAmbient": false,
-                "labels": {
-                  "istio.io/rev": "default",
-                  "kubernetes.io/metadata.name": "data-plane-4"
-                },
-                "annotations": null,
-                "revision": "default"
-              }
-            ],
-            "resources": {},
-            "revision": "default",
-            "status": "",
-            "thresholds": {
-              "memory": 0,
-              "cpu": 0
+              "name" : "istio",
+              "namespace" : "istio-system"
             }
           },
-          "version": "Unknown"
-        }
-      },
-      {
-        "data": {
-          "id": "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405",
-          "parent": "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
-          "cluster": "cluster-remote",
-          "infraName": "Data Plane",
-          "infraType": "dataplane",
-          "namespace": "",
-          "nodeType": "infra",
-          "healthData": "Healthy",
-          "infraData": [
-            {
-              "name": "data-plane-3",
-              "cluster": "cluster-remote",
-              "isAmbient": false,
-              "labels": {
-                "istio-injection": "enabled",
-                "kubernetes.io/metadata.name": "data-plane-3"
-              },
-              "annotations": null,
-              "revision": "default"
+          "externalControlPlane" : false,
+          "id" : "cluster-primary",
+          "istiodName" : "istiod",
+          "istiodNamespace" : "istio-system",
+          "managedClusters" : [ {
+            "apiEndpoint" : "http://127.0.0.2:9443",
+            "isKialiHome" : true,
+            "kialiInstances" : [ {
+              "namespace" : "istio-system",
+              "operatorResource" : "",
+              "serviceName" : "kiali",
+              "url" : "",
+              "version" : ""
+            } ],
+            "name" : "cluster-primary",
+            "secretName" : "",
+            "accessible" : true
+          }, {
+            "apiEndpoint" : "",
+            "isKialiHome" : false,
+            "kialiInstances" : null,
+            "name" : "cluster-remote",
+            "secretName" : "",
+            "accessible" : true
+          } ],
+          "managesExternal" : true,
+          "managedNamespaces" : [ {
+            "name" : "data-plane-1",
+            "cluster" : "cluster-primary",
+            "isAmbient" : false,
+            "labels" : {
+              "istio-injection" : "enabled",
+              "kubernetes.io/metadata.name" : "data-plane-1"
             },
-            {
-              "name": "data-plane-4",
-              "cluster": "cluster-remote",
-              "isAmbient": false,
-              "labels": {
-                "istio.io/rev": "default",
-                "kubernetes.io/metadata.name": "data-plane-4"
-              },
-              "annotations": null,
-              "revision": "default"
-            }
-          ],
-          "version": "default"
-        }
+            "annotations" : null,
+            "revision" : "default"
+          }, {
+            "name" : "data-plane-2",
+            "cluster" : "cluster-primary",
+            "isAmbient" : false,
+            "labels" : {
+              "istio.io/rev" : "default",
+              "kubernetes.io/metadata.name" : "data-plane-2"
+            },
+            "annotations" : null,
+            "revision" : "default"
+          }, {
+            "name" : "data-plane-3",
+            "cluster" : "cluster-remote",
+            "isAmbient" : false,
+            "labels" : {
+              "istio-injection" : "enabled",
+              "kubernetes.io/metadata.name" : "data-plane-3"
+            },
+            "annotations" : null,
+            "revision" : "default"
+          }, {
+            "name" : "data-plane-4",
+            "cluster" : "cluster-remote",
+            "isAmbient" : false,
+            "labels" : {
+              "istio.io/rev" : "default",
+              "kubernetes.io/metadata.name" : "data-plane-4"
+            },
+            "annotations" : null,
+            "revision" : "default"
+          } ],
+          "resources" : { },
+          "revision" : "default",
+          "status" : "",
+          "thresholds" : {
+            "memory" : 0,
+            "cpu" : 0
+          }
+        },
+        "version" : "Unknown"
       }
-    ],
-    "edges": [
-      {
-        "data": {
-          "id": "3c1e5459c5b6268c2f2452f3e6b620fd1ae0297b4621175c97e376558bea9d58",
-          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-          "target": "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0"
-        }
-      },
-      {
-        "data": {
-          "id": "3800bf57dd84bfc7bb159fc26de3fd964dc4faa7274b6250dcde483edeba542d",
-          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-          "target": "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686"
-        }
-      },
-      {
-        "data": {
-          "id": "3444b8aee4ed1e0a9d9388de97152e089e723e1efae8db5658d9558b12346ac4",
-          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-          "target": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2"
-        }
-      },
-      {
-        "data": {
-          "id": "6fb152c8305cf91514cbb0d86796cc62eb47e275cf803af3fbb26224c9ddf600",
-          "source": "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
-          "target": "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f"
-        }
-      },
-      {
-        "data": {
-          "id": "8213394da06023363e2d5747d26db6f7e23d0beeacc84fcf97119768822e6dc8",
-          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-          "target": "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c"
-        }
-      },
-      {
-        "data": {
-          "id": "7263daad487f2d7fb6e3b4225f94ccc140429de5456489175644410155bdaead",
-          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-          "target": "1cf59ab6492289f4c7371ad6c0de54505a037f949855c902b5547fa6f15e3674"
-        }
-      },
-      {
-        "data": {
-          "id": "ea8e134859bb70d340128c62d2468a12174517151b37014f82613d6a0957eb8e",
-          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-          "target": "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9"
-        }
-      },
-      {
-        "data": {
-          "id": "f25288d2a684e5be99e297439adad0d0f65e5893ba592b397f4841a42e3bab68",
-          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-          "target": "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f"
-        }
-      },
-      {
-        "data": {
-          "id": "1622664f016a337006cce1427c522c2e24c1b1b39548109f49ad478594f7d8ca",
-          "source": "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
-          "target": "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405"
-        }
+    }, {
+      "data" : {
+        "id" : "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405",
+        "parent" : "421c6926a61f0f21f1fe0babb48ea9fac6454c1d9a448dea2c559402723ec4da",
+        "cluster" : "cluster-remote",
+        "infraName" : "Data Plane",
+        "infraType" : "dataplane",
+        "namespace" : "",
+        "nodeType" : "infra",
+        "healthData" : "Healthy",
+        "infraData" : [ {
+          "name" : "data-plane-3",
+          "cluster" : "cluster-remote",
+          "isAmbient" : false,
+          "labels" : {
+            "istio-injection" : "enabled",
+            "kubernetes.io/metadata.name" : "data-plane-3"
+          },
+          "annotations" : null,
+          "revision" : "default"
+        }, {
+          "name" : "data-plane-4",
+          "cluster" : "cluster-remote",
+          "isAmbient" : false,
+          "labels" : {
+            "istio.io/rev" : "default",
+            "kubernetes.io/metadata.name" : "data-plane-4"
+          },
+          "annotations" : null,
+          "revision" : "default"
+        } ],
+        "version" : "default"
+      }
+    } ],
+    "edges" : [ {
+      "data" : {
+        "id" : "3c1e5459c5b6268c2f2452f3e6b620fd1ae0297b4621175c97e376558bea9d58",
+        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+        "target" : "33493098cdd63801f773396da680159d0bb57b482ef38a4b208899a2e477e1f0"
+      }
+    }, {
+      "data" : {
+        "id" : "3800bf57dd84bfc7bb159fc26de3fd964dc4faa7274b6250dcde483edeba542d",
+        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+        "target" : "41b50563edf837e2e828b50492b24356bf392be6e0a7a7f21d90eabe551dd686"
+      }
+    }, {
+      "data" : {
+        "id" : "3444b8aee4ed1e0a9d9388de97152e089e723e1efae8db5658d9558b12346ac4",
+        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+        "target" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2"
+      }
+    }, {
+      "data" : {
+        "id" : "6fb152c8305cf91514cbb0d86796cc62eb47e275cf803af3fbb26224c9ddf600",
+        "source" : "29c05b5dfa30d15d450e54f0651c03c68a25a78ae4dc8f292c6ceb5ce6be7e6d",
+        "target" : "e0733a58436c292ece7cbcf37db7b77510545c027d03c1798c3d6d8202b93f6f"
+      }
+    }, {
+      "data" : {
+        "id" : "8213394da06023363e2d5747d26db6f7e23d0beeacc84fcf97119768822e6dc8",
+        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+        "target" : "0bb699228f65bc52ec82ed3c72726df27b56e7518b38b7e2021196ea0065233c"
+      }
+    }, {
+      "data" : {
+        "id" : "ea8e134859bb70d340128c62d2468a12174517151b37014f82613d6a0957eb8e",
+        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+        "target" : "4ffd132efe1291fcd70490d6add887f9e160a72c4885a83669432bea09de23f9"
+      }
+    }, {
+      "data" : {
+        "id" : "f25288d2a684e5be99e297439adad0d0f65e5893ba592b397f4841a42e3bab68",
+        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+        "target" : "5d63e313a5acadb4bf9f5bcaa606b69bbaf81a4193a511719a878e9ad8729a5f"
+      }
+    }, {
+      "data" : {
+        "id" : "1622664f016a337006cce1427c522c2e24c1b1b39548109f49ad478594f7d8ca",
+        "source" : "9b5c974b8b5c42157a685413c86f2ff15ee4266f307f0c519d9acc50b6c97bc2",
+        "target" : "5ff0f691c850d727e75813fb484e06f5bad835b005b08caf2a62c0cff7cd5405"
       }
-    ]
+    } ]
   },
-  "meshName": "",
-  "timestamp": 1523364075
-}
\ No newline at end of file
+  "meshName" : "",
+  "timestamp" : 1523364075
+}
diff --git a/tracing/discovery_client_test.go b/tracing/discovery_client_test.go
new file mode 100644
index 000000000..3178981cb
--- /dev/null
+++ b/tracing/discovery_client_test.go
@@ -0,0 +1,320 @@
+package tracing
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"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/log"
+	"github.com/kiali/kiali/tracing/jaeger/model"
+	"github.com/kiali/kiali/tracing/otel"
+)
+
+func fakeK8sNs() kubernetes.UserClientInterface {
+	objects := []runtime.Object{
+		kubetest.FakeNamespace("bookinfo"),
+	}
+
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	k8s.OpenShift = false
+	return k8s
+}
+
+func TestCreateClient(t *testing.T) {
+
+	k8s := fakeK8sNs()
+	conf := config.Get()
+	tc, err := TestNewClient(context.TODO(), conf, k8s.GetToken())
+
+	assert.Nil(t, err)
+	assert.NotNil(t, tc)
+}
+
+func TestParseUrlValidWithPort(t *testing.T) {
+	url := "http://localhost:8080/test/path"
+
+	parsed, logs, err := parseUrl(url)
+
+	assert.Nil(t, err)
+	assert.Equal(t, "localhost", parsed.Host)
+	assert.Equal(t, "8080", parsed.Port)
+	assert.Equal(t, "http://localhost:8080", parsed.BaseUrl)
+	assert.Equal(t, "/test/path", parsed.Path)
+	assert.Equal(t, "http", parsed.Scheme)
+
+	assert.Len(t, logs, 1)
+	assert.Contains(t, logs[0].Result, "[Ok]")
+}
+
+func TestParseUrlValidWithoutPort(t *testing.T) {
+	url := "https://example.com/api"
+
+	parsed, logs, err := parseUrl(url)
+
+	assert.Nil(t, err)
+	assert.Equal(t, "example.com", parsed.Host)
+	assert.Equal(t, "", parsed.Port)
+	assert.Equal(t, "https://example.com", parsed.BaseUrl)
+	assert.Equal(t, "/api", parsed.Path)
+	assert.Equal(t, "https", parsed.Scheme)
+
+	assert.Len(t, logs, 1)
+	assert.Contains(t, logs[0].Result, "[Ok]")
+}
+
+type mockConn struct{}
+
+func (m *mockConn) Read(b []byte) (n int, err error)   { return 0, nil }
+func (m *mockConn) Write(b []byte) (n int, err error)  { return len(b), nil }
+func (m *mockConn) Close() error                       { return nil }
+func (m *mockConn) LocalAddr() net.Addr                { return nil }
+func (m *mockConn) RemoteAddr() net.Addr               { return nil }
+func (m *mockConn) SetDeadline(t time.Time) error      { return nil }
+func (m *mockConn) SetReadDeadline(t time.Time) error  { return nil }
+func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
+
+func TestDiscoverPortsWithDial(t *testing.T) {
+	mockDial := func(network, address string, timeout time.Duration) (net.Conn, error) {
+		// Simulate only port 80 and 443 being open
+		if address == "localhost:80" || address == "localhost:443" {
+			return &mockConn{}, nil
+		}
+		return nil, errors.New("connection refused")
+	}
+	zl := log.WithGroup(log.TracingLogName)
+	openPorts, logs := discoverPortsWithDial(zl, "localhost", mockDial)
+
+	expectedPorts := map[string]bool{
+		"80":  true,
+		"443": true,
+	}
+
+	assert.Equal(t, len(expectedPorts), 2)
+	assert.Equal(t, expectedPorts["80"], true)
+
+	for _, port := range openPorts {
+		assert.True(t, expectedPorts[port])
+		assert.True(t, expectedPorts[port])
+	}
+	assert.Equal(t, len(logs), 2)
+}
+
+func mockMakeRequest(ctx context.Context, client http.Client, endpoint string, body io.Reader) ([]byte, int, error) {
+	switch endpoint {
+	case "http://tempo-host:3100/api/search?q={}":
+		resp := []byte(`{"traces":[{"rootServiceName":"service.namespace"}]}`)
+		return resp, 200, nil
+	case "http://tempo-host:3100/api/services":
+		resp := []byte(`{"data":["service.namespace"]}`)
+		return resp, 200, nil
+	default:
+		return nil, 500, errors.New("mock error")
+	}
+}
+
+func TestValidateSimpleTempoHTTP(t *testing.T) {
+	// Override the global MakeRequest with the mock version
+	MakeRequestFunc = mockMakeRequest
+	defer func() { MakeRequestFunc = otel.MakeRequest }() // restore after test
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "tempo-host",
+	}
+	port := "3100"
+
+	validConfigs, logs := validateSimpleTempoHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	assert.Len(t, validConfigs, 2, "Should detect two valid configs")
+	assert.Equal(t, "tempo", validConfigs[0].Provider)
+	assert.Equal(t, "jaeger", validConfigs[1].Provider)
+
+	assert.True(t, validConfigs[0].NamespaceSelector)
+	assert.True(t, validConfigs[1].NamespaceSelector)
+
+	assert.Equal(t, "http://tempo-host:3100", validConfigs[0].Url)
+	assert.Equal(t, "http://tempo-host:3100", validConfigs[1].Url)
+	assert.Greater(t, len(logs), 0)
+}
+
+// Mock for MakeRequest that simulates HTTP responses
+func mockMakeRequestTempo(ctx context.Context, client http.Client, endpoint string, body io.Reader) ([]byte, int, error) {
+	switch endpoint {
+	case "http://gateway-host:3100/api/traces/v1/tenantA/tempo/api/search?q={}":
+		return []byte(`{"traces":[{"rootServiceName":"svc.namespace"}]}`), 200, nil
+	case "http://gateway-host:3100/api/traces/v1/tenantA/api/services":
+		return []byte(`{"data":["svc.namespace"]}`), 200, nil
+	case "http://tempo-host:3100/api/traces/v1/tempo/api/search?q={}":
+		return []byte(`{"traces":[{"rootServiceName":"svc.namespace"}]}`), 200, nil
+	case "http://tempo-host:3100/api/traces/v1/api/traces/v1/api/services":
+		return []byte(`{"data":["svc.namespace"]}`), 200, nil
+	default:
+		return nil, 500, errors.New("not found")
+	}
+}
+
+func TestValidateTempoHTTP_WithGateway(t *testing.T) {
+	MakeRequestFunc = mockMakeRequestTempo
+	defer func() { MakeRequestFunc = otel.MakeRequest }()
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "gateway-host",
+		Path:   "/api/traces/v1/tenantA", // path includes tenant
+	}
+	port := "3100"
+
+	validConfigs, logs := validateTempoHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	assert.Len(t, validConfigs, 2)
+	assert.Equal(t, "tempo", validConfigs[0].Provider)
+	assert.Equal(t, "jaeger", validConfigs[1].Provider)
+	assert.Greater(t, len(logs), 0)
+}
+
+func TestValidateTempoHTTP_WithoutGateway(t *testing.T) {
+	MakeRequestFunc = mockMakeRequestTempo
+	defer func() { MakeRequestFunc = otel.MakeRequest }()
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "tempo-host",
+		Path:   "",
+	}
+	port := "3100"
+
+	validConfigs, logs := validateTempoHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	assert.Len(t, validConfigs, 2)
+	assert.Equal(t, "tempo", validConfigs[0].Provider)
+	assert.Equal(t, "jaeger", validConfigs[1].Provider)
+	assert.Greater(t, len(logs), 0)
+}
+
+func TestValidateTempoHTTP_GatewayMissingTenant(t *testing.T) {
+	MakeRequestFunc = mockMakeRequestTempo
+	defer func() { MakeRequestFunc = otel.MakeRequest }()
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "gateway-host",
+		Path:   "/api/traces/v1", // No tenant segment
+	}
+	port := "3100"
+
+	validConfigs, logs := validateTempoHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	// Should fail due to missing tenant
+	assert.Len(t, validConfigs, 0)
+	assert.True(t, anyLogContains(logs, "tenant name not found"))
+}
+
+// helper to check logs
+func anyLogContains(logs []model.LogLine, substr string) bool {
+	for _, l := range logs {
+		if strings.Contains(l.Result, substr) {
+			return true
+		}
+	}
+	return false
+}
+
+// Mock MakeRequest for Jaeger scenarios
+func mockMakeRequestJaeger(ctx context.Context, client http.Client, endpoint string, body io.Reader) ([]byte, int, error) {
+	switch endpoint {
+	case "http://jaeger-host:16686/jaeger/api/services":
+		return []byte(`{"data":["svc.namespace"]}`), 200, nil
+	case "http://jaeger-host:16686/api/services":
+		return []byte(`{"data":["svc.namespace"]}`), 200, nil
+	default:
+		return nil, 404, errors.New("not found")
+	}
+}
+
+func TestValidateJaegerHTTP_SuccessfulBothEndpoints(t *testing.T) {
+	MakeRequestFunc = mockMakeRequestJaeger
+	defer func() { MakeRequestFunc = otel.MakeRequest }()
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "jaeger-host",
+	}
+	port := "16686"
+
+	validConfigs, logs := validateJaegerHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	assert.Len(t, validConfigs, 2)
+	assert.Equal(t, "jaeger", validConfigs[0].Provider)
+	assert.Equal(t, "jaeger", validConfigs[1].Provider)
+	assert.GreaterOrEqual(t, len(logs), 2)
+	assert.True(t, validConfigs[0].NamespaceSelector)
+	assert.True(t, validConfigs[1].NamespaceSelector)
+}
+
+func TestValidateJaegerHTTP_OnlyOneSuccess(t *testing.T) {
+	MakeRequestFunc = func(ctx context.Context, client http.Client, endpoint string, body io.Reader) ([]byte, int, error) {
+		if endpoint == "http://jaeger-host:16686/jaeger/api/services" {
+			return nil, 500, errors.New("fail")
+		}
+		if endpoint == "http://jaeger-host:16686/api/services" {
+			return []byte(`{"data":["svc.namespace"]}`), 200, nil
+		}
+		return nil, 404, errors.New("not found")
+	}
+	defer func() { MakeRequestFunc = otel.MakeRequest }()
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "jaeger-host",
+	}
+	port := "16686"
+
+	validConfigs, logs := validateJaegerHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	assert.Len(t, validConfigs, 1)
+	assert.Equal(t, "jaeger", validConfigs[0].Provider)
+	assert.GreaterOrEqual(t, len(logs), 1)
+}
+
+func TestValidateJaegerHTTP_NoSuccess(t *testing.T) {
+	MakeRequestFunc = func(ctx context.Context, client http.Client, endpoint string, body io.Reader) ([]byte, int, error) {
+		return nil, 500, errors.New("mock failure")
+	}
+	defer func() { MakeRequestFunc = otel.MakeRequest }()
+
+	client := http.Client{}
+	zl := log.WithGroup(log.TracingLogName)
+	parsedUrl := model.ParsedUrl{
+		Scheme: "http",
+		Host:   "jaeger-host",
+	}
+	port := "16686"
+
+	validConfigs, logs := validateJaegerHTTP(context.TODO(), client, zl, parsedUrl, port)
+
+	assert.Len(t, validConfigs, 0)
+	assert.GreaterOrEqual(t, len(logs), 2)
+}