Repository: kiali/kiali

Issue:
Data race in Mesh Discovery
### Describe the bug

There's a data race in Mesh Discovery. This was actually caught by CI but inconsistently and exit code was so opaque that it wasn't clear what was happening until you looked at the Kiali logs. This is the cause of [these failures](https://github.com/kiali/kiali/actions/runs/17921247590/job/50957215180?pr=8729#step:8:1661). As part of fixing this issue, the data detection race logs need to be improved.

### Expected Behavior

### What are the steps to reproduce this bug?

N/A

### Environment
*Learn about how to determine versions [here](https://kiali.io/docs/faq/general/#how-do-i-determine-what-version-i-am-running).*

- **Kiali version: latest**
- **Istio version: latest**
- **Kubernetes impl: vanilla** 
- **Kubernetes version: 1.32** 
- **Other notable environmental factors:**


Hints from issue comments:
@nrfox: ```
==================
WARNING: DATA RACE
Write at 0x00c000a9c0e0 by goroutine 109:
  github.com/kiali/kiali/istio.(*Discovery).Mesh()
      /home/runner/work/kiali/kiali/istio/discovery.go:810 +0x218f
  github.com/kiali/kiali/istio.(*Discovery).setControlPlaneConfig()
      /home/runner/work/kiali/kiali/istio/discovery.go:111 +0x7fd
  github.com/kiali/kiali/istio.(*Discovery).Mesh()
      /home/runner/work/kiali/kiali/istio/discovery.go:550 +0x54cb
  github.com/kiali/kiali/istio.(*Discovery).GetControlPlaneNamespaces()
      /home/runner/work/kiali/kiali/istio/discovery.go:302 +0x115
  github.com/kiali/kiali/istio.(*Discovery).IsControlPlane()
      /home/runner/work/kiali/kiali/istio/discovery.go:320 +0x5c
  github.com/kiali/kiali/business.(*MeshService).IsControlPlane()
      /home/runner/work/kiali/kiali/business/mesh.go:57 +0x9b
  github.com/kiali/kiali/business.(*MeshService).IsControlPlane-fm()
      <autogenerated>:1 +0x1f
  github.com/kiali/kiali/models.isIstioProxy()
      /home/runner/work/kiali/kiali/models/pod.go:148 +0x82
  github.com/kiali/kiali/models.(*Pod).Parse()
      /home/runner/work/kiali/kiali/models/pod.go:129 +0xfc4
  github.com/kiali/kiali/models.(*Pods).Parse()
      /home/runner/work/kiali/kiali/models/pod.go:70 +0x192
  github.com/kiali/kiali/models.(*Workload).SetPods()
      /home/runner/work/kiali/kiali/models/workload.go:606 +0x85
  github.com/kiali/kiali/business.(*WorkloadService).fetchWorkloadsFromCluster()
      /home/runner/work/kiali/kiali/business/workloads.go:1346 +0x8a2a
  github.com/kiali/kiali/business.(*WorkloadService).getAllWorkloads()
      /home/runner/work/kiali/kiali/business/workloads.go:190 +0x38e
  github.com/kiali/kiali/business.(*WorkloadService).GetAllWorkloads()
      /home/runner/work/kiali/kiali/business/workloads.go:178 +0x37a
  github.com/kiali/kiali/business.(*IstioValidationsService).NewValidationInfo()
      /home/runner/work/kiali/kiali/business/istio_validations.go:217 +0x584
  github.com/kiali/kiali/controller.(*ValidationsReconciler).Reconcile()
      /home/runner/work/kiali/kiali/controller/validations.go:134 +0x2eb
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Reconcile()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:119 +0x1bd
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).reconcileHandler()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:340 +0x52f
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).processNextWorkItem()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300 +0x3b2
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2.1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202 +0xe4

Previous read at 0x00c000a9c0e0 by goroutine 11614:
  github.com/kiali/kiali/istio.(*Discovery).GetRootNamespace()
      /home/runner/work/kiali/kiali/istio/discovery.go:348 +0xa4
  github.com/kiali/kiali/business.(*TLSService).ClusterWideNSmTLSStatus()
      /home/runner/work/kiali/kiali/business/tls.go:181 +0x773
  github.com/kiali/kiali/routing.NewRoutes.ClustersTls.func54()
      /home/runner/work/kiali/kiali/handlers/tls.go:97 +0x4a4
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/kiali/kiali/routing.NewRouter.metricHandler.func6()
      /home/runner/work/kiali/kiali/routing/router.go:314 +0x21e
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/kiali/kiali/routing.NewRouter.(*AuthenticationHandler).Handle.func7()
      /home/runner/work/kiali/kiali/handlers/authentication.go:104 +0x4c1
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/kiali/kiali/routing.buildHttpHandlerLogger.func1.RequestIDHandler.2.1()
      /home/runner/go/pkg/mod/github.com/rs/zerolog@v1.34.0/hlog/hlog.go:233 +0x2af
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/kiali/kiali/routing.buildHttpHandlerLogger.func1.1()
      /home/runner/work/kiali/kiali/routing/router.go:410 +0xc1
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/kiali/kiali/routing.buildHttpHandlerLogger.NewHandler.func2.1()
      /home/runner/go/pkg/mod/github.com/rs/zerolog@v1.34.0/hlog/hlog.go:31 +0x1b6
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/kiali/kiali/server.plainHttpMiddleware.func1()
      /home/runner/work/kiali/kiali/server/server.go:182 +0xb0
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  github.com/gorilla/mux.(*Router).ServeHTTP()
      /home/runner/go/pkg/mod/github.com/gorilla/mux@v1.8.1/mux.go:212 +0x366
  github.com/NYTimes/gziphandler.GzipHandlerWithOpts.func1.1()
      /home/runner/go/pkg/mod/github.com/!n!y!times/gziphandler@v1.1.1/gzip.go:336 +0x474
  net/http.HandlerFunc.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2294 +0x47
  net/http.(*ServeMux).ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2822 +0x23e
  net/http.serverHandler.ServeHTTP()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:3301 +0x2a1
  net/http.(*conn).serve()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:2102 +0x1304
  net/http.(*Server).Serve.gowrap3()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:3454 +0x4f

Goroutine 109 (running) created at:
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start.func2()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:198 +0x494
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[go.shape.struct { k8s.io/apimachinery/pkg/types.NamespacedName }]).Start()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:209 +0x4ef
  sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[sigs.k8s.io/controller-runtime/pkg/reconcile.Request]).Start()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:148 +0x4a
  sigs.k8s.io/controller-runtime/pkg/manager.(*runnableGroup).reconcile.func1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/manager/runnable_group.go:226 +0x1bd
  sigs.k8s.io/controller-runtime/pkg/manager.(*runnableGroup).reconcile.gowrap1()
      /home/runner/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/manager/runnable_group.go:229 +0x41

Goroutine 11614 (running) created at:
  net/http.(*Server).Serve()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:3454 +0x8c9
  net/http.(*Server).ListenAndServe()
      /opt/hostedtoolcache/go/1.24.2/x64/src/net/http/server.go:3350 +0xb3
  github.com/kiali/kiali/server.(*Server).Start.func1()
      /home/runner/work/kiali/kiali/server/server.go:136 +0x372
==================
```

Solution patch:
diff --git a/business/apps.go b/business/apps.go
index 894d3780d..c61c605c3 100644
--- a/business/apps.go
+++ b/business/apps.go
@@ -405,7 +405,7 @@ func (in *AppService) GetAppDetails(ctx context.Context, criteria AppCriteria) (
 		}
 	}
 
-	appInstance.Runtimes = NewDashboardsService(in.conf, in.grafana, in.prom, ns, nil).GetCustomDashboardRefs(criteria.Namespace, criteria.AppName, "", pods)
+	appInstance.Runtimes = NewDashboardsService(in.conf, in.grafana, in.prom, ns, nil).GetCustomDashboardRefs(ctx, criteria.Namespace, criteria.AppName, "", pods)
 	if criteria.IncludeHealth {
 		appInstance.Health, err = in.businessLayer.Health.GetAppHealth(ctx, criteria.Namespace, criteria.Cluster, criteria.AppName, criteria.RateInterval, criteria.QueryTime, appDetails)
 		if err != nil {
diff --git a/business/dashboards.go b/business/dashboards.go
index 7f78c0197..ff2dd1784 100644
--- a/business/dashboards.go
+++ b/business/dashboards.go
@@ -178,13 +178,13 @@ func (in *DashboardsService) GetDashboard(ctx context.Context, params models.Das
 					if chart.Aggregator != "" {
 						aggregator = chart.Aggregator
 					}
-					metric := promClient.FetchRange(ref.MetricName, filters, grouping, aggregator, &params.RangeQuery)
+					metric := promClient.FetchRange(ctx, ref.MetricName, filters, grouping, aggregator, &params.RangeQuery)
 					converted, err = models.ConvertMetric(ref.DisplayName, metric, conversionParams)
 				case dashboards.Rate:
-					metric := promClient.FetchRateRange(ref.MetricName, []string{filters}, grouping, &params.RangeQuery)
+					metric := promClient.FetchRateRange(ctx, ref.MetricName, []string{filters}, grouping, &params.RangeQuery)
 					converted, err = models.ConvertMetric(ref.DisplayName, metric, conversionParams)
 				default:
-					histo := promClient.FetchHistogramRange(ref.MetricName, filters, grouping, &params.RangeQuery)
+					histo := promClient.FetchHistogramRange(ctx, ref.MetricName, filters, grouping, &params.RangeQuery)
 					converted, err = models.ConvertHistogram(ref.DisplayName, histo, conversionParams)
 				}
 
@@ -267,7 +267,7 @@ func (in *DashboardsService) buildRuntimesList(templatesNames []string) []models
 	return runtimes
 }
 
-func (in *DashboardsService) fetchDashboardMetricNames(namespace string, labelsFilters map[string]string) []string {
+func (in *DashboardsService) fetchDashboardMetricNames(ctx context.Context, namespace string, labelsFilters map[string]string) []string {
 	promClient := in.promClient
 
 	// Get the list of metrics that we look for to determine which dashboards can be used.
@@ -280,7 +280,7 @@ func (in *DashboardsService) fetchDashboardMetricNames(namespace string, labelsF
 	}
 
 	labels := in.buildLabelsQueryString(namespace, labelsFilters)
-	metrics, err := promClient.GetMetricsForLabels(discoverOnMetrics, labels)
+	metrics, err := promClient.GetMetricsForLabels(ctx, discoverOnMetrics, labels)
 	if err != nil {
 		log.Errorf("custom dashboard discovery failed, cannot load metrics for labels [%s]: %v", labels, err)
 	}
@@ -288,10 +288,10 @@ func (in *DashboardsService) fetchDashboardMetricNames(namespace string, labelsF
 }
 
 // discoverDashboards tries to discover dashboards based on existing metrics
-func (in *DashboardsService) discoverDashboards(namespace string, labelsFilters map[string]string) []models.Runtime {
+func (in *DashboardsService) discoverDashboards(ctx context.Context, namespace string, labelsFilters map[string]string) []models.Runtime {
 	log.Tracef("starting custom dashboard discovery on namespace [%s] with filters [%v]", namespace, labelsFilters)
 
-	metrics := in.fetchDashboardMetricNames(namespace, labelsFilters)
+	metrics := in.fetchDashboardMetricNames(ctx, namespace, labelsFilters)
 
 	return runDiscoveryMatcher(metrics, in.dashboards)
 }
@@ -603,7 +603,7 @@ func (in *DashboardsService) BuildIstioDashboard(metrics models.MetricsMap, dire
 }
 
 // GetCustomDashboardRefs finds all dashboard IDs and Titles associated to this app and add them to the model
-func (in *DashboardsService) GetCustomDashboardRefs(namespace, app, version string, pods []*models.Pod) []models.Runtime {
+func (in *DashboardsService) GetCustomDashboardRefs(ctx context.Context, namespace, app, version string, pods []*models.Pod) []models.Runtime {
 	if !in.CustomEnabled || app == "" {
 		// Custom dashboards are disabled or the app label is not configured
 		return []models.Runtime{}
@@ -623,7 +623,7 @@ func (in *DashboardsService) GetCustomDashboardRefs(namespace, app, version stri
 			(discoveryEnabled == config.DashboardsDiscoveryAuto &&
 				len(pods) <= in.conf.ExternalServices.CustomDashboards.DiscoveryAutoThreshold) {
 			for _, appVersionLabelSelector := range in.conf.GetAppVersionLabelSelectors(app, version) {
-				for _, discoveredDashboard := range in.discoverDashboards(namespace, appVersionLabelSelector.Requirements) {
+				for _, discoveredDashboard := range in.discoverDashboards(ctx, namespace, appVersionLabelSelector.Requirements) {
 					discoveredRuntimes[discoveredDashboard.Name] = discoveredDashboard
 				}
 			}
diff --git a/business/health.go b/business/health.go
index 42a11326a..5882ff087 100644
--- a/business/health.go
+++ b/business/health.go
@@ -55,7 +55,7 @@ func (in *HealthService) GetServiceHealth(ctx context.Context, namespace, cluste
 	)
 	defer end()
 
-	rqHealth, err := in.getServiceRequestsHealth(namespace, cluster, service, rateInterval, queryTime, svc)
+	rqHealth, err := in.getServiceRequestsHealth(ctx, namespace, cluster, service, rateInterval, queryTime, svc)
 	return models.ServiceHealth{Requests: rqHealth}, err
 }
 
@@ -72,10 +72,10 @@ func (in *HealthService) GetAppHealth(ctx context.Context, namespace, cluster, a
 	)
 	defer end()
 
-	return in.getAppHealth(namespace, cluster, app, rateInterval, queryTime, appD.Workloads)
+	return in.getAppHealth(ctx, namespace, cluster, app, rateInterval, queryTime, appD.Workloads)
 }
 
-func (in *HealthService) getAppHealth(namespace, cluster, app, rateInterval string, queryTime time.Time, ws models.Workloads) (models.AppHealth, error) {
+func (in *HealthService) getAppHealth(ctx context.Context, namespace, cluster, app, rateInterval string, queryTime time.Time, ws models.Workloads) (models.AppHealth, error) {
 	health := models.EmptyAppHealth()
 
 	// Perf: do not bother fetching request rate if there are no workloads or no workload has sidecar
@@ -90,7 +90,7 @@ func (in *HealthService) getAppHealth(namespace, cluster, app, rateInterval stri
 	// Fetch services requests rates
 	var errRate error
 	if hasSidecar {
-		rate, err := in.getAppRequestsHealth(namespace, cluster, app, rateInterval, queryTime)
+		rate, err := in.getAppRequestsHealth(ctx, namespace, cluster, app, rateInterval, queryTime)
 		health.Requests = rate
 		errRate = err
 	}
@@ -122,7 +122,7 @@ func (in *HealthService) GetWorkloadHealth(ctx context.Context, namespace, clust
 	}
 
 	// Add Telemetry info
-	rate, err := in.getWorkloadRequestsHealth(namespace, cluster, workload, rateInterval, queryTime, w)
+	rate, err := in.getWorkloadRequestsHealth(ctx, namespace, cluster, workload, rateInterval, queryTime, w)
 	return models.WorkloadHealth{
 		WorkloadStatus: w.CastWorkloadStatus(),
 		Requests:       rate,
@@ -186,7 +186,7 @@ func (in *HealthService) getNamespaceAppHealth(appEntities namespaceApps, criter
 
 	if sidecarPresent && criteria.IncludeMetrics {
 		// Fetch services requests rates
-		rates, err := in.prom.GetAllRequestRates(namespace, cluster, rateInterval, queryTime)
+		rates, err := in.prom.GetAllRequestRates(context.Background(), namespace, cluster, rateInterval, queryTime)
 		if err != nil {
 			return allHealth, errors.NewServiceUnavailable(err.Error())
 		}
@@ -234,10 +234,10 @@ func (in *HealthService) GetNamespaceServiceHealth(ctx context.Context, criteria
 	if err != nil {
 		return nil, err
 	}
-	return in.getNamespaceServiceHealth(services, criteria), nil
+	return in.getNamespaceServiceHealth(ctx, services, criteria), nil
 }
 
-func (in *HealthService) getNamespaceServiceHealth(services *models.ServiceList, criteria NamespaceHealthCriteria) models.NamespaceServiceHealth {
+func (in *HealthService) getNamespaceServiceHealth(ctx context.Context, services *models.ServiceList, criteria NamespaceHealthCriteria) models.NamespaceServiceHealth {
 	namespace := criteria.Namespace
 	queryTime := criteria.QueryTime
 	rateInterval := criteria.RateInterval
@@ -256,7 +256,7 @@ func (in *HealthService) getNamespaceServiceHealth(services *models.ServiceList,
 
 	if criteria.IncludeMetrics {
 		// Fetch services requests rates
-		rates, _ := in.prom.GetNamespaceServicesRequestRates(namespace, cluster, rateInterval, queryTime)
+		rates, _ := in.prom.GetNamespaceServicesRequestRates(ctx, namespace, cluster, rateInterval, queryTime)
 		// Fill with collected request rates
 		lblDestSvc := model.LabelName("destination_service_name")
 		for _, sample := range rates {
@@ -301,10 +301,10 @@ func (in *HealthService) GetNamespaceWorkloadHealth(ctx context.Context, criteri
 		return nil, err
 	}
 
-	return in.getNamespaceWorkloadHealth(wl, criteria)
+	return in.getNamespaceWorkloadHealth(ctx, wl, criteria)
 }
 
-func (in *HealthService) getNamespaceWorkloadHealth(ws models.Workloads, criteria NamespaceHealthCriteria) (models.NamespaceWorkloadHealth, error) {
+func (in *HealthService) getNamespaceWorkloadHealth(ctx context.Context, ws models.Workloads, criteria NamespaceHealthCriteria) (models.NamespaceWorkloadHealth, error) {
 	// Perf: do not bother fetching request rate if no workloads or no workload has sidecar
 	hasSidecar := false
 	namespace := criteria.Namespace
@@ -326,7 +326,7 @@ func (in *HealthService) getNamespaceWorkloadHealth(ws models.Workloads, criteri
 
 	if hasSidecar && criteria.IncludeMetrics {
 		// Fetch services requests rates
-		rates, err := in.prom.GetAllRequestRates(namespace, cluster, rateInterval, queryTime)
+		rates, err := in.prom.GetAllRequestRates(ctx, namespace, cluster, rateInterval, queryTime)
 		if err != nil {
 			return allHealth, errors.NewServiceUnavailable(err.Error())
 		}
@@ -382,7 +382,7 @@ func fillWorkloadRequestRates(allHealth models.NamespaceWorkloadHealth, rates mo
 	}
 }
 
-func (in *HealthService) getServiceRequestsHealth(namespace, cluster, service, rateInterval string, queryTime time.Time, svc *models.Service) (models.RequestHealth, error) {
+func (in *HealthService) getServiceRequestsHealth(ctx context.Context, namespace, cluster, service, rateInterval string, queryTime time.Time, svc *models.Service) (models.RequestHealth, error) {
 	rqHealth := models.NewEmptyRequestHealth()
 
 	if svc.Type == "External" {
@@ -390,7 +390,7 @@ func (in *HealthService) getServiceRequestsHealth(namespace, cluster, service, r
 		// Telemetry doesn't collect a namespace
 		namespace = "unknown"
 	}
-	inbound, err := in.prom.GetServiceRequestRates(namespace, cluster, service, rateInterval, queryTime)
+	inbound, err := in.prom.GetServiceRequestRates(ctx, namespace, cluster, service, rateInterval, queryTime)
 	if err != nil {
 		return rqHealth, errors.NewServiceUnavailable(err.Error())
 	}
@@ -402,10 +402,10 @@ func (in *HealthService) getServiceRequestsHealth(namespace, cluster, service, r
 	return rqHealth, nil
 }
 
-func (in *HealthService) getAppRequestsHealth(namespace, cluster, app, rateInterval string, queryTime time.Time) (models.RequestHealth, error) {
+func (in *HealthService) getAppRequestsHealth(ctx context.Context, namespace, cluster, app, rateInterval string, queryTime time.Time) (models.RequestHealth, error) {
 	rqHealth := models.NewEmptyRequestHealth()
 
-	inbound, outbound, err := in.prom.GetAppRequestRates(namespace, cluster, app, rateInterval, queryTime)
+	inbound, outbound, err := in.prom.GetAppRequestRates(ctx, namespace, cluster, app, rateInterval, queryTime)
 	if err != nil {
 		return rqHealth, errors.NewServiceUnavailable(err.Error())
 	}
@@ -419,11 +419,11 @@ func (in *HealthService) getAppRequestsHealth(namespace, cluster, app, rateInter
 	return rqHealth, nil
 }
 
-func (in *HealthService) getWorkloadRequestsHealth(namespace, cluster, workload, rateInterval string, queryTime time.Time, w *models.Workload) (models.RequestHealth, error) {
+func (in *HealthService) getWorkloadRequestsHealth(ctx context.Context, namespace, cluster, workload, rateInterval string, queryTime time.Time, w *models.Workload) (models.RequestHealth, error) {
 	rqHealth := models.NewEmptyRequestHealth()
 
 	// @TODO include w.Cluster into query
-	inbound, outbound, err := in.prom.GetWorkloadRequestRates(namespace, cluster, workload, rateInterval, queryTime)
+	inbound, outbound, err := in.prom.GetWorkloadRequestRates(ctx, namespace, cluster, workload, rateInterval, queryTime)
 	if err != nil {
 		return rqHealth, err
 	}
diff --git a/business/metrics.go b/business/metrics.go
index 322cb55e6..76c8f652d 100644
--- a/business/metrics.go
+++ b/business/metrics.go
@@ -1,6 +1,7 @@
 package business
 
 import (
+	"context"
 	"fmt"
 	"math"
 	"sort"
@@ -23,10 +24,10 @@ func NewMetricsService(prom prometheus.ClientInterface, conf *config.Config) *Me
 	return &MetricsService{conf: conf, prom: prom}
 }
 
-func (in *MetricsService) GetMetrics(q models.IstioMetricsQuery, scaler func(n string) float64) (models.MetricsMap, error) {
+func (in *MetricsService) GetMetrics(ctx context.Context, q models.IstioMetricsQuery, scaler func(n string) float64) (models.MetricsMap, error) {
 	lb := createMetricsLabelsBuilder(&q, in.conf)
 	grouping := strings.Join(q.ByLabels, ",")
-	return in.fetchAllMetrics(q, lb, grouping, scaler)
+	return in.fetchAllMetrics(ctx, q, lb, grouping, scaler)
 }
 
 func createMetricsLabelsBuilder(q *models.IstioMetricsQuery, conf *config.Config) *MetricsLabelsBuilder {
@@ -68,20 +69,20 @@ func createMetricsLabelsBuilder(q *models.IstioMetricsQuery, conf *config.Config
 	return lb
 }
 
-func (in *MetricsService) fetchAllMetrics(q models.IstioMetricsQuery, lb *MetricsLabelsBuilder, grouping string, scaler func(n string) float64) (models.MetricsMap, error) {
+func (in *MetricsService) fetchAllMetrics(ctx context.Context, q models.IstioMetricsQuery, lb *MetricsLabelsBuilder, grouping string, scaler func(n string) float64) (models.MetricsMap, error) {
 	labels := lb.Build()
 	labelsError := lb.BuildForErrors()
 
 	var wg sync.WaitGroup
 	fetchRate := func(p8sFamilyName string, metric *prometheus.Metric, lbl []string) {
 		defer wg.Done()
-		m := in.prom.FetchRateRange(p8sFamilyName, lbl, grouping, &q.RangeQuery)
+		m := in.prom.FetchRateRange(ctx, p8sFamilyName, lbl, grouping, &q.RangeQuery)
 		*metric = m
 	}
 
 	fetchHisto := func(p8sFamilyName string, histo *prometheus.Histogram) {
 		defer wg.Done()
-		h := in.prom.FetchHistogramRange(p8sFamilyName, labels, grouping, &q.RangeQuery)
+		h := in.prom.FetchHistogramRange(ctx, p8sFamilyName, labels, grouping, &q.RangeQuery)
 		*histo = h
 	}
 
@@ -152,7 +153,7 @@ func (in *MetricsService) fetchAllMetrics(q models.IstioMetricsQuery, lb *Metric
 }
 
 // GetStats computes metrics stats, currently response times, for a set of queries
-func (in *MetricsService) GetStats(queries []models.MetricsStatsQuery) (map[string]models.MetricsStats, error) {
+func (in *MetricsService) GetStats(ctx context.Context, queries []models.MetricsStatsQuery) (map[string]models.MetricsStats, error) {
 	type statsChanResult struct {
 		key   string
 		stats *models.MetricsStats
@@ -184,7 +185,7 @@ func (in *MetricsService) GetStats(queries []models.MetricsStatsQuery) (map[stri
 			wg.Add(1)
 			go func(q models.MetricsStatsQuery) {
 				defer wg.Done()
-				stats, err := in.getSingleQueryStats(&q)
+				stats, err := in.getSingleQueryStats(ctx, &q)
 				statsChan <- statsChanResult{key: q.GenKey(), stats: stats, err: err}
 			}(q)
 		}
@@ -204,10 +205,10 @@ func (in *MetricsService) GetStats(queries []models.MetricsStatsQuery) (map[stri
 	return result, nil
 }
 
-func (in *MetricsService) getSingleQueryStats(q *models.MetricsStatsQuery) (*models.MetricsStats, error) {
+func (in *MetricsService) getSingleQueryStats(ctx context.Context, q *models.MetricsStatsQuery) (*models.MetricsStats, error) {
 	lb := createStatsMetricsLabelsBuilder(q, in.conf)
 	labels := lb.Build()
-	stats, err := in.prom.FetchHistogramValues("istio_request_duration_milliseconds", labels, "", q.Interval, q.Avg, q.Quantiles, q.QueryTime)
+	stats, err := in.prom.FetchHistogramValues(ctx, "istio_request_duration_milliseconds", labels, "", q.Interval, q.Avg, q.Quantiles, q.QueryTime)
 	if err != nil {
 		return nil, err
 	}
@@ -256,7 +257,7 @@ func createStatsMetricsLabelsBuilder(q *models.MetricsStatsQuery, conf *config.C
 	return lb
 }
 
-func (in *MetricsService) GetControlPlaneMetrics(q models.IstioMetricsQuery, pods models.Pods, scaler func(n string) float64) (models.MetricsMap, error) {
+func (in *MetricsService) GetControlPlaneMetrics(ctx context.Context, q models.IstioMetricsQuery, pods models.Pods, scaler func(n string) float64) (models.MetricsMap, error) {
 	podRegex := ""
 	separator := ""
 	podLabel := ""
@@ -280,12 +281,12 @@ func (in *MetricsService) GetControlPlaneMetrics(q models.IstioMetricsQuery, pod
 	// is delta(sum) / delta(count) for the time period. Note, this is pretty non-standard manipulation of a histogram,
 	// don't try this at home.
 	deltaDuration := q.End.Sub(q.Start)
-	deltaSumMetric := in.prom.FetchDelta("pilot_proxy_convergence_time_sum", podLabel, "", q.End, deltaDuration)
+	deltaSumMetric := in.prom.FetchDelta(ctx, "pilot_proxy_convergence_time_sum", podLabel, "", q.End, deltaDuration)
 	deltaSumConverted, err := models.ConvertMetric("pilot_proxy_convergence_time_sum", deltaSumMetric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
 	}
-	deltaCountMetric := in.prom.FetchDelta("pilot_proxy_convergence_time_count", podLabel, "", q.End, deltaDuration)
+	deltaCountMetric := in.prom.FetchDelta(ctx, "pilot_proxy_convergence_time_count", podLabel, "", q.End, deltaDuration)
 	deltaCountConverted, err := models.ConvertMetric("pilot_proxy_convergence_time_count", deltaCountMetric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
@@ -302,14 +303,14 @@ func (in *MetricsService) GetControlPlaneMetrics(q models.IstioMetricsQuery, pod
 		metrics["pilot_proxy_convergence_time"] = append(metrics["pilot_proxy_convergence_time"], converted...)
 	}
 
-	metric := in.prom.FetchRateRange("container_cpu_usage_seconds_total", []string{podLabel}, "", &q.RangeQuery)
+	metric := in.prom.FetchRateRange(ctx, "container_cpu_usage_seconds_total", []string{podLabel}, "", &q.RangeQuery)
 	converted, err = models.ConvertMetric("container_cpu_usage_seconds_total", metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
 	}
 	metrics["container_cpu_usage_seconds_total"] = append(metrics["container_cpu_usage_seconds_total"], converted...)
 
-	metric = in.prom.FetchRateRange("process_cpu_seconds_total", []string{podLabel}, "", &q.RangeQuery)
+	metric = in.prom.FetchRateRange(ctx, "process_cpu_seconds_total", []string{podLabel}, "", &q.RangeQuery)
 
 	converted, err = models.ConvertMetric("process_cpu_seconds_total", metric, models.ConversionParams{Scale: 1})
 	if err != nil {
@@ -317,7 +318,7 @@ func (in *MetricsService) GetControlPlaneMetrics(q models.IstioMetricsQuery, pod
 	}
 	metrics["process_cpu_seconds_total"] = append(metrics["process_cpu_seconds_total"], converted...)
 
-	metric = in.prom.FetchRange("container_memory_working_set_bytes", podLabel, "", "", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, "container_memory_working_set_bytes", podLabel, "", "", &q.RangeQuery)
 
 	converted, err = models.ConvertMetric("container_memory_working_set_bytes", metric, models.ConversionParams{Scale: 0.000001})
 	if err != nil {
@@ -325,7 +326,7 @@ func (in *MetricsService) GetControlPlaneMetrics(q models.IstioMetricsQuery, pod
 	}
 	metrics["container_memory_working_set_bytes"] = append(metrics["container_memory_working_set_bytes"], converted...)
 
-	metric = in.prom.FetchRange("process_resident_memory_bytes", podLabel, "", "", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, "process_resident_memory_bytes", podLabel, "", "", &q.RangeQuery)
 	converted, err = models.ConvertMetric("process_resident_memory_bytes", metric, models.ConversionParams{Scale: 0.000001})
 	if err != nil {
 		return nil, err
@@ -335,19 +336,19 @@ func (in *MetricsService) GetControlPlaneMetrics(q models.IstioMetricsQuery, pod
 	return metrics, nil
 }
 
-func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.MetricsMap, error) {
+func (in *MetricsService) GetZtunnelMetrics(ctx context.Context, q models.IstioMetricsQuery) (models.MetricsMap, error) {
 	metrics := make(models.MetricsMap)
 	var err error
 	var converted []models.Metric
 
 	// ZTunnel connections
-	metric := in.prom.FetchRateRange("istio_tcp_connections_opened_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
+	metric := in.prom.FetchRateRange(ctx, "istio_tcp_connections_opened_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
 	converted, err = models.ConvertMetric("istio_tcp_connections_opened_total", metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
 	}
 	metrics["ztunnel_connections"] = append(metrics["istio_tcp_connections_closed_total"], converted...)
-	metric = in.prom.FetchRateRange("istio_tcp_connections_closed_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
+	metric = in.prom.FetchRateRange(ctx, "istio_tcp_connections_closed_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
 	converted, err = models.ConvertMetric("istio_tcp_connections_closed_total", metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
@@ -355,7 +356,7 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 	metrics["ztunnel_connections"] = append(metrics["istio_tcp_connections_closed_total"], converted...)
 
 	// Ztunnel versions
-	metric = in.prom.FetchRange("istio_build", "{component=\"ztunnel\"}", "tag", "sum", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, "istio_build", "{component=\"ztunnel\"}", "tag", "sum", &q.RangeQuery)
 	converted, err = models.ConvertMetric("istio_build", metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
@@ -363,7 +364,7 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 	metrics["ztunnel_versions"] = append(metrics["istio_build"], converted...)
 
 	// Ztunnel memory usage ztunnel_memory_usage
-	metric = in.prom.FetchRange("container_memory_working_set_bytes", "{pod=~\"ztunnel-.*\"}", "pod", "sum", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, "container_memory_working_set_bytes", "{pod=~\"ztunnel-.*\"}", "pod", "sum", &q.RangeQuery)
 	converted, err = models.ConvertMetric("container_memory_working_set_bytes", metric, models.ConversionParams{Scale: 0.000001})
 	if err != nil {
 		return nil, err
@@ -372,7 +373,7 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 
 	// Ztunnel ztunnel_cpu_usage
 	metricName := fmt.Sprintf("irate(container_cpu_usage_seconds_total{pod=~\"ztunnel-.*\"}[%s])", q.RateInterval)
-	metric = in.prom.FetchRange(metricName, "", "pod", "sum", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, metricName, "", "pod", "sum", &q.RangeQuery)
 	converted, err = models.ConvertMetric(metricName, metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
@@ -380,13 +381,13 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 	metrics["ztunnel_cpu_usage"] = append(metrics[metricName], converted...)
 
 	// ztunnel_bytes_transmitted
-	metric = in.prom.FetchRateRange("istio_tcp_received_bytes_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
+	metric = in.prom.FetchRateRange(ctx, "istio_tcp_received_bytes_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
 	converted, err = models.ConvertMetric("ztunnel_bytes_transmitted", metric, models.ConversionParams{Scale: 0.001, LabelPrefix: "Received"})
 	if err != nil {
 		return nil, err
 	}
 	metrics["ztunnel_bytes_transmitted"] = append(metrics["ztunnel_bytes_transmitted"], converted...)
-	metric = in.prom.FetchRateRange("istio_tcp_sent_bytes_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
+	metric = in.prom.FetchRateRange(ctx, "istio_tcp_sent_bytes_total", []string{"{pod=~\"ztunnel-.*\"}"}, "pod", &q.RangeQuery)
 	converted, err = models.ConvertMetric("ztunnel_bytes_transmitted", metric, models.ConversionParams{Scale: 0.001, LabelPrefix: "Sent"})
 	if err != nil {
 		return nil, err
@@ -394,7 +395,7 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 	metrics["ztunnel_bytes_transmitted"] = append(metrics["ztunnel_bytes_transmitted"], converted...)
 
 	// ztunnel_workload_manager
-	metric = in.prom.FetchRange("workload_manager_active_proxy_count", "{pod=~\"ztunnel-.*\"}", "pod", "sum", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, "workload_manager_active_proxy_count", "{pod=~\"ztunnel-.*\"}", "pod", "sum", &q.RangeQuery)
 	converted, err = models.ConvertMetric("ztunnel_workload_manager", metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
@@ -404,14 +405,14 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 	return metrics, nil
 }
 
-func (in *MetricsService) GetResourceMetrics(q models.IstioMetricsQuery) (models.MetricsMap, error) {
+func (in *MetricsService) GetResourceMetrics(ctx context.Context, q models.IstioMetricsQuery) (models.MetricsMap, error) {
 	metrics := make(models.MetricsMap)
 	var err error
 	var converted []models.Metric
 
 	// Component memory usage memory_usage
 	labels := fmt.Sprintf("{pod=~\"%s-.*\"}", q.App)
-	metric := in.prom.FetchRange("container_memory_working_set_bytes", labels, "pod", "sum", &q.RangeQuery)
+	metric := in.prom.FetchRange(ctx, "container_memory_working_set_bytes", labels, "pod", "sum", &q.RangeQuery)
 	converted, err = models.ConvertMetric("container_memory_working_set_bytes", metric, models.ConversionParams{Scale: 0.000001})
 	if err != nil {
 		return nil, err
@@ -420,7 +421,7 @@ func (in *MetricsService) GetResourceMetrics(q models.IstioMetricsQuery) (models
 
 	// Component cpu_usage
 	metricName := fmt.Sprintf("irate(container_cpu_usage_seconds_total{pod=~\"%s-.*\"}[%s])", q.App, q.RateInterval)
-	metric = in.prom.FetchRange(metricName, "", "pod", "sum", &q.RangeQuery)
+	metric = in.prom.FetchRange(ctx, metricName, "", "pod", "sum", &q.RangeQuery)
 	converted, err = models.ConvertMetric(metricName, metric, models.ConversionParams{Scale: 1})
 	if err != nil {
 		return nil, err
diff --git a/business/workloads.go b/business/workloads.go
index ddadca8e4..a4feaf7d2 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -546,7 +546,7 @@ func (in *WorkloadService) GetWorkload(ctx context.Context, criteria WorkloadCri
 		verLabelName, _ := conf.GetVersionLabelName(workload.Labels)
 		app := workload.Labels[appLabelName]
 		version := workload.Labels[verLabelName]
-		runtimes = NewDashboardsService(in.conf, in.grafana, in.prom, ns, workload).GetCustomDashboardRefs(criteria.Namespace, app, version, workload.Pods)
+		runtimes = NewDashboardsService(in.conf, in.grafana, in.prom, ns, workload).GetCustomDashboardRefs(ctx, criteria.Namespace, app, version, workload.Pods)
 	}()
 
 	// WorkloadGroup.Labels can be empty
diff --git a/config/config.go b/config/config.go
index b43e5d55c..6eb5ba458 100644
--- a/config/config.go
+++ b/config/config.go
@@ -672,13 +672,14 @@ type KialiURL struct {
 
 // KialiFeatureFlags available from the CR
 type KialiFeatureFlags struct {
-	Clustering            FeatureFlagClustering `yaml:"clustering,omitempty" json:"clustering,omitempty"`
-	DisabledFeatures      []string              `yaml:"disabled_features,omitempty" json:"disabledFeatures,omitempty"`
-	IstioAnnotationAction bool                  `yaml:"istio_annotation_action,omitempty" json:"istioAnnotationAction"`
-	IstioInjectionAction  bool                  `yaml:"istio_injection_action,omitempty" json:"istioInjectionAction"`
-	IstioUpgradeAction    bool                  `yaml:"istio_upgrade_action,omitempty" json:"istioUpgradeAction"`
-	UIDefaults            UIDefaults            `yaml:"ui_defaults,omitempty" json:"uiDefaults,omitempty"`
-	Validations           Validations           `yaml:"validations,omitempty" json:"validations,omitempty"`
+	Clustering            FeatureFlagClustering     `yaml:"clustering,omitempty" json:"clustering,omitempty"`
+	CustomWorkloadTypes   []metav1.GroupVersionKind `yaml:"custom_workload_types,omitempty" json:"customWorkloadTypes,omitempty"`
+	DisabledFeatures      []string                  `yaml:"disabled_features,omitempty" json:"disabledFeatures,omitempty"`
+	IstioAnnotationAction bool                      `yaml:"istio_annotation_action,omitempty" json:"istioAnnotationAction"`
+	IstioInjectionAction  bool                      `yaml:"istio_injection_action,omitempty" json:"istioInjectionAction"`
+	IstioUpgradeAction    bool                      `yaml:"istio_upgrade_action,omitempty" json:"istioUpgradeAction"`
+	UIDefaults            UIDefaults                `yaml:"ui_defaults,omitempty" json:"uiDefaults,omitempty"`
+	Validations           Validations               `yaml:"validations,omitempty" json:"validations,omitempty"`
 }
 
 // Tolerance config
diff --git a/frontend/cypress/integration/common/transition.ts b/frontend/cypress/integration/common/transition.ts
index cd62bb45f..656147190 100644
--- a/frontend/cypress/integration/common/transition.ts
+++ b/frontend/cypress/integration/common/transition.ts
@@ -6,5 +6,5 @@ export const ensureKialiFinishedLoading = (): void => {
 };
 
 export const openTab = (tab: string): void => {
-  cy.get('#basic-tabs', { timeout: 60000 }).should('be.visible').contains(tab).click(); // Can be very slow for OpenShift, specially in the UI
+  cy.get('#basic-tabs', { timeout: 60000 }).should('exist').contains(tab).click(); // Can be very slow for OpenShift, specially in the UI
 };
diff --git a/frontend/cypress/integration/common/workloads_details.ts b/frontend/cypress/integration/common/workloads_details.ts
index c19d0ed6e..e68635b04 100644
--- a/frontend/cypress/integration/common/workloads_details.ts
+++ b/frontend/cypress/integration/common/workloads_details.ts
@@ -3,11 +3,11 @@ import { getCellsForCol } from './table';
 import { clusterParameterExists } from './navigation';
 
 const openTab = (tab: string): void => {
-  cy.get('#basic-tabs').should('be.visible').contains(tab).click();
+  cy.get('#basic-tabs').should('exist').contains(tab).click();
 };
 
 const openEnvoyTab = (tab: string): void => {
-  cy.get('#envoy-details').should('be.visible').contains(tab).click();
+  cy.get('#envoy-details').should('exist').contains(tab).click();
 };
 
 Then('user sees details information for workload', () => {
diff --git a/frontend/src/config/ServerConfig.ts b/frontend/src/config/ServerConfig.ts
index b5b74a366..6b71c5d3c 100644
--- a/frontend/src/config/ServerConfig.ts
+++ b/frontend/src/config/ServerConfig.ts
@@ -92,6 +92,7 @@ const defaultServerConfig: ComputedServerConfig = {
     versionLabelName: ''
   },
   kialiFeatureFlags: {
+    customWorkloadTypes: [],
     disabledFeatures: [],
     istioInjectionAction: true,
     istioAnnotationAction: true,
diff --git a/frontend/src/pages/WorkloadList/FiltersAndSorts.ts b/frontend/src/pages/WorkloadList/FiltersAndSorts.ts
index fb6985a8d..a7a40c5ab 100644
--- a/frontend/src/pages/WorkloadList/FiltersAndSorts.ts
+++ b/frontend/src/pages/WorkloadList/FiltersAndSorts.ts
@@ -235,48 +235,59 @@ const workloadTypeFilter: FilterType = {
   placeholder: 'Filter by Workload Type',
   filterType: AllFilterTypes.typeAhead,
   action: FILTER_ACTION_APPEND,
-  filterValues: [
-    {
-      id: dicTypeToGVK.CronJob.Kind,
-      title: dicTypeToGVK.CronJob.Kind
-    },
-    {
-      id: dicTypeToGVK.DaemonSet.Kind,
-      title: dicTypeToGVK.DaemonSet.Kind
-    },
-    {
-      id: dicTypeToGVK.Deployment.Kind,
-      title: dicTypeToGVK.Deployment.Kind
-    },
-    {
-      id: dicTypeToGVK.DeploymentConfig.Kind,
-      title: dicTypeToGVK.DeploymentConfig.Kind
-    },
-    {
-      id: dicTypeToGVK.Job.Kind,
-      title: dicTypeToGVK.Job.Kind
-    },
-    {
-      id: dicTypeToGVK.Pod.Kind,
-      title: dicTypeToGVK.Pod.Kind
-    },
-    {
-      id: dicTypeToGVK.ReplicaSet.Kind,
-      title: dicTypeToGVK.ReplicaSet.Kind
-    },
-    {
-      id: dicTypeToGVK.ReplicationController.Kind,
-      title: dicTypeToGVK.ReplicationController.Kind
-    },
-    {
-      id: dicTypeToGVK.StatefulSet.Kind,
-      title: dicTypeToGVK.StatefulSet.Kind
-    },
-    {
-      id: dicTypeToGVK.WorkloadGroup.Kind,
-      title: dicTypeToGVK.WorkloadGroup.Kind
-    }
-  ]
+  get filterValues() {
+    return [
+      {
+        id: dicTypeToGVK.CronJob.Kind,
+        title: dicTypeToGVK.CronJob.Kind
+      },
+      {
+        id: dicTypeToGVK.DaemonSet.Kind,
+        title: dicTypeToGVK.DaemonSet.Kind
+      },
+      {
+        id: dicTypeToGVK.Deployment.Kind,
+        title: dicTypeToGVK.Deployment.Kind
+      },
+      {
+        id: dicTypeToGVK.DeploymentConfig.Kind,
+        title: dicTypeToGVK.DeploymentConfig.Kind
+      },
+      {
+        id: dicTypeToGVK.Job.Kind,
+        title: dicTypeToGVK.Job.Kind
+      },
+      {
+        id: dicTypeToGVK.Pod.Kind,
+        title: dicTypeToGVK.Pod.Kind
+      },
+      {
+        id: dicTypeToGVK.ReplicaSet.Kind,
+        title: dicTypeToGVK.ReplicaSet.Kind
+      },
+      {
+        id: dicTypeToGVK.ReplicationController.Kind,
+        title: dicTypeToGVK.ReplicationController.Kind
+      },
+      {
+        id: dicTypeToGVK.StatefulSet.Kind,
+        title: dicTypeToGVK.StatefulSet.Kind
+      },
+      {
+        id: dicTypeToGVK.WorkloadGroup.Kind,
+        title: dicTypeToGVK.WorkloadGroup.Kind
+      }
+    ]
+      .concat(
+        (serverConfig.kialiFeatureFlags.customWorkloadTypes || [])
+          .filter(gvk => gvk.kind && gvk.kind.trim() !== '')
+          .map(gvk => ({
+            id: gvk.kind,
+            title: gvk.kind
+          }))
+      )
+      .sort((a, b) => a.title.localeCompare(b.title));
+  }
 };
 
 export const availableFilters: FilterType[] = [
diff --git a/frontend/src/types/ServerConfig.ts b/frontend/src/types/ServerConfig.ts
index 44433b58c..603dc5182 100644
--- a/frontend/src/types/ServerConfig.ts
+++ b/frontend/src/types/ServerConfig.ts
@@ -79,7 +79,14 @@ interface UIDefaults {
   tracing: TracingDefaults;
 }
 
+interface CustomWorkloadType {
+  group: string;
+  version: string;
+  kind: string;
+}
+
 interface KialiFeatureFlags {
+  customWorkloadTypes?: CustomWorkloadType[];
   disabledFeatures: string[];
   istioAnnotationAction: boolean;
   istioInjectionAction: boolean;
diff --git a/frontend/src/utils/IstioConfigUtils.ts b/frontend/src/utils/IstioConfigUtils.ts
index 1e23ce042..274922223 100644
--- a/frontend/src/utils/IstioConfigUtils.ts
+++ b/frontend/src/utils/IstioConfigUtils.ts
@@ -1,3 +1,4 @@
+import { serverConfig } from '../config/ServerConfig';
 import { IstioConfigDetails } from '../types/IstioConfigDetails';
 import {
   ConnectionPoolSettings,
@@ -251,6 +252,26 @@ export function istioTypesToGVKString(istioTypes: string[]): string[] {
   });
 }
 
-export function isGVKSupported(gvk: GroupVersionKind): boolean {
-  return getGVKTypeString(gvk) === getGVKTypeString(gvkType[gvk.Kind]) && gvk.Kind !== gvkType.WorkloadGroup;
+export function isGVKSupported(gvk?: GroupVersionKind): boolean {
+  if (!gvk || !gvk.Kind) {
+    return false;
+  }
+
+
+  // WorkloadGroup is not supported
+  if (gvk.Kind === gvkType.WorkloadGroup) {
+    return false;
+  }
+
+  // Check built-in workload types
+  const builtIn = dicTypeToGVK[gvk.Kind as gvkType];
+  if (builtIn) {
+    return getGVKTypeString(gvk) === getGVKTypeString(builtIn);
+  }
+
+  // Check custom (user-configured) workload types.
+  const custom = serverConfig.kialiFeatureFlags?.customWorkloadTypes ?? [];
+  return custom.some(
+    c => c.group === gvk.Group && c.version === gvk.Version && c.kind === gvk.Kind
+  );
 }
diff --git a/frontend/src/utils/__tests__/IstioConfigUtils.test.ts b/frontend/src/utils/__tests__/IstioConfigUtils.test.ts
index 45625145a..0ea0576e5 100644
--- a/frontend/src/utils/__tests__/IstioConfigUtils.test.ts
+++ b/frontend/src/utils/__tests__/IstioConfigUtils.test.ts
@@ -1,6 +1,7 @@
 import {
   getGVKTypeString,
   getIstioObjectGVK,
+  isGVKSupported,
   isServerHostValid,
   istioTypesToGVKString,
   isValidUrl,
@@ -246,3 +247,69 @@ describe('Validate Istio Types array To GVK Strings array', () => {
     expect(result).toEqual(['']);
   });
 });
+
+// Mock serverConfig for isGVKSupported tests.
+jest.mock('../../config/ServerConfig', () => ({
+  serverConfig: {
+    kialiFeatureFlags: {
+      customWorkloadTypes: []
+    }
+  }
+}));
+
+describe('Validate isGVKSupported', () => {
+  const { serverConfig } = require('../../config/ServerConfig');
+
+  beforeEach(() => {
+    // Reset custom workload types before each test.
+    serverConfig.kialiFeatureFlags.customWorkloadTypes = [];
+  });
+
+  it('should support built-in Deployment', () => {
+    const gvk = { Group: 'apps', Version: 'v1', Kind: 'Deployment' };
+    expect(isGVKSupported(gvk)).toBeTruthy();
+  });
+
+  it('should support built-in StatefulSet', () => {
+    const gvk = { Group: 'apps', Version: 'v1', Kind: 'StatefulSet' };
+    expect(isGVKSupported(gvk)).toBeTruthy();
+  });
+
+  it('should not support unknown GVK by default', () => {
+    const gvk = { Group: 'argoproj.io', Version: 'v1alpha1', Kind: 'Rollout' };
+    expect(isGVKSupported(gvk)).toBeFalsy();
+  });
+
+  it('should support custom workload types when configured', () => {
+    serverConfig.kialiFeatureFlags.customWorkloadTypes = [
+      { group: 'argoproj.io', version: 'v1alpha1', kind: 'Rollout' }
+    ];
+
+    const gvk = { Group: 'argoproj.io', Version: 'v1alpha1', Kind: 'Rollout' };
+    expect(isGVKSupported(gvk)).toBeTruthy();
+  });
+
+  it('should support custom workload types with exact matching', () => {
+    serverConfig.kialiFeatureFlags.customWorkloadTypes = [
+      { group: 'argoproj.io', version: 'v1alpha1', kind: 'Rollout' }
+    ];
+
+    // Exact match should work.
+    expect(isGVKSupported({ Group: 'argoproj.io', Version: 'v1alpha1', Kind: 'Rollout' })).toBeTruthy();
+
+    // Built-in should still work.
+    expect(isGVKSupported({ Group: 'apps', Version: 'v1', Kind: 'Deployment' })).toBeTruthy();
+
+    // Wrong GVK components should not match.
+    expect(isGVKSupported({ Group: 'wrong.io', Version: 'v1alpha1', Kind: 'Rollout' })).toBeFalsy();
+    expect(isGVKSupported({ Group: 'argoproj.io', Version: 'v1beta1', Kind: 'Rollout' })).toBeFalsy();
+  });
+
+  it('should handle empty or undefined custom workload types', () => {
+    serverConfig.kialiFeatureFlags.customWorkloadTypes = [];
+    expect(isGVKSupported({ Group: 'argoproj.io', Version: 'v1alpha1', Kind: 'Rollout' })).toBeFalsy();
+
+    serverConfig.kialiFeatureFlags.customWorkloadTypes = undefined;
+    expect(isGVKSupported({ Group: 'argoproj.io', Version: 'v1alpha1', Kind: 'Rollout' })).toBeFalsy();
+  });
+});
diff --git a/go.mod b/go.mod
index a68938f19..b8d87a071 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/kiali/kiali
 
-go 1.24.2
+go 1.24.4
 
 require (
 	github.com/Masterminds/semver/v3 v3.4.0
diff --git a/grafana/grafana.go b/grafana/grafana.go
index 6e9d3267a..cd2e69f1f 100644
--- a/grafana/grafana.go
+++ b/grafana/grafana.go
@@ -69,7 +69,7 @@ func (s *Service) discover(ctx context.Context) string {
 	// Try to get service and namespace from in-cluster URL, to discover route
 	routeURL := ""
 	var err error
-	if internalURL := s.conf.ExternalServices.Perses.InternalURL; internalURL != "" {
+	if internalURL := s.conf.ExternalServices.Grafana.InternalURL; internalURL != "" {
 		routeURL, err = ParseUrl(ctx, internalURL, s.homeClusterSAClient)
 		if err == nil {
 			s.routeURL = &routeURL
diff --git a/graph/util.go b/graph/util.go
index 4768e61c3..376f6f9cd 100644
--- a/graph/util.go
+++ b/graph/util.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
+	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/prometheus/internalmetrics"
 )
@@ -97,6 +98,17 @@ func PromQueryAppender(ctx context.Context, query string, queryTime time.Time, a
 		return model.Vector{}
 	}
 
+	// Add tracing span for Prometheus query
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(
+		ctx,
+		"PromQueryAppender",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("query", query),
+		observability.Attribute("appenderName", appenderName),
+	)
+	defer end()
+
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
diff --git a/hack/ci-minikube-molecule-tests.sh b/hack/ci-minikube-molecule-tests.sh
index fa39f8228..91d18dddf 100755
--- a/hack/ci-minikube-molecule-tests.sh
+++ b/hack/ci-minikube-molecule-tests.sh
@@ -169,7 +169,7 @@ fi
 
 if ! ${minikube_sh} status; then
 
-  ${minikube_sh} start --dex-enabled true ${olm_enabled_arg}
+  ${minikube_sh} start --hydra-enabled true ${olm_enabled_arg}
   if ! ${minikube_sh} status; then
     echo "Failed to install the minikube cluster."
     exit 1
diff --git a/hack/istio/install-istio-via-sail.sh b/hack/istio/install-istio-via-sail.sh
index 91030b6da..85bfe80de 100755
--- a/hack/istio/install-istio-via-sail.sh
+++ b/hack/istio/install-istio-via-sail.sh
@@ -116,12 +116,54 @@ helm upgrade sail-operator sail-operator \
 K8S_GATEWAY_API_VERSION=$(curl --head --silent "https://github.com/kubernetes-sigs/gateway-api/releases/latest" | grep "location: " | awk '{print $2}' | sed "s/.*tag\///g" | cat -v | sed "s/\^M//g")
 echo "Verifying that Gateway API is installed; if it is not then Gateway API version ${K8S_GATEWAY_API_VERSION} will be installed now."
 kubectl get crd gateways.gateway.networking.k8s.io &> /dev/null || \
-  { kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=${K8S_GATEWAY_API_VERSION}" | kubectl apply -f -; }
+  {
+    echo "Installing Gateway API CRDs with retry logic..."
+    RETRY_COUNT=0
+    MAX_RETRIES=30
+    RETRY_INTERVAL=60
+    while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
+      echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES: Installing Gateway API CRDs..."
+      if kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=${K8S_GATEWAY_API_VERSION}" | kubectl apply -f -; then
+        echo "Gateway API CRDs installed successfully."
+        break
+      else
+        RETRY_COUNT=$((RETRY_COUNT + 1))
+        if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
+          echo "Failed to install Gateway API CRDs. Retrying in ${RETRY_INTERVAL} seconds..."
+          sleep $RETRY_INTERVAL
+        else
+          echo "Failed to install Gateway API CRDs after $MAX_RETRIES attempts. Exiting."
+          exit 1
+        fi
+      fi
+    done
+  }
 
 K8S_GATEWAY_API_IE_VERSION=$(curl --head --silent "https://github.com/kubernetes-sigs/gateway-api-inference-extension/releases/latest" | grep "location: " | awk '{print $2}' | sed "s/.*tag\///g" | cat -v | sed "s/\^M//g")
 echo "Verifying that Gateway API Inference Extension is installed; if it is not then Gateway API Inference Extension version ${K8S_GATEWAY_API_IE_VERSION} will be installed now."
 kubectl get crd inferencepools.inference.networking.x-k8s.io &> /dev/null || \
-  { kubectl kustomize "github.com/kubernetes-sigs/gateway-api-inference-extension/config/crd?ref=${K8S_GATEWAY_API_IE_VERSION}" | kubectl apply -f -; }
+  {
+    echo "Installing Gateway API Inference Extension CRDs with retry logic..."
+    RETRY_COUNT=0
+    MAX_RETRIES=30
+    RETRY_INTERVAL=60
+    while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
+      echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES: Installing Gateway API Inference Extension CRDs..."
+      if kubectl kustomize "github.com/kubernetes-sigs/gateway-api-inference-extension/config/crd?ref=${K8S_GATEWAY_API_IE_VERSION}" | kubectl apply -f -; then
+        echo "Gateway API Inference Extension CRDs installed successfully."
+        break
+      else
+        RETRY_COUNT=$((RETRY_COUNT + 1))
+        if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
+          echo "Failed to install Gateway API Inference Extension CRDs. Retrying in ${RETRY_INTERVAL} seconds..."
+          sleep $RETRY_INTERVAL
+        else
+          echo "Failed to install Gateway API Inference Extension CRDs after $MAX_RETRIES attempts. Exiting."
+          exit 1
+        fi
+      fi
+    done
+  }
 
 SERVICE="jaeger-collector.istio-system.svc.cluster.local"
 if is_in_array "tempo" "tempo" "${ADDONS}"; then
diff --git a/hack/k8s-minikube.sh b/hack/k8s-minikube.sh
index 61da45f45..c82a175b3 100755
--- a/hack/k8s-minikube.sh
+++ b/hack/k8s-minikube.sh
@@ -22,10 +22,9 @@
 set -u
 
 DEFAULT_CLIENT_EXE="kubectl"
-DEFAULT_DEX_ENABLED="false"
-DEFAULT_DEX_REPO="https://github.com/dexidp/dex"
-DEFAULT_DEX_VERSION="v2.30.2"
-DEFAULT_DEX_USER_NAMESPACES="bookinfo"
+DEFAULT_HYDRA_ENABLED="false"
+DEFAULT_HYDRA_USER_NAMESPACES="bookinfo"
+DEFAULT_HYDRA_VERSION="v2.2.0"
 DEFAULT_INSECURE_REGISTRY_IP=""
 DEFAULT_K8S_CNI="auto"
 DEFAULT_K8S_CPU="4"
@@ -102,24 +101,9 @@ check_insecure_registry() {
   fi
 }
 
-install_dex() {
-  echo 'Installing Dex for OpenID Connect support...'
 
-  # Download dex - prepare a clean copy
-  DEX_VERSION_PATH="${OUTPUT_PATH}/dex/${DEX_VERSION}"
-  rm -rf ${DEX_VERSION_PATH}
-  if [ ! -d "${DEX_VERSION_PATH}" ]; then
-    echo "Will download Dex version [${DEX_VERSION}] to [${DEX_VERSION_PATH}]"
-    mkdir -p ${DEX_VERSION_PATH}
-
-    if command -v wget >/dev/null 2>&1; then
-      wget ${DEX_REPO}/archive/${DEX_VERSION}.tar.gz -O - | tar -C ${DEX_VERSION_PATH} --strip-components 1 -zxf -
-    else
-      curl -L -o - | tar -C ${DEX_VERSION_PATH} --strip-components 1 -zxf -
-    fi
-  else
-    echo "Will use existing Dex version [${DEX_VERSION}] found at [${DEX_VERSION_PATH}]"
-  fi
+install_hydra() {
+  echo 'Installing Ory Hydra for OpenID Connect support...'
 
   # Find minikube ip
   MINIKUBE_IP=$(${MINIKUBE_EXEC_WITH_PROFILE} ip)
@@ -129,88 +113,107 @@ install_dex() {
   KUBE_HOSTNAME="${MINIKUBE_IP_DASHED}.nip.io"
   echo "Hostname will be ${KUBE_HOSTNAME}"
 
-  # Generate certs for the minikube instance, if we still don't have them
-  CERTS_PATH="${DEX_VERSION_PATH}/examples/k8s/ssl_${KUBE_HOSTNAME}"
-  if [ ! -d "${CERTS_PATH}" ]; then
-    # Patch gencert.sh script from dex
-    rm -f ${DEX_VERSION_PATH}/examples/k8s/kiali.gencert.sh
-    rm -rf ${DEX_VERSION_PATH}/examples/k8s/ssl
-    patch -i - -o ${DEX_VERSION_PATH}/examples/k8s/kiali.gencert.sh ${DEX_VERSION_PATH}/examples/k8s/gencert.sh <<EOF
-18c18
-< DNS.1 = dex.example.com
----
-> DNS.1 = ${KUBE_HOSTNAME}
-EOF
-    [ "$?" != "0" ] && echo "ERROR: Failed to patch gencert.sh" && exit 1
+  # Create output directory for Hydra
+  HYDRA_PATH="${OUTPUT_PATH}/hydra"
+  mkdir -p ${HYDRA_PATH}
 
-    $(cd ${DEX_VERSION_PATH}/examples/k8s/; bash ./kiali.gencert.sh)
-    mv ${DEX_VERSION_PATH}/examples/k8s/ssl ${CERTS_PATH}
+  # Generate certificates using our existing gencert.sh script
+  CERTS_PATH="${HYDRA_PATH}/ssl_${KUBE_HOSTNAME}"
+  if [ ! -d "${CERTS_PATH}" ]; then
+    echo "Generating TLS certificates for Hydra..."
+    # Use the gencert.sh script from our Hydra implementation (use absolute path)
+    HYDRA_GENCERT_SCRIPT="$(pwd)/ory-hydra/scripts/gencert.sh"
+    if [ ! -f "${HYDRA_GENCERT_SCRIPT}" ]; then
+      echo "ERROR: Hydra gencert.sh script not found at ${HYDRA_GENCERT_SCRIPT}"
+      exit 1
+    fi
 
+    # Run certificate generation
+    mkdir -p ${CERTS_PATH}
+    MINIKUBE_IP=${MINIKUBE_IP} KUBE_HOSTNAME=${KUBE_HOSTNAME} bash ${HYDRA_GENCERT_SCRIPT} "${KUBE_HOSTNAME}" "${MINIKUBE_IP}" "${CERTS_PATH}/ssl"
+    [ "$?" != "0" ] && echo "ERROR: Failed to generate certificates for Hydra" && exit 1
   fi
 
   # Copy certificates to minikube cluster
-  # Because the user may destroy and create many minikube VMs, expect the VM fingerprint to change (i.e. avoid known_hosts checks)
   mkdir -p ${OUTPUT_PATH}
   local tmp_known_hosts="${OUTPUT_PATH}/minikube-known-hosts"
   rm -f ${tmp_known_hosts}
-  ${MINIKUBE_EXEC_WITH_PROFILE} ssh -- mkdir dex_certs
-  scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=${tmp_known_hosts} -i $(${MINIKUBE_EXEC_WITH_PROFILE} ssh-key) ${CERTS_PATH}/* docker@$(${MINIKUBE_EXEC_WITH_PROFILE} ip):dex_certs/
+  ${MINIKUBE_EXEC_WITH_PROFILE} ssh -- mkdir -p hydra_certs
+  scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=${tmp_known_hosts} -i $(${MINIKUBE_EXEC_WITH_PROFILE} ssh-key) ${CERTS_PATH}/ssl/* docker@$(${MINIKUBE_EXEC_WITH_PROFILE} ip):hydra_certs/
   ${MINIKUBE_EXEC_WITH_PROFILE} ssh -- sudo mkdir -p /var/lib/minikube/certs/
-  ${MINIKUBE_EXEC_WITH_PROFILE} ssh -- sudo cp /home/docker/dex_certs/* /var/lib/minikube/certs/
+  ${MINIKUBE_EXEC_WITH_PROFILE} ssh -- sudo cp /home/docker/hydra_certs/* /var/lib/minikube/certs/
 
-  # Patch dex file
-  rm -rf ${DEX_VERSION_PATH}/examples/k8s/dex.kiali.yaml
-  patch -i - -o ${DEX_VERSION_PATH}/examples/k8s/dex.kiali.yaml ${DEX_VERSION_PATH}/examples/k8s/dex.yaml << EOF
-15c15
-<   replicas: 3
----
->   replicas: 1
-26c26
-<       - image: dexidp/dex:v2.27.0 #or quay.io/dexidp/dex:v2.26.0
----
->       - image: ghcr.io/dexidp/dex:${DEX_VERSION}
-41c41
-<         - name: GITHUB_CLIENT_ID
----
->         - name: KUBERNETES_POD_NAMESPACE
-43,50c43,44
-<             secretKeyRef:
-<               name: github-client
-<               key: client-id
-<         - name: GITHUB_CLIENT_SECRET
-<           valueFrom:
-<             secretKeyRef:
-<               name: github-client
-<               key: client-secret
----
->             fieldRef:
->               fieldPath: metadata.namespace
-75c69
-<     issuer: https://dex.example.com:32000
----
->     issuer: https://${KUBE_HOSTNAME}:32000
-85,92d78
-<     - type: github
-<       id: github
-<       name: GitHub
-<       config:
-<         clientID: \$GITHUB_CLIENT_ID
-<         clientSecret: \$GITHUB_CLIENT_SECRET
-<         redirectURI: https://dex.example.com:32000/callback
-<         org: kubernetes
-94a81
->       responseTypes: ["code", "id_token"]
-96a84,89
->     - id: kiali-app
->       redirectURIs:
->       - 'http://${MINIKUBE_IP}/kiali'
->       - 'http://kiali-proxy.${MINIKUBE_IP}.nip.io:30805/oauth2/callback'
->       name: 'Kiali'
->       secret: dontTellAnyone
-EOF
-    [ "$?" != "0" ] && echo "ERROR: Failed to patch dex file" && exit 1
+  # Deploy Hydra using our existing install script
+  echo "Deploying Ory Hydra..."
+  HYDRA_INSTALL_SCRIPT="./ory-hydra/scripts/install-hydra.sh"
+  if [ ! -f "${HYDRA_INSTALL_SCRIPT}" ]; then
+    echo "ERROR: Hydra install script not found at ${HYDRA_INSTALL_SCRIPT}"
+    exit 1
+  fi
+
+  # Run Hydra installation with minikube context
+  MINIKUBE_IP=${MINIKUBE_IP} KUBE_HOSTNAME=${KUBE_HOSTNAME} KUBECTL_CMD="${MINIKUBE_EXEC_WITH_PROFILE} kubectl --" HYDRA_VERSION=${HYDRA_VERSION} MINIKUBE_PROFILE=${MINIKUBE_PROFILE} bash ${HYDRA_INSTALL_SCRIPT}
+  [ "$?" != "0" ] && echo "ERROR: Failed to install Hydra" && exit 1
+
+  # Restart minikube with OIDC configuration
+  echo "Restarting minikube with proper flags for API server and the autodetected registry IP..."
+  ${MINIKUBE_EXEC_WITH_PROFILE} stop
+  ${MINIKUBE_EXEC_WITH_PROFILE} start \
+    ${MINIKUBE_START_FLAGS} \
+    ${INSECURE_REGISTRY_START_ARG} \
+    --insecure-registry ${MINIKUBE_IP}:5000 \
+    --cni=${K8S_CNI} \
+    --cpus=${K8S_CPU} \
+    --memory=${K8S_MEMORY} \
+    --disk-size=${K8S_DISK} \
+    --driver=${K8S_DRIVER} \
+    --kubernetes-version=${K8S_VERSION} \
+    --extra-config=apiserver.oidc-issuer-url=https://$(echo ${MINIKUBE_IP} | sed 's/\./-/g').nip.io:30967 \
+    --extra-config=apiserver.oidc-username-claim=email \
+    --extra-config=apiserver.oidc-ca-file=/var/lib/minikube/certs/hydra-ca.pem \
+    --extra-config=apiserver.oidc-client-id=kiali-app \
+    --extra-config=apiserver.oidc-groups-claim=groups
+  [ "$?" != "0" ] && echo "ERROR: Failed to restart minikube in preparation for Hydra" && exit 1
 
-    cat <<EOF > ${DEX_VERSION_PATH}/examples/k8s/oauth2.proxy
+  # Wait for Hydra to be ready again after minikube restart
+  echo "Waiting for Hydra to be ready after minikube restart..."
+  for i in {1..60}; do
+    if ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get pods -n ory -l app.kubernetes.io/name=hydra --no-headers 2>/dev/null | grep -q "1/1.*Running"; then
+      break
+    fi
+    echo "Waiting for Hydra to restart... (attempt $i/60)"
+    sleep 5
+  done
+  if ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get pods -n ory -l app.kubernetes.io/name=hydra --no-headers 2>/dev/null | grep -q "1/1.*Running"; then
+    echo "Hydra is ready after restart!"
+  else
+    echo "Error: Hydra failed to become ready after restart!"
+    exit 1
+  fi
+
+  # Verify Hydra is accessible via nip.io after restart
+  echo "Verifying Hydra OIDC endpoint is accessible..."
+  for i in {1..40}; do
+    if curl -k -s "https://$(echo ${MINIKUBE_IP} | sed 's/\./-/g').nip.io:30967/.well-known/openid-configuration" > /dev/null 2>&1; then
+      echo "Hydra OIDC endpoint is accessible!"
+      break
+    fi
+    echo "Waiting for Hydra OIDC endpoint... (attempt $i/40)"
+    sleep 2
+  done
+  if curl -k -s "https://$(echo ${MINIKUBE_IP} | sed 's/\./-/g').nip.io:30967/.well-known/openid-configuration" > /dev/null 2>&1; then
+    echo "Hydra OIDC endpoint is accessible!"
+  else
+    echo "Error: Hydra OIDC endpoint is not accessible!"
+    exit 1
+  fi
+
+  echo "Creating istio-system namespace for Kiali deployment"
+  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create namespace istio-system
+
+  # Deploy OAuth2 Proxy for header authentication testing
+  echo "Deploying OAuth2 Proxy for header authentication..."
+  cat <<EOF | ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- apply -f -
 apiVersion: v1
 kind: Namespace
 metadata:
@@ -228,15 +231,15 @@ data:
     cookie_secret="secretxxsecretxx"
     provider="oidc"
     email_domains="example.com"
-    oidc_issuer_url="https://${KUBE_HOSTNAME}:32000"
+    oidc_issuer_url="https://${KUBE_HOSTNAME}:30967"
     client_id="kiali-app"
     cookie_secure="false"
-    redirect_url="http://kiali-proxy.${MINIKUBE_IP}.nip.io:30805/oauth2/callback"
+    redirect_url="http://kiali-proxy.${KUBE_HOSTNAME}:30805/oauth2/callback"
     upstreams="http://kiali.istio-system.svc:20001"
     pass_authorization_header = true
     set_authorization_header = true
     ssl_insecure_skip_verify = true
-    client_secret="dontTellAnyone"
+    client_secret="doNotTell"
 ---
 apiVersion: apps/v1
 kind: Deployment
@@ -298,79 +301,43 @@ spec:
   selector:
     k8s-app: oauth2-proxy
 EOF
+  [ "$?" != "0" ] && echo "ERROR: Failed to deploy OAuth2 Proxy" && exit 1
 
-  # Install dex
-  echo "Deploying dex..."
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create namespace dex
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create secret tls dex.example.com.tls --cert=${CERTS_PATH}/cert.pem --key=${CERTS_PATH}/key.pem -n dex
-
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- apply -n dex -f ${DEX_VERSION_PATH}/examples/k8s/dex.kiali.yaml
-  [ "$?" != "0" ] && echo "ERROR: Failed to install dex" && exit 1
-  echo "Deploying oauth2 proxy..."
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create -f ${DEX_VERSION_PATH}/examples/k8s/oauth2.proxy
-  [ "$?" != "0" ] && echo "ERROR: Failed to deploy oauth2 proxy" && exit 1
-
-  # Restart minikube
-  echo "Restarting minikube with proper flags for API server and the autodetected registry IP..."
-  ${MINIKUBE_EXEC_WITH_PROFILE} stop
-  ${MINIKUBE_EXEC_WITH_PROFILE} start \
-    ${MINIKUBE_START_FLAGS} \
-    ${INSECURE_REGISTRY_START_ARG} \
-    --insecure-registry ${MINIKUBE_IP}:5000 \
-    --cni=${K8S_CNI} \
-    --cpus=${K8S_CPU} \
-    --memory=${K8S_MEMORY} \
-    --disk-size=${K8S_DISK} \
-    --driver=${K8S_DRIVER} \
-    --kubernetes-version=${K8S_VERSION} \
-    --extra-config=apiserver.oidc-issuer-url=https://${KUBE_HOSTNAME}:32000 \
-    --extra-config=apiserver.oidc-username-claim=email \
-    --extra-config=apiserver.oidc-ca-file=/var/lib/minikube/certs/ca.pem \
-    --extra-config=apiserver.oidc-client-id=kiali-app \
-    --extra-config=apiserver.oidc-groups-claim=groups
-  [ "$?" != "0" ] && echo "ERROR: Failed to restart minikube in preparation for dex" && exit 1
-
-  echo "Need to create the OpenID secret now - assuming Kiali will eventually be installed in istio-system"
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create namespace istio-system
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create secret generic kiali --from-literal="oidc-secret=dontTellAnyone" -n istio-system
+  echo "Minikube should now be configured with Ory Hydra OpenID connect and OAuth2 Proxy. Just wait for all pods to start."
 
-  echo "Minikube should now be configured with OpenID connect. Just wait for all pods to start."
   cat <<EOF
-Commands to query Dex deployments and pods:
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get deployments -n dex
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get pods -n dex
+Commands to query Hydra deployments and pods:
+  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get deployments -n ory
+  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get pods -n ory
 
-OpenID configuration for Kiali CR:
+OpenID configuration for Kiali CR (confidential client - secret required):
   auth:
     strategy: openid
     openid:
       client_id: "kiali-app"
       insecure_skip_verify_tls: true
-      issuer_uri: "https://${KUBE_HOSTNAME}:32000"
+      issuer_uri: "https://$(echo ${MINIKUBE_IP} | sed 's/\./-/g').nip.io:30967"
       username_claim: "email"
 
 OpenID user is:
   Username: admin@example.com
   Password: password
 
-Kiali reverse proxy URL: http://kiali-proxy.${MINIKUBE_IP}.nip.io:30805
-
-The Kiali OIDC secret named 'kiali' has been created in istio-system namespace.
-If you need to recreate this OIDC secret, run this command:
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create secret generic kiali --from-literal="oidc-secret=dontTellAnyone" -n istio-system
+NOTE: kiali-app is configured as a confidential OAuth2 client (client secret required).
+A Kubernetes secret containing the OAuth2 client secret needs to be created for Kiali to authenticate with this client.
 
 EOF
 
-  if [ "${DEX_USER_NAMESPACES}" != "none" ]; then
-    if [ "${DEX_USER_NAMESPACES}" == "all" ]; then
+  if [ "${HYDRA_USER_NAMESPACES}" != "none" ]; then
+    if [ "${HYDRA_USER_NAMESPACES}" == "all" ]; then
       echo "!!!CAUTION!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
       echo "!! The user 'admin@example.com' will be granted cluster-admin permissions ! "
       echo "!!!CAUTION!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
-      ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create clusterrolebinding dex-rolebinding-admin --clusterrole=cluster-admin --user="admin@example.com"
+      ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create clusterrolebinding hydra-rolebinding-admin --clusterrole=cluster-admin --user="admin@example.com"
     else
       echo "After you install Kiali, execute these commands to grant the user 'admin@example.com' permission to see specific namespaces:"
-      for ns in ${DEX_USER_NAMESPACES}; do
-        echo ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create rolebinding dex-rolebinding-${ns} --clusterrole=kiali --user="admin@example.com" --namespace=${ns}
+      for ns in ${HYDRA_USER_NAMESPACES}; do
+        echo ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create rolebinding hydra-rolebinding-${ns} --clusterrole=kiali --user="admin@example.com" --namespace=${ns}
       done
     fi
   fi
@@ -548,10 +515,9 @@ while [[ $# -gt 0 ]]; do
     resetclock) _CMD="resetclock"; shift ;;
     olm) _CMD="olm"; shift ;;
     -ce|--client-exe) CLIENT_EXE="$2"; shift;shift ;;
-    -de|--dex-enabled) DEX_ENABLED="$2"; shift;shift ;;
-    -dr|--dex-repo) DEX_REPO="$2"; shift;shift ;;
-    -dun|--dex-user-namespaces) DEX_USER_NAMESPACES="$2"; shift;shift ;;
-    -dv|--dex-version) DEX_VERSION="$2"; shift;shift ;;
+    -he|--hydra-enabled) HYDRA_ENABLED="$2"; shift;shift ;;
+    -hun|--hydra-user-namespaces) HYDRA_USER_NAMESPACES="$2"; shift;shift ;;
+    -hv|--hydra-version) HYDRA_VERSION="$2"; shift;shift ;;
     -iri|--insecure-registry-ip) INSECURE_REGISTRY_IP="$2"; shift;shift ;;
     -kc|--kubernetes-cpu) K8S_CPU="$2"; shift;shift ;;
     -kcni|--kubernetes-cni) K8S_CNI="$2"; shift;shift ;;
@@ -577,27 +543,23 @@ Valid options:
       The kubectl client to use.
       Only used for needing to install Istio or the Bookinfo demo. The "minikube kubectl" command will be used instead when possible.
       Default: ${DEFAULT_CLIENT_EXE}
-  -de|--dex-enabled
-      If true, install and configure Dex. This provides an OpenID Connect implementation.
+  -he|--hydra-enabled
+      If true, install and configure Ory Hydra. This provides an OpenID Connect implementation with multi-audience support.
       Only used for the 'start' command.
-      Default: ${DEFAULT_DEX_ENABLED}
-  -dr|--dex-repo
-      The github repo where the Dex archive is to be found.
-      Only used for the 'start' command and when Dex is to be installed (--dex-enabled=true).
-      Default: ${DEFAULT_DEX_REPO}
-  -dun|--dex-user-namespaces
+      Default: ${DEFAULT_HYDRA_ENABLED}
+  -hun|--hydra-user-namespaces
       A space-separated list of namespaces that you would like the admin@example.com user to be able to see.
       If this value is set to "all", the admin@example.com user will immediately be granted cluster-admin permissions.
       If this value is set to "none", nothing is done.
       Any other value and this will not trigger actual creation of role bindings but instead the script merely
       outputs the commands in the final summary that you should then execute in order to grant those permissions.
       This is because the namespaces may not exist yet (such as "bookinfo") nor will the Kiali role exist.
-      Only used for the 'start' command and when Dex is to be installed (--dex-enabled=true).
-      Default: ${DEFAULT_DEX_USER_NAMESPACES}
-  -dv|--dex-version
-      The version of Dex to be installed.
-      Only used for the 'start' command and when Dex is to be installed (--dex-enabled=true).
-      Default: ${DEFAULT_DEX_VERSION}
+      Only used for the 'start' command and when Hydra is to be installed (--hydra-enabled=true).
+      Default: ${DEFAULT_HYDRA_USER_NAMESPACES}
+  -hv|--hydra-version
+      The version of Ory Hydra to be installed.
+      Only used for the 'start' command and when Hydra is to be installed (--hydra-enabled=true).
+      Default: ${DEFAULT_HYDRA_VERSION}
   -iri|--insecure-registry-ip
       This is used for the setting up an insecure registry IP within the minikube docker daemon.
       This is needed to easily authenticate and push images to the docker daemon.
@@ -696,10 +658,9 @@ done
 
 # Prepare some env vars
 : ${CLIENT_EXE:=${DEFAULT_CLIENT_EXE}}
-: ${DEX_ENABLED:=${DEFAULT_DEX_ENABLED}}
-: ${DEX_REPO:=${DEFAULT_DEX_REPO}}
-: ${DEX_USER_NAMESPACES:=${DEFAULT_DEX_USER_NAMESPACES}}
-: ${DEX_VERSION:=${DEFAULT_DEX_VERSION}}
+: ${HYDRA_ENABLED:=${DEFAULT_HYDRA_ENABLED}}
+: ${HYDRA_USER_NAMESPACES:=${DEFAULT_HYDRA_USER_NAMESPACES}}
+: ${HYDRA_VERSION:=${DEFAULT_HYDRA_VERSION}}
 : ${INSECURE_REGISTRY_IP:=${DEFAULT_INSECURE_REGISTRY_IP}}
 : ${K8S_CNI:=${DEFAULT_K8S_CNI}}
 : ${K8S_CPU:=${DEFAULT_K8S_CPU}}
@@ -724,10 +685,9 @@ else
 fi
 
 debug "CLIENT_EXE=$CLIENT_EXE"
-debug "DEX_ENABLED=$DEX_ENABLED"
-debug "DEX_REPO=$DEX_REPO"
-debug "DEX_USER_NAMESPACES=$DEX_USER_NAMESPACES"
-debug "DEX_VERSION=$DEX_VERSION"
+debug "HYDRA_ENABLED=$HYDRA_ENABLED"
+debug "HYDRA_USER_NAMESPACES=$HYDRA_USER_NAMESPACES"
+debug "HYDRA_VERSION=$HYDRA_VERSION"
 debug "INSECURE_REGISTRY_IP=$INSECURE_REGISTRY_IP"
 debug "INSECURE_REGISTRY_START_ARG=$INSECURE_REGISTRY_START_ARG"
 debug "K8S_CNI=$K8S_CNI"
@@ -812,8 +772,8 @@ LBCONFIGMAP
     [ "$?" != "0" ] && echo "ERROR: Failed to configure metallb addon" && exit 1
   fi
 
-  if [ "${DEX_ENABLED}" == "true" ]; then
-    install_dex
+  if [ "${HYDRA_ENABLED}" == "true" ]; then
+    install_hydra
   fi
 
   if [ "${OLM_ENABLED}" == "true" ]; then
diff --git a/hack/ory-hydra/manifests/hydra-ui-simple.yaml b/hack/ory-hydra/manifests/hydra-ui-simple.yaml
new file mode 100644
index 000000000..51ac1b03e
--- /dev/null
+++ b/hack/ory-hydra/manifests/hydra-ui-simple.yaml
@@ -0,0 +1,349 @@
+# Simplified Hydra Login/Consent UI for Testing
+# This UI handles the login and consent flows that Hydra delegates to external providers
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: hydra-ui-simple
+  namespace: ory
+data:
+  server.js: |
+    const express = require('express');
+    const https = require('https');
+    const fs = require('fs');
+    const { URL } = require('url');
+
+    const app = express();
+    app.use(express.urlencoded({ extended: true }));
+
+    const HYDRA_ADMIN_URL = process.env.HYDRA_ADMIN_URL || 'https://hydra-admin:4445';
+    const httpsAgent = new https.Agent({ rejectUnauthorized: false });
+
+    // Test user for molecule tests
+    const testUser = {
+      email: 'admin@example.com',
+      password: 'password',
+      name: 'Admin User',
+      groups: ['admin', 'users']
+    };
+
+    // Login flow
+    app.get('/login', async (req, res) => {
+      const challenge = req.query.login_challenge;
+      if (!challenge) return res.status(400).send('Missing login challenge');
+
+      // Simple login form with proper challenge handling for Hydra v2.2.0 CSRF protection
+      res.send(`
+        <html><body style="font-family:Arial;max-width:400px;margin:100px auto;padding:20px">
+          <h2>Kiali Test Login</h2>
+          <form method="post" action="/login?login_challenge=${challenge}">
+            <input type="hidden" name="challenge" value="${challenge}">
+            <div style="margin-bottom:15px">
+              <label>Email:</label><br>
+              <input type="email" name="email" value="admin@example.com" style="width:100%;padding:8px" required>
+            </div>
+            <div style="margin-bottom:15px">
+              <label>Password:</label><br>
+              <input type="password" name="password" value="password" style="width:100%;padding:8px" required>
+            </div>
+            <button type="submit" style="background:#007bff;color:white;padding:10px 20px;border:none;width:100%">Login</button>
+          </form>
+        </body></html>
+      `);
+    });
+
+    app.post('/login', async (req, res) => {
+      const challenge = req.body.challenge || req.query.login_challenge;
+      const { email, password } = req.body;
+
+      if (!challenge) return res.status(400).send('Missing login challenge');
+
+      // Validate credentials (simple test validation)
+      if (email !== testUser.email || password !== testUser.password) {
+        return res.status(401).send('Invalid credentials');
+      }
+
+      try {
+        console.log('Attempting login acceptance with challenge:', challenge);
+        console.log('Using admin URL:', HYDRA_ADMIN_URL);
+        const fullUrl = `${HYDRA_ADMIN_URL}/admin/oauth2/auth/requests/login/accept`;
+        console.log('Full API URL:', fullUrl);
+
+        // Accept login using native https module
+        const url = new URL(`${fullUrl}?login_challenge=${encodeURIComponent(challenge)}`);
+        const postData = JSON.stringify({
+          subject: email,
+          remember: true,
+          remember_for: 3600,
+          context: { name: testUser.name, email: testUser.email, groups: testUser.groups }
+        });
+
+        const options = {
+          hostname: url.hostname,
+          port: url.port,
+          path: url.pathname + url.search,
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+            'Content-Length': Buffer.byteLength(postData)
+          },
+          rejectUnauthorized: false
+        };
+
+        console.log('Connection details:', { hostname: url.hostname, port: url.port, path: url.pathname + url.search });
+
+        const response = await new Promise((resolve, reject) => {
+          const makeRequest = (requestUrl) => {
+            const reqUrl = new URL(requestUrl);
+            const reqOptions = {
+              hostname: reqUrl.hostname,
+              port: reqUrl.port,
+              path: reqUrl.pathname + reqUrl.search,
+              method: 'PUT',
+              headers: {
+                'Content-Type': 'application/json',
+                'Content-Length': Buffer.byteLength(postData)
+              },
+              rejectUnauthorized: false
+            };
+
+            const req = https.request(reqOptions, (res) => {
+              let data = '';
+              res.on('data', (chunk) => data += chunk);
+              res.on('end', () => {
+                console.log('Response status:', res.statusCode);
+                console.log('Response data:', data);
+
+                if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+                  // Follow redirect
+                  console.log('Following redirect to:', res.headers.location);
+                  makeRequest(res.headers.location);
+                } else {
+                  try {
+                    const parsed = JSON.parse(data);
+                    resolve({ data: parsed, status: res.statusCode });
+                  } catch (e) {
+                    resolve({ data: { error: 'Parse failed', raw: data }, status: res.statusCode });
+                  }
+                }
+              });
+            });
+            req.on('error', reject);
+            req.write(postData);
+            req.end();
+          };
+
+          makeRequest(fullUrl + '?' + new URLSearchParams({ login_challenge: challenge }));
+        });
+
+        console.log('Login response:', response.data);
+        console.log('Login acceptance successful, redirecting to:', response.data.redirect_to);
+        res.redirect(response.data.redirect_to);
+      } catch (error) {
+        console.error('Login error details:', error.response?.data || error.message);
+        console.error('Login error status:', error.response?.status);
+        console.error('Login error challenge:', challenge);
+        res.status(500).send('Login failed');
+      }
+    });
+
+    // Consent flow (auto-accept for testing)
+    app.get('/consent', async (req, res) => {
+      const challenge = req.query.consent_challenge;
+      console.log('Consent endpoint called with challenge:', challenge);
+      if (!challenge) return res.status(400).send('Missing consent challenge');
+
+      try {
+        // Get consent request
+        // Get consent request using native https
+        const consentUrl = `${HYDRA_ADMIN_URL}/admin/oauth2/auth/requests/consent?consent_challenge=${encodeURIComponent(challenge)}`;
+        const consentReq = await new Promise((resolve, reject) => {
+          const url = new URL(consentUrl);
+          const options = {
+            hostname: url.hostname,
+            port: url.port,
+            path: url.pathname + url.search,
+            method: 'GET',
+            rejectUnauthorized: false
+          };
+          const req = https.request(options, (res) => {
+            let data = '';
+            res.on('data', (chunk) => data += chunk);
+            res.on('end', () => resolve({ data: JSON.parse(data) }));
+          });
+          req.on('error', reject);
+          req.end();
+        });
+
+        // Auto-accept consent for testing using native https
+        const acceptUrl = `${HYDRA_ADMIN_URL}/admin/oauth2/auth/requests/consent/accept?consent_challenge=${encodeURIComponent(challenge)}`;
+        const acceptData = JSON.stringify({
+          grant_scope: consentReq.data.requested_scope,
+          grant_access_token_audience: consentReq.data.requested_access_token_audience,
+          session: {
+            id_token: {
+              email: consentReq.data.subject,
+              groups: testUser.groups
+            }
+          }
+        });
+        const acceptResponse = await new Promise((resolve, reject) => {
+          const url = new URL(acceptUrl);
+          const options = {
+            hostname: url.hostname,
+            port: url.port,
+            path: url.pathname + url.search,
+            method: 'PUT',
+            headers: {
+              'Content-Type': 'application/json',
+              'Content-Length': Buffer.byteLength(acceptData)
+            },
+            rejectUnauthorized: false
+          };
+          const req = https.request(options, (res) => {
+            let data = '';
+            res.on('data', (chunk) => data += chunk);
+            res.on('end', () => resolve({ data: JSON.parse(data) }));
+          });
+          req.on('error', reject);
+          req.write(acceptData);
+          req.end();
+        });
+
+        console.log('Consent acceptance successful, redirecting to:', acceptResponse.data.redirect_to);
+        res.redirect(acceptResponse.data.redirect_to);
+      } catch (error) {
+        console.error('Consent error details:', error.response?.data || error.message);
+        console.error('Consent error status:', error.response?.status);
+        res.status(500).send('Consent failed');
+      }
+    });
+
+    app.get('/health', (req, res) => res.json({ status: 'ok' }));
+
+    const port = process.env.PORT || 3000;
+    const httpsPort = process.env.HTTPS_PORT || 8443;
+
+    // Start HTTP server (for internal health checks)
+    app.listen(port, () => console.log(`Hydra UI HTTP listening on port ${port}`));
+
+    // Start HTTPS server if certificates are available
+    try {
+      if (fs.existsSync('/etc/ssl/certs/cert.pem') && fs.existsSync('/etc/ssl/certs/key.pem')) {
+        const httpsOptions = {
+          key: fs.readFileSync('/etc/ssl/certs/key.pem'),
+          cert: fs.readFileSync('/etc/ssl/certs/cert.pem')
+        };
+        https.createServer(httpsOptions, app).listen(httpsPort, () => {
+          console.log(`Hydra UI HTTPS listening on port ${httpsPort}`);
+        });
+      } else {
+        console.log('HTTPS certificates not found, only HTTP server started');
+      }
+    } catch (err) {
+      console.log('Failed to start HTTPS server:', err.message);
+    }
+
+  package.json: |
+    {
+      "name": "hydra-ui-simple",
+      "version": "1.0.0",
+      "main": "server.js",
+      "scripts": { "start": "node server.js" },
+      "dependencies": {
+        "express": "^4.18.2"
+      }
+    }
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: hydra-ui
+  namespace: ory
+  labels:
+    app: hydra-ui
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: hydra-ui
+  template:
+    metadata:
+      labels:
+        app: hydra-ui
+    spec:
+      containers:
+      - name: hydra-ui
+        image: node:18-alpine
+        workingDir: /app
+        command: ["sh", "-c", "cp /app-config/* /app/ && npm install && npm start"]
+        ports:
+        - containerPort: 3000
+          name: http
+        - containerPort: 8443
+          name: https
+        env:
+        - name: HYDRA_ADMIN_URL
+          value: "https://hydra-admin:4445"
+        - name: PORT
+          value: "3000"
+        - name: HTTPS_PORT
+          value: "8443"
+        - name: NODE_TLS_REJECT_UNAUTHORIZED
+          value: "0"
+        volumeMounts:
+        - name: ui-config
+          mountPath: /app-config
+        - name: hydra-certs
+          mountPath: /etc/ssl/certs
+          readOnly: true
+        resources:
+          requests:
+            memory: "256Mi"
+            cpu: "50m"
+          limits:
+            memory: "512Mi"
+            cpu: "200m"
+        readinessProbe:
+          httpGet:
+            path: /health
+            port: 3000
+          initialDelaySeconds: 10
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: 3000
+          initialDelaySeconds: 20
+          periodSeconds: 10
+      volumes:
+      - name: ui-config
+        configMap:
+          name: hydra-ui-simple
+      - name: hydra-certs
+        secret:
+          secretName: hydra-tls
+          defaultMode: 0644
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: hydra-ui
+  namespace: ory
+  labels:
+    app: hydra-ui
+spec:
+  type: NodePort
+  ports:
+  - port: 3000
+    targetPort: 3000
+    protocol: TCP
+    name: http
+  - port: 8443
+    targetPort: 8443
+    nodePort: 30800
+    protocol: TCP
+    name: https
+  selector:
+    app: hydra-ui
diff --git a/hack/ory-hydra/manifests/postgresql.yaml b/hack/ory-hydra/manifests/postgresql.yaml
new file mode 100644
index 000000000..df0c218ab
--- /dev/null
+++ b/hack/ory-hydra/manifests/postgresql.yaml
@@ -0,0 +1,104 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: ory
+  labels:
+    name: ory
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: postgresql-config
+  namespace: ory
+data:
+  POSTGRES_DB: hydra
+  POSTGRES_USER: hydra
+  POSTGRES_PASSWORD: hydra_password
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: postgresql
+  namespace: ory
+  labels:
+    app: postgresql
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: postgresql
+  template:
+    metadata:
+      labels:
+        app: postgresql
+    spec:
+      containers:
+      - name: postgresql
+        image: postgres:15-alpine
+        ports:
+        - containerPort: 5432
+          name: postgres
+        env:
+        - name: POSTGRES_DB
+          valueFrom:
+            configMapKeyRef:
+              name: postgresql-config
+              key: POSTGRES_DB
+        - name: POSTGRES_USER
+          valueFrom:
+            configMapKeyRef:
+              name: postgresql-config
+              key: POSTGRES_USER
+        - name: POSTGRES_PASSWORD
+          valueFrom:
+            configMapKeyRef:
+              name: postgresql-config
+              key: POSTGRES_PASSWORD
+        - name: PGDATA
+          value: /var/lib/postgresql/data/pgdata
+        volumeMounts:
+        - name: postgresql-storage
+          mountPath: /var/lib/postgresql/data
+        resources:
+          requests:
+            memory: "128Mi"
+            cpu: "100m"
+          limits:
+            memory: "256Mi"
+            cpu: "200m"
+        readinessProbe:
+          exec:
+            command:
+            - /bin/sh
+            - -c
+            - pg_isready -U hydra -d hydra
+          initialDelaySeconds: 15
+          periodSeconds: 5
+        livenessProbe:
+          exec:
+            command:
+            - /bin/sh
+            - -c
+            - pg_isready -U hydra -d hydra
+          initialDelaySeconds: 30
+          periodSeconds: 10
+      volumes:
+      - name: postgresql-storage
+        emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: postgresql
+  namespace: ory
+  labels:
+    app: postgresql
+spec:
+  type: ClusterIP
+  ports:
+  - port: 5432
+    targetPort: 5432
+    protocol: TCP
+    name: postgres
+  selector:
+    app: postgresql
diff --git a/hack/ory-hydra/scripts/gencert.sh b/hack/ory-hydra/scripts/gencert.sh
new file mode 100755
index 000000000..6ea78bc11
--- /dev/null
+++ b/hack/ory-hydra/scripts/gencert.sh
@@ -0,0 +1,153 @@
+#!/bin/bash
+
+# Ory Hydra Certificate Generation Script
+# Based on DEX gencert.sh pattern but adapted for Hydra with enhanced TLS support
+
+set -e
+
+# Parameters
+HOSTNAME="${1:-hydra.example.com}"
+MINIKUBE_IP="${2:-}"
+CERT_DIR="${3:-ssl}"
+
+echo "=== Ory Hydra Certificate Generation ==="
+echo "Hostname: ${HOSTNAME}"
+echo "Minikube IP: ${MINIKUBE_IP}"
+echo "Certificate Directory: ${CERT_DIR}"
+echo ""
+
+# Create certificate directory
+mkdir -p "${CERT_DIR}"
+
+# Build Subject Alternative Names
+SAN_ENTRIES="DNS.1 = ${HOSTNAME}"
+SAN_COUNT=1
+
+# Add minikube IP if provided
+if [[ -n "${MINIKUBE_IP}" ]]; then
+    SAN_COUNT=$((SAN_COUNT + 1))
+    SAN_ENTRIES="${SAN_ENTRIES}
+IP.1 = ${MINIKUBE_IP}"
+    echo "Adding minikube IP ${MINIKUBE_IP} to certificate"
+
+    # Add nip.io hostname for external access
+    HOSTNAME_DASHED=$(echo "${MINIKUBE_IP}" | sed 's/\./-/g')
+    SAN_COUNT=$((SAN_COUNT + 1))
+    SAN_ENTRIES="${SAN_ENTRIES}
+DNS.${SAN_COUNT} = ${HOSTNAME_DASHED}.nip.io"
+    echo "Adding nip.io hostname ${HOSTNAME_DASHED}.nip.io to certificate"
+fi
+
+# Add localhost and common service names
+SAN_COUNT=$((SAN_COUNT + 1))
+SAN_ENTRIES="${SAN_ENTRIES}
+DNS.${SAN_COUNT} = localhost"
+
+SAN_COUNT=$((SAN_COUNT + 1))
+SAN_ENTRIES="${SAN_ENTRIES}
+DNS.${SAN_COUNT} = hydra"
+
+SAN_COUNT=$((SAN_COUNT + 1))
+SAN_ENTRIES="${SAN_ENTRIES}
+DNS.${SAN_COUNT} = hydra.ory"
+
+SAN_COUNT=$((SAN_COUNT + 1))
+SAN_ENTRIES="${SAN_ENTRIES}
+DNS.${SAN_COUNT} = hydra.ory.svc.cluster.local"
+
+# Add localhost IP
+SAN_ENTRIES="${SAN_ENTRIES}
+IP.2 = 127.0.0.1"
+
+echo "Certificate will include these Subject Alternative Names:"
+echo "${SAN_ENTRIES}" | grep -E "^(DNS|IP)\." | sed 's/^/  /'
+echo ""
+
+# Create OpenSSL configuration
+cat << EOF > "${CERT_DIR}/req.cnf"
+[req]
+req_extensions = v3_req
+distinguished_name = req_distinguished_name
+prompt = no
+
+[req_distinguished_name]
+CN = ${HOSTNAME}
+O = Kiali Test Infrastructure
+OU = Ory Hydra
+L = Test
+ST = Test
+C = US
+
+[ v3_req ]
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+extendedKeyUsage = serverAuth, clientAuth
+subjectAltName = @alt_names
+
+[alt_names]
+${SAN_ENTRIES}
+EOF
+
+# Generate CA private key
+echo "Generating CA private key..."
+openssl genrsa -out "${CERT_DIR}/ca-key.pem" 2048
+
+# Generate CA certificate
+echo "Generating CA certificate..."
+openssl req -x509 -new -nodes \
+    -key "${CERT_DIR}/ca-key.pem" \
+    -days 30 \
+    -out "${CERT_DIR}/ca.pem" \
+    -subj "/CN=Kiali Test CA/O=Kiali Test Infrastructure/OU=Certificate Authority"
+
+# Generate server private key
+echo "Generating server private key..."
+openssl genrsa -out "${CERT_DIR}/key.pem" 2048
+
+# Generate certificate signing request
+echo "Generating certificate signing request..."
+openssl req -new \
+    -key "${CERT_DIR}/key.pem" \
+    -out "${CERT_DIR}/csr.pem" \
+    -config "${CERT_DIR}/req.cnf"
+
+# Generate server certificate signed by CA
+echo "Generating server certificate..."
+openssl x509 -req \
+    -in "${CERT_DIR}/csr.pem" \
+    -CA "${CERT_DIR}/ca.pem" \
+    -CAkey "${CERT_DIR}/ca-key.pem" \
+    -CAcreateserial \
+    -out "${CERT_DIR}/cert.pem" \
+    -days 30 \
+    -extensions v3_req \
+    -extfile "${CERT_DIR}/req.cnf"
+
+# Verify certificate
+echo "Verifying certificate..."
+if openssl verify -CAfile "${CERT_DIR}/ca.pem" "${CERT_DIR}/cert.pem"; then
+    echo "✅ Certificate verification successful"
+else
+    echo "❌ Certificate verification failed"
+    exit 1
+fi
+
+echo ""
+echo "🎉 Certificate generation complete!"
+echo ""
+echo "Files generated in ${CERT_DIR}/ directory:"
+ls -la "${CERT_DIR}/"
+
+echo ""
+echo "📋 Certificate details:"
+echo "Subject: $(openssl x509 -in "${CERT_DIR}/cert.pem" -noout -subject)"
+echo "Issuer: $(openssl x509 -in "${CERT_DIR}/cert.pem" -noout -issuer)"
+echo "Valid from: $(openssl x509 -in "${CERT_DIR}/cert.pem" -noout -startdate)"
+echo "Valid to: $(openssl x509 -in "${CERT_DIR}/cert.pem" -noout -enddate)"
+
+echo ""
+echo "🔍 Subject Alternative Names:"
+openssl x509 -in "${CERT_DIR}/cert.pem" -text -noout | grep -A 10 "Subject Alternative Name" || echo "No SAN found"
+
+echo ""
+echo "📁 Certificate files ready for Kubernetes secret creation"
diff --git a/hack/ory-hydra/scripts/install-hydra.sh b/hack/ory-hydra/scripts/install-hydra.sh
new file mode 100755
index 000000000..5b43acc0a
--- /dev/null
+++ b/hack/ory-hydra/scripts/install-hydra.sh
@@ -0,0 +1,361 @@
+#!/bin/bash
+
+# Ory Hydra Installation Script using Official Helm Chart
+# This script follows Kiali patterns and avoids Bitnami charts per requirements
+
+set -e
+
+# Get the absolute path to the script directory at the start
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+NAMESPACE="${1:-ory}"
+HOSTNAME="${2:-hydra.example.com}"
+HELM_RELEASE_NAME="${3:-hydra}"
+MINIKUBE_IP="${4:-}"
+
+# Use provided kubectl command or default
+KUBECTL_CMD="${KUBECTL_CMD:-kubectl}"
+
+# Use provided Hydra version or default
+HYDRA_VERSION="${HYDRA_VERSION:-v2.2.0}"
+
+# Use provided minikube profile or default
+MINIKUBE_PROFILE="${MINIKUBE_PROFILE:-minikube}"
+
+# Auto-detect minikube IP if not provided
+if [[ -z "${MINIKUBE_IP}" ]]; then
+    if command -v minikube &> /dev/null; then
+        MINIKUBE_IP=$(minikube ip -p "${MINIKUBE_PROFILE}" 2>/dev/null || minikube ip 2>/dev/null || echo "")
+        if [[ -n "${MINIKUBE_IP}" ]]; then
+            echo "Auto-detected minikube IP: ${MINIKUBE_IP}"
+            HOSTNAME="${MINIKUBE_IP}"
+        fi
+    fi
+fi
+
+echo "=== Installing Ory Hydra via Official Helm Chart ==="
+echo "Namespace: ${NAMESPACE}"
+echo "Hostname: ${HOSTNAME}"
+echo "Release Name: ${HELM_RELEASE_NAME}"
+echo "Hydra Version: ${HYDRA_VERSION}"
+echo ""
+
+# Check if helm is available
+if ! command -v helm &> /dev/null; then
+    echo "ERROR: helm is required but not found in PATH"
+    exit 1
+fi
+
+# Add Ory Helm repository
+echo "Adding Ory Helm repository..."
+helm repo add ory https://k8s.ory.sh/helm/charts
+helm repo update
+
+# Create namespace if it doesn't exist
+echo "Creating namespace ${NAMESPACE}..."
+${KUBECTL_CMD} create namespace ${NAMESPACE} --dry-run=client -o yaml | ${KUBECTL_CMD} apply -f -
+
+# Install PostgreSQL and UI (using our custom manifests)
+echo "Installing PostgreSQL database..."
+${KUBECTL_CMD} apply -f "$(dirname "$0")/../manifests/postgresql.yaml"
+
+echo "Installing Hydra UI..."
+${KUBECTL_CMD} apply -f "$(dirname "$0")/../manifests/hydra-ui-simple.yaml"
+
+# Wait for PostgreSQL to be ready
+echo "Waiting for PostgreSQL to be ready..."
+${KUBECTL_CMD} wait --for=condition=ready pod -l app=postgresql -n ${NAMESPACE} --timeout=120s
+
+# Generate certificates with enhanced TLS support
+echo "Generating TLS certificates..."
+CERTS_DIR="/tmp/hydra-certs-$$"
+mkdir -p "${CERTS_DIR}"
+cd "${CERTS_DIR}"
+rm -rf ssl 2>/dev/null || true
+
+# Generate certificates with minikube IP support
+GENCERT_SCRIPT="${SCRIPT_DIR}/gencert.sh"
+if [[ -n "${MINIKUBE_IP}" ]]; then
+    echo "Generating certificates for hostname ${HOSTNAME} with minikube IP ${MINIKUBE_IP}"
+    bash "${GENCERT_SCRIPT}" "${HOSTNAME}" "${MINIKUBE_IP}" "ssl"
+else
+    echo "Generating certificates for hostname ${HOSTNAME}"
+    bash "${GENCERT_SCRIPT}" "${HOSTNAME}" "" "ssl"
+fi
+
+# Create TLS secret with correct key names for Hydra
+echo "Creating TLS secret..."
+${KUBECTL_CMD} create secret generic hydra-tls \
+    --from-file=cert.pem=ssl/cert.pem \
+    --from-file=key.pem=ssl/key.pem \
+    -n ${NAMESPACE} \
+    --dry-run=client -o yaml | ${KUBECTL_CMD} apply -f -
+
+# Copy Hydra CA certificate to minikube for API server OIDC validation
+echo "Copying Hydra CA certificate to minikube for OIDC validation..."
+if command -v minikube >/dev/null 2>&1; then
+    # Extract CA certificate from the generated certificate
+    openssl x509 -in ssl/cert.pem -out /tmp/hydra-ca.pem 2>/dev/null || cp ssl/cert.pem /tmp/hydra-ca.pem
+
+    # Copy to minikube node
+    if [ -n "${MINIKUBE_PROFILE}" ]; then
+        minikube cp -p "${MINIKUBE_PROFILE}" /tmp/hydra-ca.pem /var/lib/minikube/certs/hydra-ca.pem
+    else
+        minikube cp /tmp/hydra-ca.pem /var/lib/minikube/certs/hydra-ca.pem
+    fi
+
+    # Clean up temporary file
+    rm -f /tmp/hydra-ca.pem
+    echo "Hydra CA certificate copied to minikube successfully"
+else
+    echo "WARNING: minikube command not found. Hydra CA certificate not copied."
+    echo "API server OIDC validation may fail without the CA certificate."
+fi
+
+# Create Hydra values file
+echo "Creating Hydra configuration..."
+
+# Convert IP address to nip.io format (replace dots with dashes)
+NIPIP_HOSTNAME=$(echo "${HOSTNAME}" | sed 's/\./-/g')
+ISSUER_URL="https://${NIPIP_HOSTNAME}.nip.io:30967"
+echo "Using issuer URL: ${ISSUER_URL}"
+
+cat > /tmp/hydra-values.yaml << EOF
+# Ory Hydra Configuration for Kiali Testing
+# Uses official Ory Helm chart (non-Bitnami per requirements)
+
+replicaCount: 1
+
+image:
+  repository: oryd/hydra
+  tag: ${HYDRA_VERSION}
+  pullPolicy: IfNotPresent
+
+# Service configuration - will be patched to NodePort after installation
+# Note: Ory Helm chart doesn't support nodePort in values, so we patch after install
+service:
+  public:
+    enabled: true
+    type: ClusterIP
+    port: 4444
+  admin:
+    enabled: true
+    type: ClusterIP
+    port: 4445
+
+# Database configuration (PostgreSQL)
+hydra:
+  config:
+    dsn: postgres://hydra:hydra_password@postgresql:5432/hydra?sslmode=disable
+
+    urls:
+      self:
+        issuer: ${ISSUER_URL}
+        public: ${ISSUER_URL}
+      login: https://${NIPIP_HOSTNAME}.nip.io:30800/login
+      consent: https://${NIPIP_HOSTNAME}.nip.io:30800/consent
+      logout: https://${NIPIP_HOSTNAME}.nip.io:30800/logout
+
+    secrets:
+      system:
+        - "this-is-a-test-secret-change-me-in-production"
+      cookie:
+        - "this-is-another-test-secret-change-me-too"
+
+    serve:
+      tls:
+        enabled: true
+        cert:
+          path: /etc/hydra/certs/cert.pem
+        key:
+          path: /etc/hydra/certs/key.pem
+      admin:
+        tls:
+          enabled: true
+          cert:
+            path: /etc/hydra/certs/cert.pem
+          key:
+            path: /etc/hydra/certs/key.pem
+
+    oauth2:
+      expose_internal_errors: true
+
+    log:
+      level: debug
+      format: text
+
+  # Enable automatic database migrations
+  automigration:
+    enabled: true
+
+# Resource limits for minikube
+resources:
+  requests:
+    memory: "128Mi"
+    cpu: "100m"
+  limits:
+    memory: "256Mi"
+    cpu: "200m"
+
+# Deployment configuration with TLS certificates and HTTPS health checks
+deployment:
+  extraVolumes:
+    - name: hydra-certs
+      secret:
+        secretName: hydra-tls
+        defaultMode: 0644
+
+  extraVolumeMounts:
+    - name: hydra-certs
+      mountPath: /etc/hydra/certs
+      readOnly: true
+
+  livenessProbe:
+    httpGet:
+      path: /health/alive
+      port: 4445
+      scheme: HTTPS
+    initialDelaySeconds: 30
+    periodSeconds: 10
+
+  readinessProbe:
+    httpGet:
+      path: /health/ready
+      port: 4445
+      scheme: HTTPS
+    initialDelaySeconds: 15
+    periodSeconds: 5
+
+  startupProbe:
+    httpGet:
+      path: /health/ready
+      port: 4445
+      scheme: HTTPS
+    initialDelaySeconds: 5
+    periodSeconds: 5
+    failureThreshold: 30
+EOF
+
+# Install Hydra using Helm (without waiting since we need to patch first)
+echo "Installing Ory Hydra..."
+helm upgrade --install ${HELM_RELEASE_NAME} ory/hydra \
+    --namespace ${NAMESPACE} \
+    --values /tmp/hydra-values.yaml \
+    --timeout=300s
+
+# Wait for deployment to be created before patching
+echo "Waiting for deployment to be created..."
+${KUBECTL_CMD} wait --for=condition=available deployment/hydra -n ${NAMESPACE} --timeout=60s || true
+
+# Add critical environment variables for login/consent URLs
+echo "Adding login/consent URL environment variables..."
+NIPIP_HOSTNAME=$(echo "${HOSTNAME}" | sed 's/\./-/g')
+${KUBECTL_CMD} patch deployment hydra -n ${NAMESPACE} -p '{
+    "spec": {
+        "template": {
+            "spec": {
+                "containers": [{
+                    "name": "hydra",
+                    "args": ["serve", "all", "--dev", "--config", "/etc/config/hydra.yaml"],
+                    "env": [
+                        {"name": "URLS_LOGIN", "value": "https://'"${NIPIP_HOSTNAME}"'.nip.io:30800/login"},
+                        {"name": "URLS_CONSENT", "value": "https://'"${NIPIP_HOSTNAME}"'.nip.io:30800/consent"},
+                        {"name": "URLS_SELF_PUBLIC", "value": "https://'"${NIPIP_HOSTNAME}"'.nip.io:30967"},
+                        {"name": "URLS_SELF_ADMIN", "value": "https://'"${NIPIP_HOSTNAME}"'.nip.io:30448"}
+                    ]
+                }]
+            }
+        }
+    }
+}'
+
+# Update Hydra UI configuration with dynamic IP
+echo "Updating Hydra UI configuration..."
+${KUBECTL_CMD} patch deployment hydra-ui -n ${NAMESPACE} --type='json' -p='[
+    {"op": "replace", "path": "/spec/template/spec/containers/0/env/0/value", "value": "https://'"${NIPIP_HOSTNAME}"'.nip.io:30448"}
+]'
+
+# Wait for Hydra UI to restart and be ready
+echo "Waiting for Hydra UI to restart..."
+${KUBECTL_CMD} rollout status deployment/hydra-ui -n ${NAMESPACE} --timeout=120s
+
+
+# Wait for Hydra to be ready (more robust check)
+echo "Waiting for Hydra to be ready..."
+for i in {1..24}; do
+    if ${KUBECTL_CMD} get pods -n ${NAMESPACE} -l app.kubernetes.io/name=hydra --no-headers | grep -q "1/1.*Running"; then
+        echo "Hydra pod is running!"
+        break
+    fi
+    echo "Waiting for Hydra pod... (attempt $i/24)"
+    sleep 5
+done
+
+# Check Hydra readiness status
+if ! ${KUBECTL_CMD} get pods -n ${NAMESPACE} -l app.kubernetes.io/name=hydra --no-headers | grep -q "1/1.*Running"; then
+    echo "INFO: Hydra pod is still starting up, proceeding with NodePort configuration..."
+fi
+
+# Ensure fixed NodePorts are applied (fix for dynamic port assignment issue)
+echo "Ensuring fixed NodePorts are applied..."
+# Wait for services to be fully created by Helm
+echo "Waiting for services to be created..."
+sleep 10
+
+# Get actual service names from Helm chart
+PUBLIC_SVC=$(${KUBECTL_CMD} get svc -n ${NAMESPACE} -l app.kubernetes.io/name=hydra,app.kubernetes.io/component=public -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "${HELM_RELEASE_NAME}-public")
+ADMIN_SVC=$(${KUBECTL_CMD} get svc -n ${NAMESPACE} -l app.kubernetes.io/name=hydra,app.kubernetes.io/component=admin -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "${HELM_RELEASE_NAME}-admin")
+
+echo "Found services: Public=${PUBLIC_SVC}, Admin=${ADMIN_SVC}"
+
+# Patch services to use fixed NodePorts with correct targetPort references
+echo "Patching ${PUBLIC_SVC} to use NodePort 30967..."
+${KUBECTL_CMD} patch svc ${PUBLIC_SVC} -n ${NAMESPACE} -p '{"spec":{"type":"NodePort","ports":[{"port":4444,"targetPort":"http-public","nodePort":30967,"name":"http","protocol":"TCP"}]}}'
+if [ $? -ne 0 ]; then
+    echo "ERROR: Failed to patch public service to use NodePort 30967"
+    exit 1
+fi
+
+echo "Patching ${ADMIN_SVC} to use NodePort 30448..."
+${KUBECTL_CMD} patch svc ${ADMIN_SVC} -n ${NAMESPACE} -p '{"spec":{"type":"NodePort","ports":[{"port":4445,"targetPort":"http-admin","nodePort":30448,"name":"http","protocol":"TCP"}]}}'
+if [ $? -ne 0 ]; then
+    echo "ERROR: Failed to patch admin service to use NodePort 30448"
+    exit 1
+fi
+
+# Verify the correct ports are assigned
+echo "Verifying NodePort assignments..."
+PUBLIC_PORT=$(${KUBECTL_CMD} get svc ${PUBLIC_SVC} -n ${NAMESPACE} -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "unknown")
+ADMIN_PORT=$(${KUBECTL_CMD} get svc ${ADMIN_SVC} -n ${NAMESPACE} -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "unknown")
+echo "Public API NodePort: ${PUBLIC_PORT} (expected: 30967)"
+echo "Admin API NodePort: ${ADMIN_PORT} (expected: 30448)"
+
+if [ "${PUBLIC_PORT}" != "30967" ] || [ "${ADMIN_PORT}" != "30448" ]; then
+    echo "ERROR: NodePorts do not match expected values (Public: ${PUBLIC_PORT}, Admin: ${ADMIN_PORT})."
+    echo "This will cause issues with k8s-minikube.sh integration. Please check service configuration."
+    exit 1
+fi
+
+# Setup OAuth2 clients
+echo "Setting up OAuth2 clients..."
+sleep 10  # Give Hydra a moment to fully initialize
+
+"${SCRIPT_DIR}/setup-clients.sh" "https://${HOSTNAME}:30448" "${HOSTNAME}"
+
+echo ""
+echo "🎉 Ory Hydra installation complete!"
+echo ""
+echo "Services:"
+echo "  Public API:  https://${HOSTNAME}:30967"
+echo "  Admin API:   https://${HOSTNAME}:30448"
+echo ""
+echo "OAuth2 Clients configured:"
+echo "  - kiali-app (confidential client for Kiali authentication)"
+echo ""
+echo "Next steps:"
+echo "1. Verify all pods are running: kubectl get pods -n ory"
+echo "2. Test with molecule tests"
+
+# Cleanup
+rm -f /tmp/hydra-values.yaml
+rm -rf "${CERTS_DIR}"
diff --git a/hack/ory-hydra/scripts/setup-clients.sh b/hack/ory-hydra/scripts/setup-clients.sh
new file mode 100755
index 000000000..13ef5e0cf
--- /dev/null
+++ b/hack/ory-hydra/scripts/setup-clients.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+
+# Ory Hydra Client Setup Script
+# Creates OAuth2 clients required for Kiali molecule tests
+
+set -e
+
+HYDRA_ADMIN_URL="${1:-https://localhost:30448}"
+HOSTNAME="${2:-hydra.example.com}"
+
+# Convert IP address to nip.io format (replace dots with dashes)
+NIPIP_HOSTNAME=$(echo "${HOSTNAME}" | sed 's/\./-/g')
+
+echo "Setting up OAuth2 clients for Hydra at: ${HYDRA_ADMIN_URL}"
+echo "Using hostname: ${HOSTNAME}"
+echo "Using nip.io hostname: ${NIPIP_HOSTNAME}.nip.io"
+
+# Wait for Hydra to be ready
+echo "Waiting for Hydra Admin API to be ready..."
+for i in {1..30}; do
+    if curl -k -s "${HYDRA_ADMIN_URL}/health/ready" > /dev/null 2>&1; then
+        echo "Hydra Admin API is ready!"
+        break
+    fi
+    echo "Waiting for Hydra... (attempt $i/30)"
+    sleep 2
+done
+
+# Check if Hydra is actually ready
+if ! curl -k -s "${HYDRA_ADMIN_URL}/health/ready" > /dev/null 2>&1; then
+    echo "ERROR: Hydra Admin API is not ready after 60 seconds"
+    exit 1
+fi
+
+echo ""
+echo "Creating OAuth2 clients..."
+
+# Create kiali-app client (supports both single and multi-audience testing)
+echo "Creating kiali-app client (confidential client with multi-audience support)..."
+curl -k -X POST "${HYDRA_ADMIN_URL}/admin/clients" \
+    -H "Content-Type: application/json" \
+    -d '{
+        "client_id": "kiali-app",
+        "client_secret": "doNotTell",
+        "client_name": "Kiali Application",
+        "grant_types": ["authorization_code", "refresh_token", "client_credentials"],
+        "response_types": ["code", "id_token"],
+        "scope": "openid profile email groups",
+        "redirect_uris": [
+            "http://'"${NIPIP_HOSTNAME}"'.nip.io:32080/kiali",
+            "https://'"${NIPIP_HOSTNAME}"'.nip.io:32080/kiali",
+            "http://kiali-proxy.'"${NIPIP_HOSTNAME}"'.nip.io:30805/oauth2/callback"
+        ],
+        "token_endpoint_auth_method": "client_secret_basic",
+        "subject_type": "public",
+        "audience": ["kiali-app", "additional-audience"]
+    }' || echo "kiali-app client may already exist"
+
+echo ""
+echo "OAuth2 clients setup complete!"
+
+# List all clients to verify
+echo ""
+echo "Verifying created clients:"
+curl -k -s "${HYDRA_ADMIN_URL}/admin/clients" | jq -r '.[] | "Client ID: " + .client_id + " | Name: " + .client_name' 2>/dev/null || echo "jq not available, skipping client list verification"
+
+echo ""
+echo "OAuth2 client setup completed successfully!"
\ No newline at end of file
diff --git a/hack/purge-kiali-from-cluster.sh b/hack/purge-kiali-from-cluster.sh
index d0c3fbfaa..ad6f7ef18 100755
--- a/hack/purge-kiali-from-cluster.sh
+++ b/hack/purge-kiali-from-cluster.sh
@@ -66,7 +66,7 @@ msg "Deleting any and all Kiali and OSSMC resources that are found in the cluste
 delete_namespace_resources() {
   local selector_expression="$1"
   msg "Deleting namespace-scoped resources with selector [${selector_expression}]..."
-  for r in $(${CLIENT_EXE} get --ignore-not-found=true all,secrets,sa,configmaps,deployments,roles,rolebindings,ingresses,horizontalpodautoscalers --selector="${selector_expression}" --all-namespaces -o custom-columns=NS:.metadata.namespace,K:.kind,N:.metadata.name --no-headers | sed 's/  */:/g')
+  for r in $(${CLIENT_EXE} get --ignore-not-found=true all,secrets,sa,configmaps,deployments,roles,rolebindings,ingresses,horizontalpodautoscalers,networkpolicies --selector="${selector_expression}" --all-namespaces -o custom-columns=NS:.metadata.namespace,K:.kind,N:.metadata.name --no-headers | sed 's/  */:/g')
   do
     local res_namespace=$(echo $r | cut -d: -f1)
     local res_kind=$(echo $r | cut -d: -f2)
@@ -130,12 +130,6 @@ delete_cluster_resources "app.kubernetes.io/name=kiali-operator"
 delete_namespace_resources "app.kubernetes.io/name=ossmconsole"
 delete_cluster_resources "app.kubernetes.io/name=ossmconsole"
 
-# purge anything using the old labels
-delete_namespace_resources "app=kiali"
-delete_cluster_resources "app=kiali"
-delete_namespace_resources "app=kiali-operator"
-delete_cluster_resources "app=kiali-operator"
-
 msg "Deleting Kiali and OSSMC CRDs..."
 for c in $(${CLIENT_EXE} get crds --ignore-not-found=true kialis.kiali.io ossmconsoles.kiali.io -o custom-columns=N:.metadata.name --no-headers)
 do
diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh
index 348b6d140..cff11b0c6 100755
--- a/hack/run-integration-tests.sh
+++ b/hack/run-integration-tests.sh
@@ -224,7 +224,18 @@ detectRaceConditions() {
   local kubecontext=$1
 
   local context_arg=${kubecontext:+--context ${kubecontext}}
-  kubectl ${context_arg} logs -l app.kubernetes.io/name=kiali --tail=-1 --all-containers -n istio-system | grep -vzq "WARNING: DATA RACE"
+  if ! kubectl ${context_arg} logs -l app.kubernetes.io/name=kiali --tail=-1 --all-containers -n istio-system | grep -vzq "WARNING: DATA RACE"; then
+    echo "ERROR: Data race detected in Kiali logs!"
+    echo "Showing a preview of Kiali logs with data race warnings:"
+    echo "=========================================="
+    kubectl ${context_arg} logs -l app.kubernetes.io/name=kiali --tail=-1 --all-containers -n istio-system | grep -A5 -B5 "WARNING: DATA RACE" || true
+    echo "=========================================="
+    echo "Integration tests failed due to a data race in Kiali."
+    echo "Download and review the Kiali server logs for more details."
+    exit 1
+  else
+    echo "No data race conditions detected in Kiali logs"
+  fi
 }
 
 ensureCypressInstalled() {
diff --git a/handlers/authentication/openid_auth_controller.go b/handlers/authentication/openid_auth_controller.go
index 6bbf79bf2..5ccffdb5c 100644
--- a/handlers/authentication/openid_auth_controller.go
+++ b/handlers/authentication/openid_auth_controller.go
@@ -1247,26 +1247,42 @@ func verifyAudienceClaim(openIdParams *openidFlowHelper, oidCfg config.OpenIdCon
 	} else {
 		switch ac := audienceClaim.(type) {
 		case string:
+			// client has a single audience - clientId must be the same as the audience
 			if oidCfg.ClientId != ac {
-				return fmt.Errorf("the OpenId token is not targeted for Kiali; got aud = '%s'", audienceClaim)
+				return fmt.Errorf("the OpenId token is not targeted for Kiali; got aud [%s]", audienceClaim)
 			}
 		case []string:
-			if len(ac) != 1 {
-				return fmt.Errorf("the OpenId string token was rejected because it has more than one audience; got aud = %v", audienceClaim)
+			// client has multiple audiences within string array - clientId must be in the list
+			if len(ac) == 0 {
+				return errors.New("the OpenId token contains empty audience list")
 			}
-			if oidCfg.ClientId != ac[0] {
-				return fmt.Errorf("the OpenId string token is not targeted for Kiali; got []aud = '%v'", audienceClaim)
+
+			for _, audStr := range ac {
+				if oidCfg.ClientId == audStr {
+					return nil
+				}
 			}
+			return fmt.Errorf("the OpenId token is not targeted for Kiali; ClientId [%s] not found in audiences [%v]", oidCfg.ClientId, ac)
 		case []any:
-			if len(audienceClaim.([]any)) != 1 {
-				return fmt.Errorf("the OpenId token was rejected because it has more than one audience; got aud = %v", audienceClaim)
+			// cilent has multiple audiences within interface{} array -- clientId must be in the list
+			audArray := audienceClaim.([]any)
+			if len(audArray) == 0 {
+				return errors.New("the OpenId token contains empty audience list")
 			}
-			acStr := fmt.Sprintf("%v", audienceClaim.([]any)[0])
-			if oidCfg.ClientId != acStr {
-				return fmt.Errorf("the OpenId token is not targeted for Kiali; got []aud = '%v'", acStr)
+
+			for _, audItem := range audArray {
+				audStr := fmt.Sprintf("%v", audItem)
+				if oidCfg.ClientId == audStr {
+					return nil
+				}
 			}
+			return fmt.Errorf("the OpenId token is not targeted for Kiali; ClientId [%s] not found in audiences [%v]", oidCfg.ClientId, audArray)
 		default:
-			return fmt.Errorf("the OpenId token has an unexpected audience claim; value [%v] of type [%T]", audienceClaim, audienceClaim)
+			// Try to convert the single audience claim to string, similar to []any case
+			audStr := fmt.Sprintf("%v", audienceClaim)
+			if oidCfg.ClientId != audStr {
+				return fmt.Errorf("the OpenId token is not targeted for Kiali; got aud [%s]", audStr)
+			}
 		}
 	}
 
diff --git a/handlers/config.go b/handlers/config.go
index ed105cc8d..c248212d7 100644
--- a/handlers/config.go
+++ b/handlers/config.go
@@ -1,6 +1,7 @@
 package handlers
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -173,7 +174,7 @@ func getPrometheusConfig(conf *config.Config, client prometheus.ClientInterface,
 			promConfig.StorageTsdbRetention = int64(time.Duration(retention).Seconds())
 		}
 	} else {
-		configResult, err := client.GetConfiguration()
+		configResult, err := client.GetConfiguration(context.Background())
 		if checkErr(err, "Failed to fetch Prometheus configuration", logger) {
 			var config PrometheusPartialConfig
 			if checkErr(yaml.Unmarshal([]byte(configResult.YAML), &config), "Failed to unmarshal Prometheus configuration", logger) {
@@ -185,7 +186,7 @@ func getPrometheusConfig(conf *config.Config, client prometheus.ClientInterface,
 			}
 		}
 
-		promRuntimeinfoResults, err := client.GetRuntimeinfo()
+		promRuntimeinfoResults, err := client.GetRuntimeinfo(context.Background())
 		if checkErr(err, "Failed to fetch Prometheus runtime info", logger) {
 			// the storage retention as reported by Prometheus endpoint /api/v1/status/runtimeinfo
 			// It will either be time-based (e.g. "1d") or size-based (e.g "10GB") or both (e.g "1d or 10GB").
@@ -251,7 +252,7 @@ func CrippledFeatures(conf *config.Config, client prometheus.ClientInterface) ht
 			return
 		}
 
-		existingMetrics, err := client.GetExistingMetricNames(requiredMetrics)
+		existingMetrics, err := client.GetExistingMetricNames(r.Context(), requiredMetrics)
 		if !checkErr(err, "", logger) {
 			log.Error(err)
 			RespondWithJSONIndent(w, http.StatusOK, crippledFeatures)
diff --git a/handlers/dashboards.go b/handlers/dashboards.go
index 9286e9219..cc103cb96 100644
--- a/handlers/dashboards.go
+++ b/handlers/dashboards.go
@@ -150,7 +150,7 @@ func AppDashboard(
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, business.GetIstioScaler())
+		metrics, err := metricsService.GetMetrics(r.Context(), params, business.GetIstioScaler())
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -210,7 +210,7 @@ func ServiceDashboard(
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, business.GetIstioScaler())
+		metrics, err := metricsService.GetMetrics(r.Context(), params, business.GetIstioScaler())
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -247,7 +247,7 @@ func WorkloadDashboard(
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, business.GetIstioScaler())
+		metrics, err := metricsService.GetMetrics(r.Context(), params, business.GetIstioScaler())
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -290,7 +290,7 @@ func ZtunnelDashboard(
 
 		metricsService := business.NewMetricsService(prom, conf)
 
-		ztunnelMetrics, err := metricsService.GetZtunnelMetrics(params)
+		ztunnelMetrics, err := metricsService.GetZtunnelMetrics(r.Context(), params)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
diff --git a/handlers/metrics.go b/handlers/metrics.go
index a9e7027e9..3e97c30fd 100644
--- a/handlers/metrics.go
+++ b/handlers/metrics.go
@@ -48,7 +48,7 @@ func AppMetrics(conf *config.Config, cache cache.KialiCache, discovery *istio.Di
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, nil)
+		metrics, err := metricsService.GetMetrics(r.Context(), params, nil)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -78,7 +78,7 @@ func WorkloadMetrics(conf *config.Config, cache cache.KialiCache, discovery *ist
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, nil)
+		metrics, err := metricsService.GetMetrics(r.Context(), params, nil)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -108,7 +108,7 @@ func ServiceMetrics(conf *config.Config, cache cache.KialiCache, discovery *isti
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, nil)
+		metrics, err := metricsService.GetMetrics(r.Context(), params, nil)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -168,7 +168,7 @@ func AggregateMetrics(conf *config.Config, cache cache.KialiCache, discovery *is
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, nil)
+		metrics, err := metricsService.GetMetrics(r.Context(), params, nil)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -236,7 +236,7 @@ func ControlPlaneMetrics(
 
 		metrics := make(models.MetricsMap)
 		metricsService := business.NewMetricsService(prom, conf)
-		controlPlaneMetrics, err := metricsService.GetControlPlaneMetrics(params, cpWorkload.Pods, nil)
+		controlPlaneMetrics, err := metricsService.GetControlPlaneMetrics(r.Context(), params, cpWorkload.Pods, nil)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -275,7 +275,7 @@ func ResourceUsageMetrics(conf *config.Config, cache cache.KialiCache, discovery
 		metricsService := business.NewMetricsService(prom, conf)
 		metrics := make(models.MetricsMap)
 
-		resourceMetrics, err := metricsService.GetResourceMetrics(params)
+		resourceMetrics, err := metricsService.GetResourceMetrics(r.Context(), params)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -311,7 +311,7 @@ func NamespaceMetrics(conf *config.Config, cache cache.KialiCache, discovery *is
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		metrics, err := metricsService.GetMetrics(params, nil)
+		metrics, err := metricsService.GetMetrics(r.Context(), params, nil)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -369,7 +369,7 @@ func ClustersMetrics(conf *config.Config, cache cache.KialiCache, discovery *ist
 				return
 			}
 
-			metrics, err := metricsService.GetMetrics(params, nil)
+			metrics, err := metricsService.GetMetrics(r.Context(), params, nil)
 			if err != nil {
 				RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 				return
@@ -537,7 +537,7 @@ func MetricsStats(conf *config.Config, cache cache.KialiCache, discovery *istio.
 		}
 
 		metricsService := business.NewMetricsService(prom, conf)
-		stats, err := metricsService.GetStats(queries)
+		stats, err := metricsService.GetStats(r.Context(), queries)
 		if err != nil {
 			handleErrorResponse(w, err)
 			return
diff --git a/handlers/tracing.go b/handlers/tracing.go
index 0844360bb..a207609a6 100644
--- a/handlers/tracing.go
+++ b/handlers/tracing.go
@@ -32,6 +32,7 @@ func GetTracingInfo(conf *config.Config) http.HandlerFunc {
 			info = models.TracingInfo{
 				Enabled:              true,
 				Integration:          tracingConfig.InternalURL != "",
+				InternalURL:          tracingConfig.InternalURL, // This is needed for OSSMC distributed tracing redirection
 				Provider:             string(tracingConfig.Provider),
 				TempoConfig:          tracingConfig.TempoConfig,
 				URL:                  tracingConfig.ExternalURL,
diff --git a/istio/discovery.go b/istio/discovery.go
index 76a5b390e..27727af15 100644
--- a/istio/discovery.go
+++ b/istio/discovery.go
@@ -345,6 +345,9 @@ func (in *Discovery) HasControlPlane(ctx context.Context, cluster string, ns str
 
 // GetRootNamespace returns the Istio root namespace for the control plane managing the given namespace
 func (in *Discovery) GetRootNamespace(ctx context.Context, cluster, namespace string) string {
+	defer in.meshMutex.Unlock()
+	in.meshMutex.Lock()
+
 	cp := in.namespaceMap[in.namespaceMapKey(cluster, namespace)]
 	if cp != nil {
 		return cp.RootNamespace
diff --git a/make/Makefile.molecule.mk b/make/Makefile.molecule.mk
index d4a3619da..61def9678 100644
--- a/make/Makefile.molecule.mk
+++ b/make/Makefile.molecule.mk
@@ -42,7 +42,7 @@ MOLECULE_IMAGE_ENV_ARGS = --env MOLECULE_KIALI_OPERATOR_IMAGE_NAME=dev --env MOL
 else ifeq ($(CLUSTER_TYPE),minikube)
 MOLECULE_IMAGE_ENV_ARGS = --env MOLECULE_KIALI_OPERATOR_IMAGE_NAME=localhost:5000/kiali/kiali-operator --env MOLECULE_KIALI_OPERATOR_IMAGE_VERSION=dev --env MOLECULE_KIALI_IMAGE_NAME=localhost:5000/kiali/kiali --env MOLECULE_KIALI_IMAGE_VERSION=dev
 else ifeq ($(CLUSTER_TYPE),kind)
-MOLECULE_IMAGE_ENV_ARGS = --env MOLECULE_KIALI_OPERATOR_IMAGE_NAME=localhost/kiali/kiali-operator --env MOLECULE_KIALI_OPERATOR_IMAGE_VERSION=dev --env MOLECULE_KIALI_IMAGE_NAME=localhost/kiali/kiali --env MOLECULE_KIALI_IMAGE_VERSION=dev
+MOLECULE_IMAGE_ENV_ARGS = --env MOLECULE_KIALI_OPERATOR_IMAGE_NAME=localhost:5000/kiali/kiali-operator --env MOLECULE_KIALI_OPERATOR_IMAGE_VERSION=dev --env MOLECULE_KIALI_IMAGE_NAME=localhost:5000/kiali/kiali --env MOLECULE_KIALI_IMAGE_VERSION=dev
 MOLECULE_IMAGE_PULL_POLICY="IfNotPresent"
 endif
 else ifeq ($(MOLECULE_USE_DEFAULT_IMAGES),true)
diff --git a/make/Makefile.operator.mk b/make/Makefile.operator.mk
index 4bdd3cdf1..a5b32b461 100644
--- a/make/Makefile.operator.mk
+++ b/make/Makefile.operator.mk
@@ -144,7 +144,7 @@ kiali-purge: .ensure-oc-exists
 	    ${OC} patch  kiali $${cr_name} -n $${cr_namespace} -p '{"metadata":{"finalizers": []}}' --type=merge ;\
 	    ${OC} delete kiali $${cr_name} -n $${cr_namespace} ;\
 	  done
-	${OC} delete --ignore-not-found=true all,secrets,sa,configmaps,deployments,roles,rolebindings,ingresses,horizontalpodautoscalers --selector="app.kubernetes.io/name=kiali" -n "${NAMESPACE}"
+	${OC} delete --ignore-not-found=true all,secrets,sa,configmaps,deployments,roles,rolebindings,ingresses,horizontalpodautoscalers,networkpolicies --selector="app.kubernetes.io/name=kiali" -n "${NAMESPACE}"
 	${OC} delete --ignore-not-found=true clusterroles,clusterrolebindings --selector="app.kubernetes.io/name=kiali"
 	${OC} delete --ignore-not-found=true networkpolicies.networking.k8s.io -n ${NAMESPACE} kiali-network-policy-from-make
 ifeq ($(CLUSTER_TYPE),openshift)
diff --git a/observability/tracing.go b/observability/tracing.go
index 4f74f655d..438d10372 100644
--- a/observability/tracing.go
+++ b/observability/tracing.go
@@ -24,6 +24,7 @@ import (
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/util"
+	utilcontext "github.com/kiali/kiali/util/context"
 )
 
 const (
@@ -121,9 +122,15 @@ func Attribute(key string, val interface{}) attribute.KeyValue {
 // a new context with the span added and a func to be called when the span ends.
 // If tracing is not enabled, this function does nothing. The return func is
 // safe to call even when tracing is not enabled.
+// Automatically includes X-Request-Id as a span attribute if available in context.
 func StartSpan(ctx context.Context, funcName string, attrs ...attribute.KeyValue) (context.Context, EndFunc) {
 	var span trace.Span
 	if config.Get().Server.Observability.Tracing.Enabled {
+		// Add X-Request-Id as span attribute if available in context
+		if headers := utilcontext.GetRequestHeadersContext(ctx); headers != nil && headers.IsValid() {
+			attrs = append(attrs, Attribute("request_id", headers.XRequestID))
+		}
+
 		ctx, span = otel.Tracer(TracerName()).Start(ctx, funcName,
 			trace.WithAttributes(attrs...),
 		)
diff --git a/prometheus/client.go b/prometheus/client.go
index 652267ca9..6746242ba 100644
--- a/prometheus/client.go
+++ b/prometheus/client.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
+	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/util/httputil"
 )
 
@@ -183,74 +184,74 @@ func NewOfflineClient(dataDir string, buildInfo *config.OfflineManifest) *Offlin
 }
 
 // GetAllRequestRates implements ClientInterface
-func (oc *OfflineClient) GetAllRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (oc *OfflineClient) GetAllRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
 	return model.Vector{}, nil
 }
 
 // GetNamespaceServicesRequestRates implements ClientInterface
-func (oc *OfflineClient) GetNamespaceServicesRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (oc *OfflineClient) GetNamespaceServicesRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
 	return model.Vector{}, nil
 }
 
 // GetServiceRequestRates implements ClientInterface
-func (oc *OfflineClient) GetServiceRequestRates(namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (oc *OfflineClient) GetServiceRequestRates(ctx context.Context, namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
 	return model.Vector{}, nil
 }
 
 // GetAppRequestRates implements ClientInterface
-func (oc *OfflineClient) GetAppRequestRates(namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+func (oc *OfflineClient) GetAppRequestRates(ctx context.Context, namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
 	return model.Vector{}, model.Vector{}, nil
 }
 
 // GetWorkloadRequestRates implements ClientInterface
-func (oc *OfflineClient) GetWorkloadRequestRates(namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+func (oc *OfflineClient) GetWorkloadRequestRates(ctx context.Context, namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
 	return model.Vector{}, model.Vector{}, nil
 }
 
 // FetchDelta implements ClientInterface
-func (oc *OfflineClient) FetchDelta(metricName, labels, grouping string, queryTime time.Time, duration time.Duration) Metric {
+func (oc *OfflineClient) FetchDelta(ctx context.Context, metricName, labels, grouping string, queryTime time.Time, duration time.Duration) Metric {
 	// Return empty metric - this method is not recorded by ClientRecorder
 	return Metric{}
 }
 
 // FetchHistogramRange implements ClientInterface
-func (oc *OfflineClient) FetchHistogramRange(metricName, labels, grouping string, q *RangeQuery) Histogram {
+func (oc *OfflineClient) FetchHistogramRange(ctx context.Context, metricName, labels, grouping string, q *RangeQuery) Histogram {
 	// Return empty histogram - this method is not recorded by ClientRecorder
 	return Histogram{}
 }
 
 // FetchHistogramValues implements ClientInterface
-func (oc *OfflineClient) FetchHistogramValues(metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
+func (oc *OfflineClient) FetchHistogramValues(ctx context.Context, metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
 	// Return empty map - this method is not recorded by ClientRecorder
 	return map[string]model.Vector{}, nil
 }
 
 // FetchRange implements ClientInterface
-func (oc *OfflineClient) FetchRange(metricName, labels, grouping, aggregator string, q *RangeQuery) Metric {
+func (oc *OfflineClient) FetchRange(ctx context.Context, metricName, labels, grouping, aggregator string, q *RangeQuery) Metric {
 	// Return empty metric - this method is not recorded by ClientRecorder
 	return Metric{}
 }
 
 // FetchRateRange implements ClientInterface
-func (oc *OfflineClient) FetchRateRange(metricName string, labels []string, grouping string, q *RangeQuery) Metric {
+func (oc *OfflineClient) FetchRateRange(ctx context.Context, metricName string, labels []string, grouping string, q *RangeQuery) Metric {
 	// Return empty metric - this method is not recorded by ClientRecorder
 	return Metric{}
 }
 
 // GetConfiguration implements ClientInterface
-func (oc *OfflineClient) GetConfiguration() (prom_v1.ConfigResult, error) {
+func (oc *OfflineClient) GetConfiguration(ctx context.Context) (prom_v1.ConfigResult, error) {
 	// Return empty config - this method is not recorded by ClientRecorder
 	return prom_v1.ConfigResult{}, nil
 }
 
 // GetExistingMetricNames implements ClientInterface
-func (oc *OfflineClient) GetExistingMetricNames(metricNames []string) ([]string, error) {
+func (oc *OfflineClient) GetExistingMetricNames(ctx context.Context, metricNames []string) ([]string, error) {
 	// Return empty slice - this method is not recorded by ClientRecorder
 	return []string{}, nil
 }
 
 // GetMetricsForLabels implements ClientInterface
-func (oc *OfflineClient) GetMetricsForLabels(metricNames []string, labels string) ([]string, error) {
+func (oc *OfflineClient) GetMetricsForLabels(ctx context.Context, metricNames []string, labels string) ([]string, error) {
 	// Return empty slice - this method is not recorded by ClientRecorder
 	return []string{}, nil
 }
@@ -265,28 +266,28 @@ func (oc *OfflineClient) GetBuildInfo(ctx context.Context) (*prom_v1.BuildinfoRe
 }
 
 // GetRuntimeinfo implements ClientInterface
-func (oc *OfflineClient) GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error) {
+func (oc *OfflineClient) GetRuntimeinfo(ctx context.Context) (prom_v1.RuntimeinfoResult, error) {
 	return prom_v1.RuntimeinfoResult{}, nil
 }
 
 // ClientInterface for mocks (only mocked function are necessary here)
 type ClientInterface interface {
 	API() prom_v1.API
-	FetchDelta(metricName, labels, grouping string, queryTime time.Time, duration time.Duration) Metric
-	FetchHistogramRange(metricName, labels, grouping string, q *RangeQuery) Histogram
-	FetchHistogramValues(metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error)
-	FetchRange(metricName, labels, grouping, aggregator string, q *RangeQuery) Metric
-	FetchRateRange(metricName string, labels []string, grouping string, q *RangeQuery) Metric
-	GetAllRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error)
-	GetAppRequestRates(namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error)
+	FetchDelta(ctx context.Context, metricName, labels, grouping string, queryTime time.Time, duration time.Duration) Metric
+	FetchHistogramRange(ctx context.Context, metricName, labels, grouping string, q *RangeQuery) Histogram
+	FetchHistogramValues(ctx context.Context, metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error)
+	FetchRange(ctx context.Context, metricName, labels, grouping, aggregator string, q *RangeQuery) Metric
+	FetchRateRange(ctx context.Context, metricName string, labels []string, grouping string, q *RangeQuery) Metric
+	GetAllRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error)
+	GetAppRequestRates(ctx context.Context, namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error)
 	GetBuildInfo(ctx context.Context) (*prom_v1.BuildinfoResult, error)
-	GetConfiguration() (prom_v1.ConfigResult, error)
-	GetExistingMetricNames(metricNames []string) ([]string, error)
-	GetMetricsForLabels(metricNames []string, labels string) ([]string, error)
-	GetNamespaceServicesRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error)
-	GetServiceRequestRates(namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error)
-	GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error)
-	GetWorkloadRequestRates(namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error)
+	GetConfiguration(ctx context.Context) (prom_v1.ConfigResult, error)
+	GetExistingMetricNames(ctx context.Context, metricNames []string) ([]string, error)
+	GetMetricsForLabels(ctx context.Context, metricNames []string, labels string) ([]string, error)
+	GetNamespaceServicesRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error)
+	GetServiceRequestRates(ctx context.Context, namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error)
+	GetRuntimeinfo(ctx context.Context) (prom_v1.RuntimeinfoResult, error)
+	GetWorkloadRequestRates(ctx context.Context, namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error)
 }
 
 // Client for Prometheus API.
@@ -295,7 +296,12 @@ type Client struct {
 	conf *config.Config
 	p8s  api.Client
 	api  prom_v1.API
-	ctx  context.Context
+	// ctx is a fallback context containing a prometheus-specific logger for operations
+	// outside of HTTP request contexts. When client methods are called with a valid
+	// request context (containing X-Request-Id, user info, etc.), that context takes
+	// precedence. This context serves as the default logging context for background
+	// operations, ensuring consistent prometheus component identification in logs.
+	ctx context.Context
 }
 
 var (
@@ -354,7 +360,9 @@ func NewClient(conf config.Config, kialiSAToken string) (*Client, error) {
 	if err != nil {
 		return nil, err
 	}
-	clientConfig.RoundTripper = transportConfig
+
+	// Add context headers RoundTripper to the chain for X-Request-Id propagation
+	clientConfig.RoundTripper = newContextHeadersRoundTripper(transportConfig)
 
 	p8s, err := api.NewClient(clientConfig)
 	if err != nil {
@@ -382,12 +390,21 @@ func (in *Client) Inject(api prom_v1.API) {
 // be inflated due to duplication, and therefore should be used mainly for calculating ratios
 // (e.g total rates / error rates).
 // Returns (rates, error)
-func (in *Client) GetAllRequestRates(namespace, cluster string, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (in *Client) GetAllRequestRates(ctx context.Context, namespace, cluster string, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetAllRequestRates",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("namespace", namespace),
+		observability.Attribute("cluster", cluster),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
+
 	if in.conf.RunMode == config.RunModeOffline {
 		return model.Vector{}, nil
 	}
 
-	log.FromContext(in.ctx).Trace().Msgf("GetAllRequestRates [namespace: %s] [ratesInterval: %s] [queryTime: %s]", namespace, ratesInterval, queryTime.String())
+	log.FromContext(ctx).Trace().Msgf("GetAllRequestRates [namespace: %s] [ratesInterval: %s] [queryTime: %s]", namespace, ratesInterval, queryTime.String())
 
 	var result model.Vector
 	var err error
@@ -399,7 +416,7 @@ func (in *Client) GetAllRequestRates(namespace, cluster string, ratesInterval st
 	}
 
 	if result == nil {
-		result, err = getAllRequestRates(in.ctx, in.api, namespace, cluster, queryTime, ratesInterval)
+		result, err = getAllRequestRates(ctx, in.api, namespace, cluster, queryTime, ratesInterval)
 		if err == nil && promCache != nil {
 			promCache.SetAllRequestRates(namespace, cluster, ratesInterval, queryTime, result)
 		}
@@ -413,19 +430,28 @@ func (in *Client) GetAllRequestRates(namespace, cluster string, ratesInterval st
 // be inflated due to duplication, and therefore should be used mainly for calculating ratios
 // (e.g total rates / error rates).
 // Returns (rates, error)
-func (in *Client) GetNamespaceServicesRequestRates(namespace, cluster string, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (in *Client) GetNamespaceServicesRequestRates(ctx context.Context, namespace, cluster string, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetNamespaceServicesRequestRates",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("namespace", namespace),
+		observability.Attribute("cluster", cluster),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
+
 	if in.conf.RunMode == config.RunModeOffline {
 		return model.Vector{}, nil
 	}
 
-	log.FromContext(in.ctx).Trace().Msgf("GetNamespaceServicesRequestRates [namespace: %s] [ratesInterval: %s] [queryTime: %s]", namespace, ratesInterval, queryTime.String())
+	log.FromContext(ctx).Trace().Msgf("GetNamespaceServicesRequestRates [namespace: %s] [ratesInterval: %s] [queryTime: %s]", namespace, ratesInterval, queryTime.String())
 
 	if promCache != nil {
 		if isCached, result := promCache.GetNamespaceServicesRequestRates(namespace, cluster, ratesInterval, queryTime); isCached {
 			return result, nil
 		}
 	}
-	result, err := getNamespaceServicesRequestRates(in.ctx, in.api, namespace, cluster, queryTime, ratesInterval)
+	result, err := getNamespaceServicesRequestRates(ctx, in.api, namespace, cluster, queryTime, ratesInterval)
 	if err != nil {
 		return result, err
 	}
@@ -440,19 +466,29 @@ func (in *Client) GetNamespaceServicesRequestRates(namespace, cluster string, ra
 // be inflated due to duplication, and therefore should be used mainly for calculating ratios
 // (e.g total rates / error rates).
 // Returns (in, error)
-func (in *Client) GetServiceRequestRates(namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (in *Client) GetServiceRequestRates(ctx context.Context, namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetServiceRequestRates",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("namespace", namespace),
+		observability.Attribute("cluster", cluster),
+		observability.Attribute("service", service),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
+
 	if in.conf.RunMode == config.RunModeOffline {
 		return model.Vector{}, nil
 	}
 
-	log.FromContext(in.ctx).Trace().Msgf("GetServiceRequestRates [namespace: %s] [service: %s] [ratesInterval: %s] [queryTime: %s]", namespace, service, ratesInterval, queryTime.String())
+	log.FromContext(ctx).Trace().Msgf("GetServiceRequestRates [namespace: %s] [service: %s] [ratesInterval: %s] [queryTime: %s]", namespace, service, ratesInterval, queryTime.String())
 
 	if promCache != nil {
 		if isCached, result := promCache.GetServiceRequestRates(namespace, cluster, service, ratesInterval, queryTime); isCached {
 			return result, nil
 		}
 	}
-	result, err := getServiceRequestRates(in.ctx, in.api, namespace, cluster, service, queryTime, ratesInterval)
+	result, err := getServiceRequestRates(ctx, in.api, namespace, cluster, service, queryTime, ratesInterval)
 	if err != nil {
 		return result, err
 	}
@@ -467,19 +503,29 @@ func (in *Client) GetServiceRequestRates(namespace, cluster, service, ratesInter
 // be inflated due to duplication, and therefore should be used mainly for calculating ratios
 // (e.g total rates / error rates).
 // Returns (in, out, error)
-func (in *Client) GetAppRequestRates(namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+func (in *Client) GetAppRequestRates(ctx context.Context, namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetAppRequestRates",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("namespace", namespace),
+		observability.Attribute("cluster", cluster),
+		observability.Attribute("app", app),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
+
 	if in.conf.RunMode == config.RunModeOffline {
 		return model.Vector{}, model.Vector{}, nil
 	}
 
-	log.FromContext(in.ctx).Trace().Msgf("GetAppRequestRates [namespace: %s] [cluster: %s] [app: %s] [ratesInterval: %s] [queryTime: %s]", namespace, cluster, app, ratesInterval, queryTime.String())
+	log.FromContext(ctx).Trace().Msgf("GetAppRequestRates [namespace: %s] [cluster: %s] [app: %s] [ratesInterval: %s] [queryTime: %s]", namespace, cluster, app, ratesInterval, queryTime.String())
 
 	if promCache != nil {
 		if isCached, inResult, outResult := promCache.GetAppRequestRates(namespace, cluster, app, ratesInterval, queryTime); isCached {
 			return inResult, outResult, nil
 		}
 	}
-	inResult, outResult, err := getItemRequestRates(in.ctx, in.api, namespace, cluster, app, "app", queryTime, ratesInterval)
+	inResult, outResult, err := getItemRequestRates(ctx, in.api, namespace, cluster, app, "app", queryTime, ratesInterval)
 	if err != nil {
 		return inResult, outResult, err
 	}
@@ -494,19 +540,29 @@ func (in *Client) GetAppRequestRates(namespace, cluster, app, ratesInterval stri
 // be inflated due to duplication, and therefore should be used mainly for calculating ratios
 // (e.g total rates / error rates).
 // Returns (in, out, error)
-func (in *Client) GetWorkloadRequestRates(namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+func (in *Client) GetWorkloadRequestRates(ctx context.Context, namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetWorkloadRequestRates",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("namespace", namespace),
+		observability.Attribute("cluster", cluster),
+		observability.Attribute("workload", workload),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
+
 	if in.conf.RunMode == config.RunModeOffline {
 		return model.Vector{}, model.Vector{}, nil
 	}
 
-	log.FromContext(in.ctx).Trace().Msgf("GetWorkloadRequestRates [namespace: %s] [workload: %s] [ratesInterval: %s] [queryTime: %s]", namespace, workload, ratesInterval, queryTime.String())
+	log.FromContext(ctx).Trace().Msgf("GetWorkloadRequestRates [namespace: %s] [workload: %s] [ratesInterval: %s] [queryTime: %s]", namespace, workload, ratesInterval, queryTime.String())
 
 	if promCache != nil {
 		if isCached, inResult, outResult := promCache.GetWorkloadRequestRates(namespace, cluster, workload, ratesInterval, queryTime); isCached {
 			return inResult, outResult, nil
 		}
 	}
-	inResult, outResult, err := getItemRequestRates(in.ctx, in.api, namespace, cluster, workload, "workload", queryTime, ratesInterval)
+	inResult, outResult, err := getItemRequestRates(ctx, in.api, namespace, cluster, workload, "workload", queryTime, ratesInterval)
 	if err != nil {
 		return inResult, outResult, err
 	}
@@ -517,36 +573,83 @@ func (in *Client) GetWorkloadRequestRates(namespace, cluster, workload, ratesInt
 }
 
 // FetchDelta fetches a delta for a simple metric (gauge or counter), for a given duration
-func (in *Client) FetchDelta(metricName, labels, grouping string, queryTime time.Time, duration time.Duration) Metric {
+func (in *Client) FetchDelta(ctx context.Context, metricName, labels, grouping string, queryTime time.Time, duration time.Duration) Metric {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "FetchDelta",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("metricName", metricName),
+		observability.Attribute("labels", labels),
+		observability.Attribute("grouping", grouping),
+	)
+	defer end()
+
 	query := fmt.Sprintf("delta(%s%s[%s])", metricName, labels, duration.Round(time.Second).String())
 	if grouping != "" {
 		query += fmt.Sprintf(" by (%s)", grouping)
 	}
-	return fetchQuery(in.ctx, in.api, query, queryTime)
+	return fetchQuery(ctx, in.api, query, queryTime)
 }
 
 // FetchRange fetches a simple metric (gauge or counter) in given range
-func (in *Client) FetchRange(metricName, labels, grouping, aggregator string, q *RangeQuery) Metric {
+func (in *Client) FetchRange(ctx context.Context, metricName, labels, grouping, aggregator string, q *RangeQuery) Metric {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "FetchRange",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("metricName", metricName),
+		observability.Attribute("labels", labels),
+		observability.Attribute("grouping", grouping),
+		observability.Attribute("aggregator", aggregator),
+	)
+	defer end()
+
 	query := fmt.Sprintf("%s(%s%s)", aggregator, metricName, labels)
 	if grouping != "" {
 		query += fmt.Sprintf(" by (%s)", grouping)
 	}
-	return fetchRange(in.ctx, in.api, query, q.Range)
+	return fetchRange(ctx, in.api, query, q.Range)
 }
 
 // FetchRateRange fetches a counter's rate in given range
-func (in *Client) FetchRateRange(metricName string, labels []string, grouping string, q *RangeQuery) Metric {
-	return fetchRateRange(in.ctx, in.api, metricName, labels, grouping, q)
+func (in *Client) FetchRateRange(ctx context.Context, metricName string, labels []string, grouping string, q *RangeQuery) Metric {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "FetchRateRange",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("metricName", metricName),
+		observability.Attribute("grouping", grouping),
+	)
+	defer end()
+
+	return fetchRateRange(ctx, in.api, metricName, labels, grouping, q)
 }
 
 // FetchHistogramRange fetches bucketed metric as histogram in given range
-func (in *Client) FetchHistogramRange(metricName, labels, grouping string, q *RangeQuery) Histogram {
-	return fetchHistogramRange(in.ctx, in.api, metricName, labels, grouping, q)
+func (in *Client) FetchHistogramRange(ctx context.Context, metricName, labels, grouping string, q *RangeQuery) Histogram {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "FetchHistogramRange",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("metricName", metricName),
+		observability.Attribute("labels", labels),
+		observability.Attribute("grouping", grouping),
+	)
+	defer end()
+
+	return fetchHistogramRange(ctx, in.api, metricName, labels, grouping, q)
 }
 
 // FetchHistogramValues fetches bucketed metric as histogram at a given specific time
-func (in *Client) FetchHistogramValues(metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
-	return fetchHistogramValues(in.ctx, in.api, metricName, labels, grouping, rateInterval, avg, quantiles, queryTime)
+func (in *Client) FetchHistogramValues(ctx context.Context, metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "FetchHistogramValues",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("metricName", metricName),
+		observability.Attribute("labels", labels),
+		observability.Attribute("grouping", grouping),
+		observability.Attribute("rateInterval", rateInterval),
+		observability.Attribute("avg", avg),
+	)
+	defer end()
+
+	return fetchHistogramValues(ctx, in.api, metricName, labels, grouping, rateInterval, avg, quantiles, queryTime)
 }
 
 // API returns the Prometheus V1 HTTP API for performing calls not supported natively by this client
@@ -555,6 +658,12 @@ func (in *Client) API() prom_v1.API {
 }
 
 func (in *Client) GetBuildInfo(ctx context.Context) (*prom_v1.BuildinfoResult, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(ctx, "GetBuildInfo",
+		observability.Attribute("package", "prometheus"),
+	)
+	defer end()
+
 	info, err := in.api.Buildinfo(ctx)
 	if err != nil {
 		return nil, err
@@ -562,16 +671,28 @@ func (in *Client) GetBuildInfo(ctx context.Context) (*prom_v1.BuildinfoResult, e
 	return &info, nil
 }
 
-func (in *Client) GetConfiguration() (prom_v1.ConfigResult, error) {
-	config, err := in.API().Config(in.ctx)
+func (in *Client) GetConfiguration(ctx context.Context) (prom_v1.ConfigResult, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetConfiguration",
+		observability.Attribute("package", "prometheus"),
+	)
+	defer end()
+
+	config, err := in.API().Config(ctx)
 	if err != nil {
 		return prom_v1.ConfigResult{}, err
 	}
 	return config, nil
 }
 
-func (in *Client) GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error) {
-	ri, err := in.API().Runtimeinfo(in.ctx)
+func (in *Client) GetRuntimeinfo(ctx context.Context) (prom_v1.RuntimeinfoResult, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetRuntimeinfo",
+		observability.Attribute("package", "prometheus"),
+	)
+	defer end()
+
+	ri, err := in.API().Runtimeinfo(ctx)
 	if err != nil {
 		return prom_v1.RuntimeinfoResult{}, err
 	}
@@ -580,17 +701,25 @@ func (in *Client) GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error) {
 
 // GetMetricsForLabels returns a list of metrics existing for the provided labels set. Only metrics that match a name in the given
 // list of metricNames will be returned - others will be ignored.
-func (in *Client) GetMetricsForLabels(metricNames []string, labelQueryString string) ([]string, error) {
+func (in *Client) GetMetricsForLabels(ctx context.Context, metricNames []string, labelQueryString string) ([]string, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetMetricsForLabels",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("labelQueryString", labelQueryString),
+		observability.Attribute("metricNamesCount", len(metricNames)),
+	)
+	defer end()
+
 	if len(metricNames) == 0 {
 		return []string{}, nil
 	}
 
-	zl := log.FromContext(in.ctx)
+	zl := log.FromContext(ctx)
 
 	zl.Trace().Msgf("GetMetricsForLabels: labels=[%v] metricNames=[%v]", labelQueryString, metricNames)
 	startT := time.Now()
 	queryString := fmt.Sprintf("count(%v) by (__name__)", labelQueryString)
-	results, warnings, err := in.api.Query(in.ctx, queryString, time.Now())
+	results, warnings, err := in.api.Query(ctx, queryString, time.Now())
 	if len(warnings) > 0 {
 		zl.Warn().Msgf("GetMetricsForLabels. Prometheus Warnings: [%s]", strings.Join(warnings, ","))
 	}
@@ -616,16 +745,23 @@ func (in *Client) GetMetricsForLabels(metricNames []string, labelQueryString str
 }
 
 // GetExistingMetricNames returns a list of the requested metric names that exist in Prometheus (meaning there is a matching __name__ label).
-func (in *Client) GetExistingMetricNames(metricNames []string) ([]string, error) {
+func (in *Client) GetExistingMetricNames(ctx context.Context, metricNames []string) ([]string, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(in.contextOrDefault(ctx), "GetExistingMetricNames",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("metricNamesCount", len(metricNames)),
+	)
+	defer end()
+
 	if len(metricNames) == 0 {
 		return []string{}, nil
 	}
 
-	zl := log.FromContext(in.ctx)
+	zl := log.FromContext(ctx)
 
 	zl.Trace().Msgf("GetExistingMetricNames: metricNames=[%v]", metricNames)
 	startT := time.Now()
-	results, warnings, err := in.api.LabelValues(in.ctx, "__name__", []string{}, time.Unix(0, 0), time.Now())
+	results, warnings, err := in.api.LabelValues(ctx, "__name__", []string{}, time.Unix(0, 0), time.Now())
 	if len(warnings) > 0 {
 		zl.Warn().Msgf("GetExistingMetricNames. Prometheus Warnings: [%s]", strings.Join(warnings, ","))
 	}
@@ -655,3 +791,13 @@ func (in *Client) GetExistingMetricNames(metricNames []string) ([]string, error)
 func SanitizeLabelName(name string) string {
 	return invalidLabelCharRE.ReplaceAllString(name, "_")
 }
+
+// contextOrDefault returns the provided context if it's not nil or background,
+// otherwise returns the client's default context. This ensures proper context
+// propagation for HTTP requests while maintaining backwards compatibility.
+func (in *Client) contextOrDefault(ctx context.Context) context.Context {
+	if ctx == nil || ctx == context.Background() {
+		return in.ctx
+	}
+	return ctx
+}
diff --git a/prometheus/context_headers_roundtripper.go b/prometheus/context_headers_roundtripper.go
new file mode 100644
index 000000000..f6bc7cdbf
--- /dev/null
+++ b/prometheus/context_headers_roundtripper.go
@@ -0,0 +1,40 @@
+package prometheus
+
+import (
+	"net/http"
+
+	"github.com/kiali/kiali/log"
+	utilcontext "github.com/kiali/kiali/util/context"
+)
+
+// contextHeadersRoundTripper injects headers from request context into outgoing HTTP requests
+type contextHeadersRoundTripper struct {
+	originalRT http.RoundTripper
+}
+
+// RoundTrip implements http.RoundTripper interface
+// It extracts headers from the request context and injects them into the outgoing HTTP request
+func (rt *contextHeadersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	// Safely extract headers from request context
+	if headers := utilcontext.GetRequestHeadersContext(req.Context()); headers != nil {
+		if headers.IsValid() {
+			// Add X-Request-Id header to outgoing request
+			req.Header.Set("X-Request-Id", headers.XRequestID)
+
+			// Optional trace logging for debugging
+			if log.IsTrace() {
+				log.Tracef("Propagated X-Request-Id to Prometheus: %s", headers.XRequestID)
+			}
+		}
+	}
+
+	// Continue with the original RoundTripper
+	return rt.originalRT.RoundTrip(req)
+}
+
+// newContextHeadersRoundTripper creates a new context-aware RoundTripper
+func newContextHeadersRoundTripper(rt http.RoundTripper) http.RoundTripper {
+	return &contextHeadersRoundTripper{
+		originalRT: rt,
+	}
+}
diff --git a/prometheus/metrics.go b/prometheus/metrics.go
index fd929ddc9..80f258f0e 100644
--- a/prometheus/metrics.go
+++ b/prometheus/metrics.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
+	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/prometheus/internalmetrics"
 	"github.com/kiali/kiali/util/sliceutil"
 )
@@ -96,6 +97,13 @@ func buildHistogramQueries(ctx context.Context, metricName, labels, grouping, ra
 }
 
 func fetchQuery(ctx context.Context, api prom_v1.API, query string, queryTime time.Time) Metric {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(ctx, "fetchQuery",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("query", query),
+	)
+	defer end()
+
 	result, warnings, err := api.Query(ctx, query, queryTime)
 	if len(warnings) > 0 {
 		zl := log.FromContext(ctx)
@@ -129,6 +137,13 @@ func vectorToMatrix(ctx context.Context, vector []*model.Sample, t model.Time) m
 }
 
 func fetchRange(ctx context.Context, api prom_v1.API, query string, bounds prom_v1.Range) Metric {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(ctx, "fetchRange",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("query", query),
+	)
+	defer end()
+
 	result, warnings, err := api.QueryRange(ctx, query, bounds)
 	if len(warnings) > 0 {
 		zl := log.FromContext(ctx)
@@ -148,6 +163,14 @@ func fetchRange(ctx context.Context, api prom_v1.API, query string, bounds prom_
 // Note that it does not discriminate on "reporter", so rates can be inflated due to duplication, and therefore
 // should be used mainly for calculating ratios (e.g total rates / error rates)
 func getAllRequestRates(ctx context.Context, api prom_v1.API, namespace, cluster string, queryTime time.Time, ratesInterval string) (model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(ctx, "getAllRequestRates",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("namespace", namespace),
+		observability.Attribute("cluster", cluster),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
 	// traffic originating outside the namespace to destinations inside the namespace
 	lbl := fmt.Sprintf(`destination_service_namespace="%s",source_workload_namespace!="%s",destination_cluster="%s"`, namespace, namespace, cluster)
 	fromOutside, err := getRequestRatesForLabel(ctx, api, queryTime, lbl, ratesInterval)
@@ -215,6 +238,14 @@ func getItemRequestRates(ctx context.Context, api prom_v1.API, namespace, cluste
 // TODO: Disable health rate calculations in offline mode.
 // It would be better if we could align the graph rate calculations with the health rate calculations.
 func getRequestRatesForLabel(ctx context.Context, api prom_v1.API, time time.Time, labels, ratesInterval string) (model.Vector, error) {
+	var end observability.EndFunc
+	ctx, end = observability.StartSpan(ctx, "getRequestRatesForLabel",
+		observability.Attribute("package", "prometheus"),
+		observability.Attribute("labels", labels),
+		observability.Attribute("ratesInterval", ratesInterval),
+	)
+	defer end()
+
 	zl := log.FromContext(ctx)
 	query := fmt.Sprintf("rate(istio_requests_total{%s}[%s]) > 0", labels, ratesInterval)
 	zl.Trace().Msgf("getRequestRatesForLabel: %s", query)
diff --git a/routing/router.go b/routing/router.go
index 7f51ce3f5..da064eafd 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -29,6 +29,7 @@ import (
 	kialiprometheus "github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/prometheus/internalmetrics"
 	"github.com/kiali/kiali/tracing"
+	utilcontext "github.com/kiali/kiali/util/context"
 )
 
 // NewRouter creates the router with all API routes and the static files handler
@@ -414,5 +415,30 @@ func buildHttpHandlerLogger(route Route, handlerFunction http.Handler) http.Hand
 		})
 	})
 
+	// Store X-Request-Id in context for propagation to external services
+	// We need this because hlog handlers serve different purposes:
+	// - CustomHeaderHandler: Preserves existing X-Request-Id for logging but doesn't provide programmatic access
+	// - RequestIDHandler: Generates new IDs for logging and provides access via hlog.IDFromRequest()
+	// For external service propagation, we need the actual ID value from either source
+	c = c.append(func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			var xRequestId string
+
+			// First try HTTP header (preserved by CustomHeaderHandler)
+			if xRequestId = r.Header.Get(xRequestIdHeader); xRequestId == "" {
+				// Fall back to hlog-generated ID (from RequestIDHandler)
+				if id, ok := hlog.IDFromRequest(r); ok {
+					xRequestId = id.String()
+				}
+			}
+
+			if xRequestId != "" {
+				headers := &utilcontext.RequestHeaders{XRequestID: xRequestId}
+				r = r.WithContext(utilcontext.SetRequestHeadersContext(r.Context(), headers))
+			}
+			next.ServeHTTP(w, r)
+		})
+	})
+
 	return c.then(handlerFunction)
 }
diff --git a/util/context/request_headers.go b/util/context/request_headers.go
new file mode 100644
index 000000000..8b5fb8c68
--- /dev/null
+++ b/util/context/request_headers.go
@@ -0,0 +1,32 @@
+package context
+
+import (
+	"context"
+)
+
+type contextKey string
+
+var ContextKeyRequestHeaders contextKey = "requestHeaders"
+
+// RequestHeaders holds request-scoped header information for propagation to external services
+type RequestHeaders struct {
+	XRequestID string
+}
+
+// IsValid performs basic validation on request headers
+func (h *RequestHeaders) IsValid() bool {
+	return h.XRequestID != "" && len(h.XRequestID) <= 1024
+}
+
+// SetRequestHeadersContext stores request headers in context for propagation
+func SetRequestHeadersContext(ctx context.Context, headers *RequestHeaders) context.Context {
+	return context.WithValue(ctx, ContextKeyRequestHeaders, headers)
+}
+
+// GetRequestHeadersContext retrieves request headers from context
+func GetRequestHeadersContext(ctx context.Context) *RequestHeaders {
+	if headers, ok := ctx.Value(ContextKeyRequestHeaders).(*RequestHeaders); ok {
+		return headers
+	}
+	return nil
+}



Test patch:
diff --git a/business/dashboards_test.go b/business/dashboards_test.go
index ed35b4643..ed9467a70 100644
--- a/business/dashboards_test.go
+++ b/business/dashboards_test.go
@@ -50,8 +50,8 @@ func TestGetDashboard(t *testing.T) {
 		},
 	}
 	query.FillDefaults()
-	prom.MockMetric("my_metric_1_1", expectedLabels, &query.RangeQuery, 10)
-	prom.MockHistogram("my_metric_1_2", expectedLabels, &query.RangeQuery, 11, 12)
+	prom.MockMetric(context.Background(), "my_metric_1_1", expectedLabels, &query.RangeQuery, 10)
+	prom.MockHistogram(context.Background(), "my_metric_1_2", expectedLabels, &query.RangeQuery, 11, 12)
 
 	dashboard, err := service.GetDashboard(context.Background(), query, "dashboard1")
 
@@ -91,8 +91,8 @@ func TestGetDashboardFromKialiNamespace(t *testing.T) {
 		},
 	}
 	query.FillDefaults()
-	prom.MockMetric("my_metric_1_1", expectedLabels, &query.RangeQuery, 10)
-	prom.MockHistogram("my_metric_1_2", expectedLabels, &query.RangeQuery, 11, 12)
+	prom.MockMetric(context.Background(), "my_metric_1_1", expectedLabels, &query.RangeQuery, 10)
+	prom.MockHistogram(context.Background(), "my_metric_1_2", expectedLabels, &query.RangeQuery, 11, 12)
 
 	dashboard, err := service.GetDashboard(context.Background(), query, "dashboard1")
 
@@ -218,10 +218,10 @@ func TestGetCustomDashboardRefs(t *testing.T) {
 	// Setup mocks
 	service, prom := setupService(conf, "my-namespace", []dashboards.MonitoringDashboard{*fakeDashboard("1"), *fakeDashboard("2")})
 
-	prom.MockMetricsForLabels([]string{"my_metric_1_1", "request_count", "tcp_received", "tcp_sent"})
+	prom.MockMetricsForLabels(context.Background(), []string{"my_metric_1_1", "request_count", "tcp_received", "tcp_sent"})
 	pods := []*models.Pod{}
 
-	runtimes := service.GetCustomDashboardRefs("my-namespace", "app", "", pods)
+	runtimes := service.GetCustomDashboardRefs(context.Background(), "my-namespace", "app", "", pods)
 
 	prom.AssertNumberOfCalls(t, "GetMetricsForLabels", 1)
 	assert.Len(runtimes, 1)
diff --git a/business/health_test.go b/business/health_test.go
index 255d27b21..6750a5fad 100644
--- a/business/health_test.go
+++ b/business/health_test.go
@@ -38,7 +38,7 @@ func TestGetServiceHealth(t *testing.T) {
 	prom := new(prometheustest.PromClientMock)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
-	prom.MockServiceRequestRates("ns", conf.KubernetesConfig.ClusterName, "httpbin", serviceRates)
+	prom.MockServiceRequestRates(context.Background(), "ns", conf.KubernetesConfig.ClusterName, "httpbin", serviceRates)
 
 	hs := NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Health
 
@@ -85,7 +85,7 @@ func TestGetAppHealth(t *testing.T) {
 	hs := NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Health
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
-	prom.MockAppRequestRates("ns", conf.KubernetesConfig.ClusterName, "reviews", otherRatesIn, otherRatesOut)
+	prom.MockAppRequestRates(context.Background(), "ns", conf.KubernetesConfig.ClusterName, "reviews", otherRatesIn, otherRatesOut)
 
 	mockWkd := models.Workload{}
 	mockWkd.Name = "reviews-v1"
@@ -135,7 +135,7 @@ func TestGetWorkloadHealth(t *testing.T) {
 	prom := new(prometheustest.PromClientMock)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
-	prom.MockWorkloadRequestRates("ns", conf.KubernetesConfig.ClusterName, "reviews-v1", otherRatesIn, otherRatesOut)
+	prom.MockWorkloadRequestRates(context.Background(), "ns", conf.KubernetesConfig.ClusterName, "reviews-v1", otherRatesIn, otherRatesOut)
 
 	hs := NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Health
 
@@ -185,7 +185,7 @@ func TestGetAppHealthWithoutIstio(t *testing.T) {
 	prom := new(prometheustest.PromClientMock)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
-	prom.MockAppRequestRates("ns", conf.KubernetesConfig.ClusterName, "reviews", otherRatesIn, otherRatesOut)
+	prom.MockAppRequestRates(context.Background(), "ns", conf.KubernetesConfig.ClusterName, "reviews", otherRatesIn, otherRatesOut)
 
 	hs := NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Health
 
@@ -216,7 +216,7 @@ func TestGetWorkloadHealthWithoutIstio(t *testing.T) {
 	prom := new(prometheustest.PromClientMock)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
-	prom.MockWorkloadRequestRates("ns", conf.KubernetesConfig.ClusterName, "reviews-v1", otherRatesIn, otherRatesOut)
+	prom.MockWorkloadRequestRates(context.Background(), "ns", conf.KubernetesConfig.ClusterName, "reviews-v1", otherRatesIn, otherRatesOut)
 
 	hs := NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Health
 
@@ -278,7 +278,7 @@ func TestGetNamespaceServiceHealthWithNA(t *testing.T) {
 	k8s.OpenShift = true
 	prom := new(prometheustest.PromClientMock)
 
-	prom.On("GetNamespaceServicesRequestRates", "tutorial", conf.KubernetesConfig.ClusterName, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
+	prom.On("GetNamespaceServicesRequestRates", mock.Anything, "tutorial", conf.KubernetesConfig.ClusterName, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
 
 	hs := NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Health
 
@@ -322,8 +322,8 @@ func TestGetNamespaceServicesHealthMultiCluster(t *testing.T) {
 		),
 	}
 	prom := new(prometheustest.PromClientMock)
-	prom.On("GetNamespaceServicesRequestRates", "tutorial", conf.KubernetesConfig.ClusterName, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
-	prom.On("GetNamespaceServicesRequestRates", "tutorial", "west", mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
+	prom.On("GetNamespaceServicesRequestRates", mock.Anything, "tutorial", conf.KubernetesConfig.ClusterName, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
+	prom.On("GetNamespaceServicesRequestRates", mock.Anything, "tutorial", "west", mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
 
 	hs := NewLayerBuilder(t, conf).WithClients(clients).WithProm(prom).Build().Health
 
@@ -355,8 +355,8 @@ func TestGetNamespaceApplicationsHealthMultiCluster(t *testing.T) {
 		),
 	}
 	prom := new(prometheustest.PromClientMock)
-	prom.On("GetAllRequestRates", "tutorial", conf.KubernetesConfig.ClusterName, "1m", mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
-	prom.On("GetAllRequestRates", "tutorial", "west", "1m", mock.AnythingOfType("time.Time")).Return(serviceRates500, nil)
+	prom.On("GetAllRequestRates", mock.Anything, "tutorial", conf.KubernetesConfig.ClusterName, "1m", mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
+	prom.On("GetAllRequestRates", mock.Anything, "tutorial", "west", "1m", mock.AnythingOfType("time.Time")).Return(serviceRates500, nil)
 
 	hs := NewLayerBuilder(t, conf).WithClients(clients).WithProm(prom).Build().Health
 
@@ -388,8 +388,8 @@ func TestGetNamespaceWorkloadsHealthMultiCluster(t *testing.T) {
 		),
 	}
 	prom := new(prometheustest.PromClientMock)
-	prom.On("GetAllRequestRates", "tutorial", conf.KubernetesConfig.ClusterName, "1m", mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
-	prom.On("GetAllRequestRates", "tutorial", "west", "1m", mock.AnythingOfType("time.Time")).Return(serviceRates500, nil)
+	prom.On("GetAllRequestRates", mock.Anything, "tutorial", conf.KubernetesConfig.ClusterName, "1m", mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
+	prom.On("GetAllRequestRates", mock.Anything, "tutorial", "west", "1m", mock.AnythingOfType("time.Time")).Return(serviceRates500, nil)
 
 	hs := NewLayerBuilder(t, conf).WithClients(clients).WithProm(prom).Build().Health
 
diff --git a/business/metrics_test.go b/business/metrics_test.go
index 292423570..3aa89ec5c 100644
--- a/business/metrics_test.go
+++ b/business/metrics_test.go
@@ -1,6 +1,7 @@
 package business
 
 import (
+	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -60,7 +61,7 @@ func TestGetServiceMetrics(t *testing.T) {
 	api.MockHistoRange("istio_response_bytes", "{"+labels+"}[5m]", 0.35, 0.2, 0.3, 0.9)
 
 	// Test that range and rate interval are changed when needed (namespace bounds)
-	metrics, err := srv.GetMetrics(q, nil)
+	metrics, err := srv.GetMetrics(context.Background(), q, nil)
 
 	assert.Nil(err)
 	assert.Equal(13, len(metrics))
@@ -150,7 +151,7 @@ func TestGetAppMetrics(t *testing.T) {
 	q.FillDefaults()
 	q.RateInterval = "5m"
 	q.Quantiles = []string{"0.5", "0.95", "0.99"}
-	metrics, err := srv.GetMetrics(q, nil)
+	metrics, err := srv.GetMetrics(context.Background(), q, nil)
 
 	assert.Nil(err)
 	assert.Equal(13, len(metrics))
@@ -209,7 +210,7 @@ func TestGetFilteredAppMetrics(t *testing.T) {
 	q.FillDefaults()
 	q.RateInterval = "5m"
 	q.Filters = []string{"request_count", "request_size"}
-	metrics, err := srv.GetMetrics(q, nil)
+	metrics, err := srv.GetMetrics(context.Background(), q, nil)
 
 	assert.Nil(err)
 	assert.Equal(2, len(metrics))
@@ -234,7 +235,7 @@ func TestGetAppMetricsInstantRates(t *testing.T) {
 	q.FillDefaults()
 	q.RateFunc = "irate"
 	q.Filters = []string{"request_count"}
-	metrics, err := srv.GetMetrics(q, nil)
+	metrics, err := srv.GetMetrics(context.Background(), q, nil)
 
 	assert.Nil(err)
 	assert.Equal(1, len(metrics))
@@ -260,7 +261,7 @@ func TestGetAppMetricsUnavailable(t *testing.T) {
 	q.RateInterval = "5m"
 	q.Quantiles = []string{"0.5", "0.95", "0.99"}
 	q.Filters = []string{"request_count", "request_size"}
-	metrics, err := srv.GetMetrics(q, nil)
+	metrics, err := srv.GetMetrics(context.Background(), q, nil)
 
 	assert.Nil(err)
 	assert.Equal(2, len(metrics))
@@ -306,7 +307,7 @@ func TestGetNamespaceMetrics(t *testing.T) {
 	q.FillDefaults()
 	q.RateInterval = "5m"
 	q.Quantiles = []string{"0.5", "0.95", "0.99"}
-	metrics, err := srv.GetMetrics(q, nil)
+	metrics, err := srv.GetMetrics(context.Background(), q, nil)
 
 	assert.Nil(err)
 	assert.Equal(13, len(metrics))
@@ -450,10 +451,10 @@ func TestGetMetricsStats(t *testing.T) {
 	q2P95 := model.Vector{createSample(9.3)}
 	q1Labels := `reporter="source",source_workload_namespace="ns1",source_canonical_service="foo"`
 	q2Labels := `reporter="destination",destination_service_name="bar",destination_service_namespace="ns2",source_workload_namespace="ns3",source_workload="w1"`
-	api.MockHistoValue("istio_request_duration_milliseconds", "{"+q1Labels+"}[30m]", q1Avg, v0, q1P95, v0)
-	api.MockHistoValue("istio_request_duration_milliseconds", "{"+q2Labels+"}[3h]", v0, q2P50, q2P95, v0)
+	api.MockHistoValue(context.Background(), "istio_request_duration_milliseconds", "{"+q1Labels+"}[30m]", q1Avg, v0, q1P95, v0)
+	api.MockHistoValue(context.Background(), "istio_request_duration_milliseconds", "{"+q2Labels+"}[3h]", v0, q2P50, q2P95, v0)
 
-	stats, err := srv.GetStats(queries)
+	stats, err := srv.GetStats(context.Background(), queries)
 
 	assert.Nil(err)
 	assert.Len(stats, 2)
diff --git a/business/workloads_test.go b/business/workloads_test.go
index 80372aafb..7adf174bb 100644
--- a/business/workloads_test.go
+++ b/business/workloads_test.go
@@ -31,7 +31,7 @@ func setupWorkloadService(t testing.TB, k8s kubernetes.UserClientInterface, conf
 	// config needs to be set by other services since those rely on the global.
 	prom := new(prometheustest.PromClientMock)
 	// Mocking out for the dashboards service. Maybe this should be set to something real?
-	prom.MockMetricsForLabels([]string{})
+	prom.MockMetricsForLabels(context.Background(), []string{})
 	return NewLayerBuilder(t, conf).WithClient(k8s).WithProm(prom).Build().Workload
 }
 
diff --git a/graph/telemetry/istio/appender/health_test.go b/graph/telemetry/istio/appender/health_test.go
index 59c97fceb..689c0a7bf 100644
--- a/graph/telemetry/istio/appender/health_test.go
+++ b/graph/telemetry/istio/appender/health_test.go
@@ -499,8 +499,8 @@ func TestErrorCausesPanic(t *testing.T) {
 	discovery := istio.NewDiscovery(map[string]kubernetes.ClientInterface{config.DefaultClusterID: k8s}, cache, conf)
 
 	prom := new(prometheustest.PromClientMock)
-	prom.MockNamespaceServicesRequestRates("testNamespace", "0s", time.Unix(0, 0), model.Vector{})
-	prom.MockAllRequestRates("testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
+	prom.MockNamespaceServicesRequestRates(context.Background(), "testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
+	prom.MockAllRequestRates(context.Background(), "testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
 	k8sclients := make(map[string]kubernetes.UserClientInterface)
 	k8sclients[conf.KubernetesConfig.ClusterName] = k8s
 	businessLayer, err := business.NewLayerWithSAClients(conf, cache, nil, nil, nil, nil, discovery, k8sclients)
@@ -550,8 +550,8 @@ func TestMultiClusterHealthConfig(t *testing.T) {
 	config.Set(conf)
 
 	prom := new(prometheustest.PromClientMock)
-	prom.MockNamespaceServicesRequestRates("testNamespace", "0s", time.Unix(0, 0), model.Vector{})
-	prom.MockAllRequestRates("testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
+	prom.MockNamespaceServicesRequestRates(context.Background(), "testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
+	prom.MockAllRequestRates(context.Background(), "testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
 	businessLayer := business.NewLayerBuilder(t, conf).WithClients(clients).WithProm(prom).Build()
 
 	globalInfo := graph.NewGlobalInfo(businessLayer, nil, config.Get(), []models.KubeCluster{}, NewGlobalIstioInfo())
@@ -622,7 +622,7 @@ func setupHealthConfig(t *testing.T, services []core_v1.Service, deployments []a
 	conf.ExternalServices.Istio.IstioAPIEnabled = false
 	config.Set(conf)
 	prom := new(prometheustest.PromClientMock)
-	prom.MockNamespaceServicesRequestRates("testNamespace", "0s", time.Unix(0, 0), model.Vector{})
-	prom.MockAllRequestRates("testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
+	prom.MockNamespaceServicesRequestRates(context.Background(), "testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
+	prom.MockAllRequestRates(context.Background(), "testNamespace", conf.KubernetesConfig.ClusterName, "0s", time.Unix(0, 0), model.Vector{})
 	return business.NewLayerBuilder(t, conf).WithClient(k8s).Build()
 }
diff --git a/handlers/authentication/context_test.go b/handlers/authentication/context_test.go
new file mode 100644
index 000000000..3a8d86895
--- /dev/null
+++ b/handlers/authentication/context_test.go
@@ -0,0 +1,144 @@
+package authentication
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	utilcontext "github.com/kiali/kiali/util/context"
+)
+
+func TestRequestHeaders_IsValid(t *testing.T) {
+	testCases := []struct {
+		name        string
+		requestID   string
+		expectValid bool
+	}{
+		{"valid short ID", "req-123", true},
+		{"valid UUID", "550e8400-e29b-41d4-a716-446655440000", true},
+		{"valid generated ID", "kiali-20250924-abc123", true},
+		{"empty ID", "", false},
+		{"valid long ID", string(make([]byte, 1024)), true}, // 1024 chars = valid
+		{"too long ID", string(make([]byte, 1025)), false},  // 1025 chars = invalid
+		{"very long ID", string(make([]byte, 2000)), false},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			headers := &utilcontext.RequestHeaders{XRequestID: tc.requestID}
+			assert.Equal(t, tc.expectValid, headers.IsValid())
+		})
+	}
+}
+
+func TestSetRequestHeadersContext(t *testing.T) {
+	testCases := []struct {
+		name    string
+		headers *utilcontext.RequestHeaders
+	}{
+		{"valid headers", &utilcontext.RequestHeaders{XRequestID: "test-123"}},
+		{"empty request ID", &utilcontext.RequestHeaders{XRequestID: ""}},
+		{"nil headers", nil},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			// Set headers in context
+			newCtx := utilcontext.SetRequestHeadersContext(ctx, tc.headers)
+
+			// Verify context was modified
+			assert.NotEqual(t, ctx, newCtx)
+
+			// Verify we can retrieve the headers
+			retrieved := utilcontext.GetRequestHeadersContext(newCtx)
+			assert.Equal(t, tc.headers, retrieved)
+		})
+	}
+}
+
+func TestGetRequestHeadersContext(t *testing.T) {
+	testCases := []struct {
+		name     string
+		setup    func() context.Context
+		expected *utilcontext.RequestHeaders
+	}{
+		{
+			name: "context with headers",
+			setup: func() context.Context {
+				headers := &utilcontext.RequestHeaders{XRequestID: "test-456"}
+				return utilcontext.SetRequestHeadersContext(context.Background(), headers)
+			},
+			expected: &utilcontext.RequestHeaders{XRequestID: "test-456"},
+		},
+		{
+			name: "empty context",
+			setup: func() context.Context {
+				return context.Background()
+			},
+			expected: nil,
+		},
+		{
+			name: "context with wrong type",
+			setup: func() context.Context {
+				// Store wrong type in context
+				return context.WithValue(context.Background(), utilcontext.ContextKeyRequestHeaders, "wrong-type")
+			},
+			expected: nil,
+		},
+		{
+			name: "context with nil headers",
+			setup: func() context.Context {
+				return utilcontext.SetRequestHeadersContext(context.Background(), nil)
+			},
+			expected: nil,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := tc.setup()
+			retrieved := utilcontext.GetRequestHeadersContext(ctx)
+			assert.Equal(t, tc.expected, retrieved)
+		})
+	}
+}
+
+func TestRequestHeadersContext_Isolation(t *testing.T) {
+	// Test that different contexts maintain isolated headers
+	ctx1 := utilcontext.SetRequestHeadersContext(context.Background(), &utilcontext.RequestHeaders{XRequestID: "request-1"})
+	ctx2 := utilcontext.SetRequestHeadersContext(context.Background(), &utilcontext.RequestHeaders{XRequestID: "request-2"})
+
+	// Verify isolation
+	headers1 := utilcontext.GetRequestHeadersContext(ctx1)
+	headers2 := utilcontext.GetRequestHeadersContext(ctx2)
+
+	assert.NotNil(t, headers1)
+	assert.NotNil(t, headers2)
+	assert.Equal(t, "request-1", headers1.XRequestID)
+	assert.Equal(t, "request-2", headers2.XRequestID)
+	assert.NotEqual(t, headers1.XRequestID, headers2.XRequestID)
+}
+
+func TestRequestHeadersContext_WithExistingAuthInfo(t *testing.T) {
+	// Test that request headers context works alongside existing auth info context
+	ctx := context.Background()
+
+	// Add auth info (existing pattern)
+	authInfo := map[string]interface{}{"cluster": "test-cluster"}
+	ctx = SetAuthInfoContext(ctx, authInfo)
+
+	// Add request headers (new pattern)
+	headers := &utilcontext.RequestHeaders{XRequestID: "test-789"}
+	ctx = utilcontext.SetRequestHeadersContext(ctx, headers)
+
+	// Verify both can be retrieved
+	retrievedAuth := GetAuthInfoContext(ctx)
+	retrievedHeaders := utilcontext.GetRequestHeadersContext(ctx)
+
+	assert.Equal(t, authInfo, retrievedAuth)
+	assert.Equal(t, headers, retrievedHeaders)
+	assert.Equal(t, "test-789", retrievedHeaders.XRequestID)
+}
diff --git a/handlers/authentication/openid_auth_controller_test.go b/handlers/authentication/openid_auth_controller_test.go
index 48612e074..f40b593e6 100644
--- a/handlers/authentication/openid_auth_controller_test.go
+++ b/handlers/authentication/openid_auth_controller_test.go
@@ -41,7 +41,7 @@ const openIdTestToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQG
 
 /*** Function tests ***/
 
-// see https://github.com/kiali/kiali/issues/6226
+// see https://github.com/kiali/kiali/issues/6226 and https://github.com/kiali/kiali/issues/8717
 func TestVerifyAudienceClaim(t *testing.T) {
 	oidCfg := config.OpenIdConfig{
 		ClientId: "kiali-client",
@@ -51,29 +51,157 @@ func TestVerifyAudienceClaim(t *testing.T) {
 		IdTokenPayload: map[string]interface{}{},
 	}
 
-	oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId}
-	err := verifyAudienceClaim(&oip, oidCfg)
-	assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	// Test single audience scenarios (existing behavior)
+	t.Run("SingleAudience_Valid", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = oidCfg.ClientId
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("SingleAudience_Invalid", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = oidCfg.ClientId + "DIFFERENT"
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+	})
+
+	// Test single-element array scenarios (existing behavior)
+	t.Run("SingleElementArray_Interface_Valid", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("SingleElementArray_String_Valid", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("SingleElementArray_Interface_Invalid", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId + "DIFFERENT"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+	})
+
+	t.Run("SingleElementArray_String_Invalid", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId + "DIFFERENT"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+	})
+
+	// Test multi-audience scenarios (new functionality for issue #8717)
+	t.Run("MultiAudience_Interface_ValidFirst", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId, "other-service", "api-gateway"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("MultiAudience_Interface_ValidMiddle", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{"other-service", oidCfg.ClientId, "api-gateway"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("MultiAudience_Interface_ValidLast", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{"other-service", "api-gateway", oidCfg.ClientId}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("MultiAudience_String_ValidFirst", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId, "other-service", "api-gateway"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("MultiAudience_String_ValidMiddle", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{"other-service", oidCfg.ClientId, "api-gateway"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("MultiAudience_String_ValidLast", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{"other-service", "api-gateway", oidCfg.ClientId}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	})
+
+	t.Run("MultiAudience_Interface_NotFound", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{"other-service", "api-gateway", "different-client"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "not found in audiences")
+	})
+
+	t.Run("MultiAudience_String_NotFound", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{"other-service", "api-gateway", "different-client"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "not found in audiences")
+	})
+
+	// Test edge cases
+	t.Run("EmptyArray_Interface", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "empty audience list")
+	})
+
+	t.Run("EmptyArray_String", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []string{}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "empty audience list")
+	})
 
-	oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId}
-	err = verifyAudienceClaim(&oip, oidCfg)
-	assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	t.Run("MixedTypeArray", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{123, oidCfg.ClientId, "other-service"}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim should pass when valid audience is found")
+	})
 
-	oip.IdTokenPayload["aud"] = oidCfg.ClientId
-	err = verifyAudienceClaim(&oip, oidCfg)
-	assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
+	t.Run("NonStringInArray", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = []interface{}{123, 456}
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "not found in audiences")
+	})
+
+	t.Run("NonStringInArray_Match", func(t *testing.T) {
+		// Test that numeric values in audience arrays can match after string conversion
+		tempOidCfg := config.OpenIdConfig{
+			ClientId: "456",
+		}
+		oip.IdTokenPayload["aud"] = []interface{}{123, 456, 789}
+		err := verifyAudienceClaim(&oip, tempOidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim should pass when numeric audience converts to matching string")
+	})
 
-	oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId + "DIFFERENT"}
-	err = verifyAudienceClaim(&oip, oidCfg)
-	assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+	t.Run("MissingAudClaim", func(t *testing.T) {
+		delete(oip.IdTokenPayload, "aud")
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "has no aud claim")
+	})
 
-	oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId + "DIFFERENT"}
-	err = verifyAudienceClaim(&oip, oidCfg)
-	assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+	t.Run("InvalidType_NoMatch", func(t *testing.T) {
+		oip.IdTokenPayload["aud"] = 12345
+		err := verifyAudienceClaim(&oip, oidCfg)
+		assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+		assert.Contains(t, err.Error(), "got aud [12345]")
+	})
 
-	oip.IdTokenPayload["aud"] = oidCfg.ClientId + "DIFFERENT"
-	err = verifyAudienceClaim(&oip, oidCfg)
-	assert.NotNil(t, err, "verifyAudienceClaim should have failed")
+	t.Run("NonStringType_ConversionMatch", func(t *testing.T) {
+		// Test that non-string audience claims are converted to strings and can match
+		// Use a temporary config with a numeric ClientId that matches the integer aud
+		tempOidCfg := config.OpenIdConfig{
+			ClientId: "12345",
+		}
+		oip.IdTokenPayload["aud"] = 12345
+		err := verifyAudienceClaim(&oip, tempOidCfg)
+		assert.Nil(t, err, "verifyAudienceClaim should pass when integer audience converts to matching string")
+	})
 }
 
 func TestValidateOpenIdTokenInHouse(t *testing.T) {
@@ -295,7 +423,7 @@ QwIDAQAB
 	response = rr.Result()
 	assert.Len(t, response.Cookies(), 1)
 	assert.Equal(t, nonceCookieName(conf.KubernetesConfig.ClusterName), response.Cookies()[0].Name)
-	assert.Equal(t, "/kiali-test/?openid_error=the+OpenID+token+was+rejected%3A+the+OpenId+token+is+not+targeted+for+Kiali%3B+got+aud+%3D+%27bad-aud-client%27", response.Header.Get("Location"))
+	assert.Equal(t, "/kiali-test/?openid_error=the+OpenID+token+was+rejected%3A+the+OpenId+token+is+not+targeted+for+Kiali%3B+got+aud+%5Bbad-aud-client%5D", response.Header.Get("Location"))
 }
 
 /*** Implicit flow tests ***/
diff --git a/handlers/config_test.go b/handlers/config_test.go
index b62bc02f4..948bfe242 100644
--- a/handlers/config_test.go
+++ b/handlers/config_test.go
@@ -1,6 +1,7 @@
 package handlers_test
 
 import (
+	"context"
 	"encoding/json"
 	"io"
 	"net/http"
@@ -23,11 +24,11 @@ type fakePromClient struct {
 	prometheustest.PromClientMock
 }
 
-func (fpc *fakePromClient) GetConfiguration() (prom_v1.ConfigResult, error) {
+func (fpc *fakePromClient) GetConfiguration(ctx context.Context) (prom_v1.ConfigResult, error) {
 	return prom_v1.ConfigResult{}, nil
 }
 
-func (fpc *fakePromClient) GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error) {
+func (fpc *fakePromClient) GetRuntimeinfo(ctx context.Context) (prom_v1.RuntimeinfoResult, error) {
 	return prom_v1.RuntimeinfoResult{}, nil
 }
 
diff --git a/handlers/health_test.go b/handlers/health_test.go
index f2f08294f..286a34850 100644
--- a/handlers/health_test.go
+++ b/handlers/health_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/prometheus/common/model"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	core_v1 "k8s.io/api/core/v1"
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -72,7 +73,7 @@ func TestClustersHealth(t *testing.T) {
 	conf := config.NewConfig()
 
 	// Test 17s on rate interval to check that rate interval is adjusted correctly.
-	prom.On("GetAllRequestRates", "ns", conf.KubernetesConfig.ClusterName, "17s", util.Clock.Now()).Return(model.Vector{}, nil)
+	prom.On("GetAllRequestRates", mock.Anything, "ns", conf.KubernetesConfig.ClusterName, "17s", util.Clock.Now()).Return(model.Vector{}, nil)
 
 	resp, err := http.Get(url)
 	if err != nil {
diff --git a/prometheus/context_headers_roundtripper_test.go b/prometheus/context_headers_roundtripper_test.go
new file mode 100644
index 000000000..26af834aa
--- /dev/null
+++ b/prometheus/context_headers_roundtripper_test.go
@@ -0,0 +1,157 @@
+package prometheus
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	utilcontext "github.com/kiali/kiali/util/context"
+)
+
+// mockRoundTripper captures the request for verification
+type mockRoundTripper struct {
+	capturedRequest *http.Request
+	response        *http.Response
+	err             error
+}
+
+func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	// Capture the request for verification
+	m.capturedRequest = req
+	return m.response, m.err
+}
+
+func setupMockRoundTripper() *mockRoundTripper {
+	return &mockRoundTripper{
+		response: &http.Response{
+			StatusCode: 200,
+			Body:       io.NopCloser(strings.NewReader(`{"status":"success"}`)),
+			Header:     make(http.Header),
+		},
+	}
+}
+
+func TestContextHeadersRoundTripper_WithXRequestId(t *testing.T) {
+	// Setup
+	mockRT := setupMockRoundTripper()
+	contextRT := newContextHeadersRoundTripper(mockRT)
+
+	// Create request with context containing X-Request-Id
+	req, err := http.NewRequest("GET", "http://prometheus:9090/api/v1/query", nil)
+	assert.NoError(t, err)
+
+	headers := &utilcontext.RequestHeaders{XRequestID: "test-prometheus-123"}
+	ctx := utilcontext.SetRequestHeadersContext(context.Background(), headers)
+	req = req.WithContext(ctx)
+
+	// Execute RoundTrip
+	resp, err := contextRT.RoundTrip(req)
+
+	// Verify response
+	assert.NoError(t, err)
+	assert.NotNil(t, resp)
+	assert.Equal(t, 200, resp.StatusCode)
+
+	// Verify X-Request-Id header was added
+	assert.NotNil(t, mockRT.capturedRequest)
+	assert.Equal(t, "test-prometheus-123", mockRT.capturedRequest.Header.Get("X-Request-Id"))
+}
+
+func TestContextHeadersRoundTripper_WithoutContext(t *testing.T) {
+	// Setup
+	mockRT := setupMockRoundTripper()
+	contextRT := newContextHeadersRoundTripper(mockRT)
+
+	// Create request without any context headers
+	req, err := http.NewRequest("GET", "http://prometheus:9090/api/v1/query", nil)
+	assert.NoError(t, err)
+
+	// Execute RoundTrip
+	resp, err := contextRT.RoundTrip(req)
+
+	// Verify response
+	assert.NoError(t, err)
+	assert.NotNil(t, resp)
+
+	// Verify no X-Request-Id header was added
+	assert.NotNil(t, mockRT.capturedRequest)
+	assert.Empty(t, mockRT.capturedRequest.Header.Get("X-Request-Id"))
+}
+
+func TestContextHeadersRoundTripper_WithInvalidHeaders(t *testing.T) {
+	testCases := []struct {
+		name         string
+		requestID    string
+		expectHeader bool
+	}{
+		{"empty request ID", "", false},
+		{"too long request ID", strings.Repeat("a", 1025), false},
+		{"valid long request ID", strings.Repeat("a", 1024), true},
+		{"valid request ID", "valid-prometheus-123", true},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup
+			mockRT := setupMockRoundTripper()
+			contextRT := newContextHeadersRoundTripper(mockRT)
+
+			// Create request with context containing headers
+			req, err := http.NewRequest("GET", "http://prometheus:9090/api/v1/query", nil)
+			assert.NoError(t, err)
+
+			headers := &utilcontext.RequestHeaders{XRequestID: tc.requestID}
+			ctx := utilcontext.SetRequestHeadersContext(context.Background(), headers)
+			req = req.WithContext(ctx)
+
+			// Execute RoundTrip
+			resp, err := contextRT.RoundTrip(req)
+
+			// Verify response
+			assert.NoError(t, err)
+			assert.NotNil(t, resp)
+
+			// Verify header behavior
+			if tc.expectHeader {
+				assert.Equal(t, tc.requestID, mockRT.capturedRequest.Header.Get("X-Request-Id"))
+			} else {
+				assert.Empty(t, mockRT.capturedRequest.Header.Get("X-Request-Id"))
+			}
+		})
+	}
+}
+
+func TestContextHeadersRoundTripper_PreservesExistingHeaders(t *testing.T) {
+	// Setup
+	mockRT := setupMockRoundTripper()
+	contextRT := newContextHeadersRoundTripper(mockRT)
+
+	// Create request with existing headers
+	req, err := http.NewRequest("GET", "http://prometheus:9090/api/v1/query", nil)
+	assert.NoError(t, err)
+
+	req.Header.Set("Authorization", "Bearer token123")
+	req.Header.Set("Content-Type", "application/json")
+
+	// Add context headers
+	headers := &utilcontext.RequestHeaders{XRequestID: "preserve-prometheus-456"}
+	ctx := utilcontext.SetRequestHeadersContext(context.Background(), headers)
+	req = req.WithContext(ctx)
+
+	// Execute RoundTrip
+	resp, err := contextRT.RoundTrip(req)
+
+	// Verify response
+	assert.NoError(t, err)
+	assert.NotNil(t, resp)
+
+	// Verify all headers are preserved
+	capturedReq := mockRT.capturedRequest
+	assert.Equal(t, "Bearer token123", capturedReq.Header.Get("Authorization"))
+	assert.Equal(t, "application/json", capturedReq.Header.Get("Content-Type"))
+	assert.Equal(t, "preserve-prometheus-456", capturedReq.Header.Get("X-Request-Id"))
+}
diff --git a/prometheus/prometheustest/client_test.go b/prometheus/prometheustest/client_test.go
index dabc8d24e..6ae6d7b0a 100644
--- a/prometheus/prometheustest/client_test.go
+++ b/prometheus/prometheustest/client_test.go
@@ -1,6 +1,7 @@
 package prometheustest
 
 import (
+	"context"
 	"testing"
 	"time"
 
@@ -52,7 +53,7 @@ func TestGetAllRequestRates(t *testing.T) {
 	}
 	api.OnQueryTime(`rate(istio_requests_total{source_workload_namespace="ns",source_cluster="east"}[5m]) > 0`, &queryTime, vectorQ2)
 
-	rates, _ := client.GetAllRequestRates("ns", "east", "5m", queryTime)
+	rates, _ := client.GetAllRequestRates(context.Background(), "ns", "east", "5m", queryTime)
 	assert.Equal(t, 2, rates.Len())
 	assert.Equal(t, vectorQ1[0], rates[0])
 	assert.Equal(t, vectorQ2[0], rates[1])
@@ -85,7 +86,7 @@ func TestGetAllRequestRatesIstioSystem(t *testing.T) {
 	}
 	api.OnQueryTime(`rate(istio_requests_total{source_workload_namespace="istio-system",source_cluster="east"}[5m]) > 0`, &queryTime, vectorQ2)
 
-	rates, _ := client.GetAllRequestRates("istio-system", "east", "5m", queryTime)
+	rates, _ := client.GetAllRequestRates(context.Background(), "istio-system", "east", "5m", queryTime)
 	assert.Equal(t, 2, rates.Len())
 	assert.Equal(t, vectorQ1[0], rates[0])
 	assert.Equal(t, vectorQ2[0], rates[1])
@@ -109,7 +110,7 @@ func TestGetNamespaceServicesRequestRates(t *testing.T) {
 	}
 	api.OnQueryTime(`rate(istio_requests_total{destination_service_namespace="ns",destination_cluster=~"east|unknown"}[5m]) > 0`, &queryTime, vectorQ1)
 
-	rates, _ := client.GetNamespaceServicesRequestRates("ns", "east", "5m", queryTime)
+	rates, _ := client.GetNamespaceServicesRequestRates(context.Background(), "ns", "east", "5m", queryTime)
 	assert.Equal(t, 1, rates.Len())
 	assert.Equal(t, vectorQ1[0], rates[0])
 }
@@ -124,7 +125,7 @@ func TestConfig(t *testing.T) {
 		YAML: `{"status":"success","data":{"yaml":"global:\n  scrape_interval: 15s\n"}}`,
 	})
 
-	config, _ := client.GetConfiguration()
+	config, _ := client.GetConfiguration(context.Background())
 	assert.Contains(t, config.YAML, "scrape_interval")
 }
 
@@ -136,7 +137,7 @@ func TestRuntimeInfo(t *testing.T) {
 	}
 	mockRuntimeinfoResult(api, prom_v1.RuntimeinfoResult{StorageRetention: "6h"})
 
-	ri, _ := client.GetRuntimeinfo()
+	ri, _ := client.GetRuntimeinfo(context.Background())
 	assert.Equal(t, "6h", ri.StorageRetention)
 }
 
diff --git a/prometheus/prometheustest/fake.go b/prometheus/prometheustest/fake.go
index 74cc9dd0e..4a788335c 100644
--- a/prometheus/prometheustest/fake.go
+++ b/prometheus/prometheustest/fake.go
@@ -20,37 +20,37 @@ func (f *FakeClient) API() prom_v1.API {
 }
 
 // FetchDelta returns empty Metric
-func (f *FakeClient) FetchDelta(metricName, labels, grouping string, queryTime time.Time, duration time.Duration) prometheus.Metric {
+func (f *FakeClient) FetchDelta(ctx context.Context, metricName, labels, grouping string, queryTime time.Time, duration time.Duration) prometheus.Metric {
 	return prometheus.Metric{}
 }
 
 // FetchHistogramRange returns empty Histogram
-func (f *FakeClient) FetchHistogramRange(metricName, labels, grouping string, q *prometheus.RangeQuery) prometheus.Histogram {
+func (f *FakeClient) FetchHistogramRange(ctx context.Context, metricName, labels, grouping string, q *prometheus.RangeQuery) prometheus.Histogram {
 	return prometheus.Histogram{}
 }
 
 // FetchHistogramValues returns empty map and nil error
-func (f *FakeClient) FetchHistogramValues(metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
+func (f *FakeClient) FetchHistogramValues(ctx context.Context, metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
 	return map[string]model.Vector{}, nil
 }
 
 // FetchRange returns empty Metric
-func (f *FakeClient) FetchRange(metricName, labels, grouping, aggregator string, q *prometheus.RangeQuery) prometheus.Metric {
+func (f *FakeClient) FetchRange(ctx context.Context, metricName, labels, grouping, aggregator string, q *prometheus.RangeQuery) prometheus.Metric {
 	return prometheus.Metric{}
 }
 
 // FetchRateRange returns empty Metric
-func (f *FakeClient) FetchRateRange(metricName string, labels []string, grouping string, q *prometheus.RangeQuery) prometheus.Metric {
+func (f *FakeClient) FetchRateRange(ctx context.Context, metricName string, labels []string, grouping string, q *prometheus.RangeQuery) prometheus.Metric {
 	return prometheus.Metric{}
 }
 
 // GetAllRequestRates returns empty Vector and nil error
-func (f *FakeClient) GetAllRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (f *FakeClient) GetAllRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
 	return model.Vector{}, nil
 }
 
 // GetAppRequestRates returns empty Vectors and nil error
-func (f *FakeClient) GetAppRequestRates(namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+func (f *FakeClient) GetAppRequestRates(ctx context.Context, namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
 	return model.Vector{}, model.Vector{}, nil
 }
 
@@ -60,36 +60,36 @@ func (f *FakeClient) GetBuildInfo(ctx context.Context) (*prom_v1.BuildinfoResult
 }
 
 // GetConfiguration returns empty ConfigResult and nil error
-func (f *FakeClient) GetConfiguration() (prom_v1.ConfigResult, error) {
+func (f *FakeClient) GetConfiguration(ctx context.Context) (prom_v1.ConfigResult, error) {
 	return prom_v1.ConfigResult{}, nil
 }
 
 // GetExistingMetricNames returns empty slice and nil error
-func (f *FakeClient) GetExistingMetricNames(metricNames []string) ([]string, error) {
+func (f *FakeClient) GetExistingMetricNames(ctx context.Context, metricNames []string) ([]string, error) {
 	return []string{}, nil
 }
 
 // GetMetricsForLabels returns empty slice and nil error
-func (f *FakeClient) GetMetricsForLabels(metricNames []string, labels string) ([]string, error) {
+func (f *FakeClient) GetMetricsForLabels(ctx context.Context, metricNames []string, labels string) ([]string, error) {
 	return []string{}, nil
 }
 
 // GetNamespaceServicesRequestRates returns empty Vector and nil error
-func (f *FakeClient) GetNamespaceServicesRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (f *FakeClient) GetNamespaceServicesRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
 	return model.Vector{}, nil
 }
 
 // GetServiceRequestRates returns empty Vector and nil error
-func (f *FakeClient) GetServiceRequestRates(namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+func (f *FakeClient) GetServiceRequestRates(ctx context.Context, namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
 	return model.Vector{}, nil
 }
 
 // GetRuntimeinfo returns empty RuntimeinfoResult and nil error
-func (f *FakeClient) GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error) {
+func (f *FakeClient) GetRuntimeinfo(ctx context.Context) (prom_v1.RuntimeinfoResult, error) {
 	return prom_v1.RuntimeinfoResult{}, nil
 }
 
 // GetWorkloadRequestRates returns empty Vectors and nil error
-func (f *FakeClient) GetWorkloadRequestRates(namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+func (f *FakeClient) GetWorkloadRequestRates(ctx context.Context, namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
 	return model.Vector{}, model.Vector{}, nil
 }
diff --git a/prometheus/prometheustest/mock.go b/prometheus/prometheustest/mock.go
index 3131473d3..c184e976b 100644
--- a/prometheus/prometheustest/mock.go
+++ b/prometheus/prometheustest/mock.go
@@ -183,7 +183,7 @@ func (o *PromAPIMock) MockEmptyRange(query string) {
 	o.OnQueryRange(query, nil, emptyMatrix())
 }
 
-func (o *PromAPIMock) MockHistoValue(baseName, suffix string, retAvg model.Vector, retMed model.Vector, ret95 model.Vector, ret99 model.Vector) {
+func (o *PromAPIMock) MockHistoValue(ctx context.Context, baseName, suffix string, retAvg model.Vector, retMed model.Vector, ret95 model.Vector, ret99 model.Vector) {
 	histMetric := "sum(rate(" + baseName + "_bucket" + suffix + ")) by (le))"
 	o.MockTime("histogram_quantile(0.5, "+histMetric, retMed)
 	o.MockTime("histogram_quantile(0.95, "+histMetric, ret95)
@@ -283,37 +283,37 @@ func (o *PromClientMock) API() prom_v1.API {
 }
 
 // MockAllRequestRates mocks GetAllRequestRates for given namespace, rateInverval and queryTime, returning out vector
-func (o *PromClientMock) MockAllRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time, out model.Vector) {
-	o.On("GetAllRequestRates", namespace, cluster, ratesInterval, queryTime).Return(out, nil)
+func (o *PromClientMock) MockAllRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time, out model.Vector) {
+	o.On("GetAllRequestRates", mock.Anything, namespace, cluster, ratesInterval, queryTime).Return(out, nil)
 }
 
 // MockAppRequestRates mocks GetAppRequestRates for given namespace and app, returning in & out vectors
-func (o *PromClientMock) MockAppRequestRates(namespace, cluster, app string, in, out model.Vector) {
-	o.On("GetAppRequestRates", namespace, cluster, app, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(in, out, nil)
+func (o *PromClientMock) MockAppRequestRates(ctx context.Context, namespace, cluster, app string, in, out model.Vector) {
+	o.On("GetAppRequestRates", mock.Anything, namespace, cluster, app, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(in, out, nil)
 }
 
 // MockNamespaceServicesRequestRates mocks GetNamespaceServicesRequestRates for given namespace, rateInterval and queryTime, returning out vector
-func (o *PromClientMock) MockNamespaceServicesRequestRates(namespace, ratesInterval string, queryTime time.Time, out model.Vector) {
-	o.On("GetNamespaceServicesRequestRates", namespace, ratesInterval, queryTime).Return(out, nil)
+func (o *PromClientMock) MockNamespaceServicesRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time, out model.Vector) {
+	o.On("GetNamespaceServicesRequestRates", mock.Anything, namespace, cluster, ratesInterval, queryTime).Return(out, nil)
 }
 
 // MockServiceRequestRates mocks GetServiceRequestRates for given namespace and service, returning in vector
-func (o *PromClientMock) MockServiceRequestRates(namespace, cluster, service string, in model.Vector) {
-	o.On("GetServiceRequestRates", namespace, cluster, service, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(in, nil)
+func (o *PromClientMock) MockServiceRequestRates(ctx context.Context, namespace, cluster, service string, in model.Vector) {
+	o.On("GetServiceRequestRates", mock.Anything, namespace, cluster, service, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(in, nil)
 }
 
 // MockWorkloadRequestRates mocks GetWorkloadRequestRates for given namespace and workload, returning in & out vectors
-func (o *PromClientMock) MockWorkloadRequestRates(namespace, cluster, wkld string, in, out model.Vector) {
-	o.On("GetWorkloadRequestRates", namespace, cluster, wkld, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(in, out, nil)
+func (o *PromClientMock) MockWorkloadRequestRates(ctx context.Context, namespace, cluster, wkld string, in, out model.Vector) {
+	o.On("GetWorkloadRequestRates", mock.Anything, namespace, cluster, wkld, mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(in, out, nil)
 }
 
 // MockMetricsForLabels mocks GetMetricsForLabels
-func (o *PromClientMock) MockMetricsForLabels(metrics []string) {
-	o.On("GetMetricsForLabels", mock.AnythingOfType("[]string"), mock.AnythingOfType("string")).Return(metrics, nil)
+func (o *PromClientMock) MockMetricsForLabels(ctx context.Context, metrics []string) {
+	o.On("GetMetricsForLabels", mock.Anything, mock.AnythingOfType("[]string"), mock.AnythingOfType("string")).Return(metrics, nil)
 }
 
-func (o *PromClientMock) GetAllRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
-	args := o.Called(namespace, cluster, ratesInterval, queryTime)
+func (o *PromClientMock) GetAllRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+	args := o.Called(ctx, namespace, cluster, ratesInterval, queryTime)
 	return args.Get(0).(model.Vector), args.Error(1)
 }
 
@@ -325,77 +325,77 @@ func (o *PromClientMock) GetBuildInfo(ctx context.Context) (*prom_v1.BuildinfoRe
 	return args.Get(0).(*prom_v1.BuildinfoResult), args.Error(1)
 }
 
-func (o *PromClientMock) GetConfiguration() (prom_v1.ConfigResult, error) {
-	args := o.Called()
+func (o *PromClientMock) GetConfiguration(ctx context.Context) (prom_v1.ConfigResult, error) {
+	args := o.Called(ctx)
 	return args.Get(0).(prom_v1.ConfigResult), args.Error(1)
 }
 
-func (o *PromClientMock) GetExistingMetricNames(metricNames []string) ([]string, error) {
-	args := o.Called(metricNames)
+func (o *PromClientMock) GetExistingMetricNames(ctx context.Context, metricNames []string) ([]string, error) {
+	args := o.Called(ctx, metricNames)
 	return args.Get(0).([]string), args.Error(1)
 }
 
-func (o *PromClientMock) GetNamespaceServicesRequestRates(namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
-	args := o.Called(namespace, cluster, ratesInterval, queryTime)
+func (o *PromClientMock) GetNamespaceServicesRequestRates(ctx context.Context, namespace, cluster, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+	args := o.Called(ctx, namespace, cluster, ratesInterval, queryTime)
 	return args.Get(0).(model.Vector), args.Error(1)
 }
 
-func (o *PromClientMock) GetAppRequestRates(namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
-	args := o.Called(namespace, cluster, app, ratesInterval, queryTime)
+func (o *PromClientMock) GetAppRequestRates(ctx context.Context, namespace, cluster, app, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+	args := o.Called(ctx, namespace, cluster, app, ratesInterval, queryTime)
 	return args.Get(0).(model.Vector), args.Get(1).(model.Vector), args.Error(2)
 }
 
-func (o *PromClientMock) GetServiceRequestRates(namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
-	args := o.Called(namespace, cluster, service, ratesInterval, queryTime)
+func (o *PromClientMock) GetServiceRequestRates(ctx context.Context, namespace, cluster, service, ratesInterval string, queryTime time.Time) (model.Vector, error) {
+	args := o.Called(ctx, namespace, cluster, service, ratesInterval, queryTime)
 	return args.Get(0).(model.Vector), args.Error(1)
 }
 
-func (o *PromClientMock) GetWorkloadRequestRates(namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
-	args := o.Called(namespace, cluster, workload, ratesInterval, queryTime)
+func (o *PromClientMock) GetWorkloadRequestRates(ctx context.Context, namespace, cluster, workload, ratesInterval string, queryTime time.Time) (model.Vector, model.Vector, error) {
+	args := o.Called(ctx, namespace, cluster, workload, ratesInterval, queryTime)
 	return args.Get(0).(model.Vector), args.Get(1).(model.Vector), args.Error(2)
 }
 
-func (o *PromClientMock) FetchDelta(metricName, labels, grouping string, queryTime time.Time, duration time.Duration) prometheus.Metric {
-	args := o.Called(metricName, labels, grouping, queryTime, duration)
+func (o *PromClientMock) FetchDelta(ctx context.Context, metricName, labels, grouping string, queryTime time.Time, duration time.Duration) prometheus.Metric {
+	args := o.Called(ctx, metricName, labels, grouping, queryTime, duration)
 	return args.Get(0).(prometheus.Metric)
 }
 
-func (o *PromClientMock) FetchRange(metricName, labels, grouping, aggregator string, q *prometheus.RangeQuery) prometheus.Metric {
-	args := o.Called(metricName, labels, grouping, aggregator, q)
+func (o *PromClientMock) FetchRange(ctx context.Context, metricName, labels, grouping, aggregator string, q *prometheus.RangeQuery) prometheus.Metric {
+	args := o.Called(ctx, metricName, labels, grouping, aggregator, q)
 	return args.Get(0).(prometheus.Metric)
 }
 
-func (o *PromClientMock) FetchRateRange(metricName string, labels []string, grouping string, q *prometheus.RangeQuery) prometheus.Metric {
-	args := o.Called(metricName, labels, grouping, q)
+func (o *PromClientMock) FetchRateRange(ctx context.Context, metricName string, labels []string, grouping string, q *prometheus.RangeQuery) prometheus.Metric {
+	args := o.Called(ctx, metricName, labels, grouping, q)
 	return args.Get(0).(prometheus.Metric)
 }
 
-func (o *PromClientMock) FetchHistogramRange(metricName, labels, grouping string, q *prometheus.RangeQuery) prometheus.Histogram {
-	args := o.Called(metricName, labels, grouping, q)
+func (o *PromClientMock) FetchHistogramRange(ctx context.Context, metricName, labels, grouping string, q *prometheus.RangeQuery) prometheus.Histogram {
+	args := o.Called(ctx, metricName, labels, grouping, q)
 	return args.Get(0).(prometheus.Histogram)
 }
 
-func (o *PromClientMock) FetchHistogramValues(metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
-	args := o.Called(metricName, labels, grouping, rateInterval, avg, quantiles, queryTime)
+func (o *PromClientMock) FetchHistogramValues(ctx context.Context, metricName, labels, grouping, rateInterval string, avg bool, quantiles []string, queryTime time.Time) (map[string]model.Vector, error) {
+	args := o.Called(ctx, metricName, labels, grouping, rateInterval, avg, quantiles, queryTime)
 	return args.Get(0).(map[string]model.Vector), args.Error((1))
 }
 
-func (o *PromClientMock) GetMetricsForLabels(metricNames []string, labels string) ([]string, error) {
-	args := o.Called(metricNames, labels)
+func (o *PromClientMock) GetMetricsForLabels(ctx context.Context, metricNames []string, labels string) ([]string, error) {
+	args := o.Called(ctx, metricNames, labels)
 	return args.Get(0).([]string), args.Error(1)
 }
 
-func (o *PromClientMock) GetRuntimeinfo() (prom_v1.RuntimeinfoResult, error) {
-	args := o.Called()
+func (o *PromClientMock) GetRuntimeinfo(ctx context.Context) (prom_v1.RuntimeinfoResult, error) {
+	args := o.Called(ctx)
 	return args.Get(0).(prom_v1.RuntimeinfoResult), args.Error(1)
 }
 
-func (o *PromClientMock) MockMetric(name string, labels string, q *prometheus.RangeQuery, value float64) {
-	o.On("FetchRateRange", name, []string{labels}, "", q).Return(fakeMetric(value))
+func (o *PromClientMock) MockMetric(ctx context.Context, name string, labels string, q *prometheus.RangeQuery, value float64) {
+	o.On("FetchRateRange", mock.Anything, name, []string{labels}, "", q).Return(fakeMetric(value))
 }
 
-func (o *PromClientMock) MockHistogram(name string, labels string, q *prometheus.RangeQuery, avg, p99 float64) {
-	o.On("FetchHistogramRange", name, labels, "", q).Return(fakeHistogram(avg, p99))
+func (o *PromClientMock) MockHistogram(ctx context.Context, name string, labels string, q *prometheus.RangeQuery, avg, p99 float64) {
+	o.On("FetchHistogramRange", mock.Anything, name, labels, "", q).Return(fakeHistogram(avg, p99))
 }
 
 func fakeMetric(value float64) prometheus.Metric {
diff --git a/routing/router_test.go b/routing/router_test.go
index 989b505b4..c8806f47e 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -9,11 +9,13 @@ import (
 
 	"github.com/gorilla/mux"
 	"github.com/prometheus/client_golang/prometheus"
+	"github.com/rs/zerolog/hlog"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes/kubetest"
 	"github.com/kiali/kiali/prometheus/internalmetrics"
+	utilcontext "github.com/kiali/kiali/util/context"
 	"github.com/kiali/kiali/util/filetest"
 )
 
@@ -223,3 +225,275 @@ func TestMetricHandlerAPIFailures(t *testing.T) {
 		}
 	}
 }
+
+func TestBuildHttpHandlerLogger_RequestIdHandling(t *testing.T) {
+	// Create a test handler that captures both context and hlog state
+	type testCapture struct {
+		contextHeaders *utilcontext.RequestHeaders
+		httpHeader     string
+		hlogId         string
+		hlogFound      bool
+	}
+
+	var captured testCapture
+	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// Capture our context
+		captured.contextHeaders = utilcontext.GetRequestHeadersContext(r.Context())
+
+		// Capture HTTP header state
+		captured.httpHeader = r.Header.Get("X-Request-Id")
+
+		// Capture hlog context state
+		if id, ok := hlog.IDFromRequest(r); ok {
+			captured.hlogId = id.String()
+			captured.hlogFound = true
+		} else {
+			captured.hlogId = ""
+			captured.hlogFound = false
+		}
+
+		w.WriteHeader(http.StatusOK)
+	})
+
+	// Create a route for testing
+	testRoute := Route{
+		Name:    "test-route",
+		Pattern: "/test",
+	}
+
+	t.Run("request with existing X-Request-Id header", func(t *testing.T) {
+		// Reset capture
+		captured = testCapture{}
+
+		// Build the handler with logger
+		handler := buildHttpHandlerLogger(testRoute, testHandler)
+
+		req := httptest.NewRequest("GET", "/test", nil)
+		req.Header.Set("X-Request-Id", "existing-request-123")
+
+		resp := httptest.NewRecorder()
+		handler.ServeHTTP(resp, req)
+
+		// Verify behavior: existing header should be used
+		assert.Equal(t, "existing-request-123", captured.httpHeader, "HTTP header should contain original value")
+		assert.False(t, captured.hlogFound, "hlog should not generate ID when header exists")
+
+		// Verify our context gets the existing header value
+		assert.NotNil(t, captured.contextHeaders, "context headers should be set")
+		assert.Equal(t, "existing-request-123", captured.contextHeaders.XRequestID, "context should contain original header value")
+		assert.True(t, captured.contextHeaders.IsValid(), "context headers should be valid")
+	})
+
+	t.Run("request without X-Request-Id header", func(t *testing.T) {
+		// Reset capture
+		captured = testCapture{}
+
+		// Build the handler with logger
+		handler := buildHttpHandlerLogger(testRoute, testHandler)
+
+		req := httptest.NewRequest("GET", "/test", nil)
+		// No X-Request-Id header
+
+		resp := httptest.NewRecorder()
+		handler.ServeHTTP(resp, req)
+
+		// Verify behavior: hlog should generate ID, header should remain empty
+		assert.Empty(t, captured.httpHeader, "HTTP header should remain empty")
+		assert.True(t, captured.hlogFound, "hlog should generate an ID")
+		assert.NotEmpty(t, captured.hlogId, "hlog ID should be generated")
+
+		// Verify our context gets the hlog-generated ID
+		assert.NotNil(t, captured.contextHeaders, "context headers should be set")
+		assert.Equal(t, captured.hlogId, captured.contextHeaders.XRequestID, "context should contain hlog-generated ID")
+		assert.True(t, captured.contextHeaders.IsValid(), "context headers should be valid")
+
+		// Verify hlog ID format (should be shorter than UUID)
+		assert.Greater(t, len(captured.hlogId), 0, "hlog ID should not be empty")
+		assert.Less(t, len(captured.hlogId), 36, "hlog ID should be shorter than UUID")
+	})
+
+	t.Run("request with empty X-Request-Id header", func(t *testing.T) {
+		// Reset capture
+		captured = testCapture{}
+
+		// Build the handler with logger
+		handler := buildHttpHandlerLogger(testRoute, testHandler)
+
+		req := httptest.NewRequest("GET", "/test", nil)
+		req.Header.Set("X-Request-Id", "")
+
+		resp := httptest.NewRecorder()
+		handler.ServeHTTP(resp, req)
+
+		// Verify behavior: empty header treated as missing, hlog should generate ID
+		assert.Empty(t, captured.httpHeader, "HTTP header should be empty")
+		assert.True(t, captured.hlogFound, "hlog should generate an ID for empty header")
+		assert.NotEmpty(t, captured.hlogId, "hlog ID should be generated")
+
+		// Verify our context gets the hlog-generated ID
+		assert.NotNil(t, captured.contextHeaders, "context headers should be set")
+		assert.Equal(t, captured.hlogId, captured.contextHeaders.XRequestID, "context should contain hlog-generated ID")
+		assert.True(t, captured.contextHeaders.IsValid(), "context headers should be valid")
+	})
+
+	t.Run("request with whitespace-only X-Request-Id header", func(t *testing.T) {
+		// Reset capture
+		captured = testCapture{}
+
+		// Build the handler with logger
+		handler := buildHttpHandlerLogger(testRoute, testHandler)
+
+		req := httptest.NewRequest("GET", "/test", nil)
+		req.Header.Set("X-Request-Id", "   ")
+
+		resp := httptest.NewRecorder()
+		handler.ServeHTTP(resp, req)
+
+		// Verify behavior: whitespace header should be treated as existing
+		assert.Equal(t, "   ", captured.httpHeader, "HTTP header should contain whitespace")
+		assert.False(t, captured.hlogFound, "hlog should not generate ID for whitespace header")
+
+		// Verify our context gets the whitespace header value
+		assert.NotNil(t, captured.contextHeaders, "context headers should be set")
+		assert.Equal(t, "   ", captured.contextHeaders.XRequestID, "context should contain whitespace header")
+		assert.True(t, captured.contextHeaders.IsValid(), "whitespace header is technically valid (non-empty, within length limit)")
+	})
+}
+
+func TestRequestIdGeneration_Uniqueness(t *testing.T) {
+	// Test that multiple requests generate unique IDs when no X-Request-Id header is provided
+	testRoute := Route{Name: "test", Pattern: "/test"}
+	requestIds := make(map[string]bool)
+
+	// Generate multiple request IDs
+	for i := 0; i < 100; i++ {
+		var capturedHeaders *utilcontext.RequestHeaders
+
+		// Capture context in handler
+		captureHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			capturedHeaders = utilcontext.GetRequestHeadersContext(r.Context())
+			w.WriteHeader(http.StatusOK)
+		})
+
+		testHandlerWithCapture := buildHttpHandlerLogger(testRoute, captureHandler)
+
+		req := httptest.NewRequest("GET", "/test", nil)
+		// Explicitly no X-Request-Id header to trigger hlog generation
+		resp := httptest.NewRecorder()
+		testHandlerWithCapture.ServeHTTP(resp, req)
+
+		// Verify uniqueness
+		assert.NotNil(t, capturedHeaders, "context headers should be set for request %d", i)
+		assert.NotEmpty(t, capturedHeaders.XRequestID, "request ID should be generated for request %d", i)
+		assert.True(t, capturedHeaders.IsValid(), "generated request ID should be valid for request %d", i)
+
+		if requestIds[capturedHeaders.XRequestID] {
+			t.Errorf("Duplicate request ID generated: %s (iteration %d)", capturedHeaders.XRequestID, i)
+		}
+		requestIds[capturedHeaders.XRequestID] = true
+	}
+
+	// Verify we generated 100 unique IDs
+	assert.Len(t, requestIds, 100, "should generate 100 unique request IDs")
+}
+
+func TestRequestIdHandling_EdgeCases(t *testing.T) {
+	testRoute := Route{Name: "test-edge-cases", Pattern: "/test"}
+
+	type testCase struct {
+		name              string
+		headerValue       *string // nil means no header, empty string means empty header
+		expectedInContext bool
+		shouldUseHlog     bool
+	}
+
+	testCases := []testCase{
+		{
+			name:              "no header set",
+			headerValue:       nil,
+			expectedInContext: true,
+			shouldUseHlog:     true,
+		},
+		{
+			name:              "empty header",
+			headerValue:       stringPtr(""),
+			expectedInContext: true,
+			shouldUseHlog:     true,
+		},
+		{
+			name:              "valid header",
+			headerValue:       stringPtr("valid-request-id-123"),
+			expectedInContext: true,
+			shouldUseHlog:     false,
+		},
+		{
+			name:              "very long header (within limit)",
+			headerValue:       stringPtr(generateString(1024)),
+			expectedInContext: true,
+			shouldUseHlog:     false,
+		},
+		{
+			name:              "very long header (exceeds limit)",
+			headerValue:       stringPtr(generateString(1025)),
+			expectedInContext: true, // Still stored, but IsValid() will return false
+			shouldUseHlog:     false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var captured *utilcontext.RequestHeaders
+			var hlogFound bool
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				captured = utilcontext.GetRequestHeadersContext(r.Context())
+				_, hlogFound = hlog.IDFromRequest(r)
+				w.WriteHeader(http.StatusOK)
+			})
+
+			loggerHandler := buildHttpHandlerLogger(testRoute, handler)
+
+			req := httptest.NewRequest("GET", "/test", nil)
+			if tc.headerValue != nil {
+				req.Header.Set("X-Request-Id", *tc.headerValue)
+			}
+
+			resp := httptest.NewRecorder()
+			loggerHandler.ServeHTTP(resp, req)
+
+			if tc.expectedInContext {
+				assert.NotNil(t, captured, "context headers should be set")
+				assert.NotEmpty(t, captured.XRequestID, "request ID should be present in context")
+			} else {
+				assert.Nil(t, captured, "context headers should not be set")
+			}
+
+			if tc.shouldUseHlog {
+				assert.True(t, hlogFound, "hlog should generate ID")
+				if captured != nil {
+					// Verify it's not empty and looks like an hlog ID (shorter than UUID)
+					assert.Greater(t, len(captured.XRequestID), 0)
+					assert.Less(t, len(captured.XRequestID), 36) // hlog IDs are shorter than UUIDs
+				}
+			} else {
+				assert.False(t, hlogFound, "hlog should not generate ID when header exists")
+				if captured != nil && tc.headerValue != nil {
+					assert.Equal(t, *tc.headerValue, captured.XRequestID, "context should contain original header value")
+				}
+			}
+		})
+	}
+}
+
+// Helper functions
+func stringPtr(s string) *string {
+	return &s
+}
+
+func generateString(length int) string {
+	result := make([]byte, length)
+	for i := range result {
+		result[i] = 'a'
+	}
+	return string(result)
+}
diff --git a/tests/integration/header_propagation_test.go b/tests/integration/header_propagation_test.go
new file mode 100644
index 000000000..4118b0669
--- /dev/null
+++ b/tests/integration/header_propagation_test.go
@@ -0,0 +1,238 @@
+package integration
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	utilcontext "github.com/kiali/kiali/util/context"
+)
+
+// MockPrometheusServer captures headers from incoming requests
+type MockPrometheusServer struct {
+	server          *httptest.Server
+	capturedHeaders map[string]string
+	mutex           sync.RWMutex
+	requestCount    int
+}
+
+func setupMockPrometheusServer(t *testing.T) *MockPrometheusServer {
+	mock := &MockPrometheusServer{
+		capturedHeaders: make(map[string]string),
+	}
+
+	mock.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		mock.mutex.Lock()
+		defer mock.mutex.Unlock()
+
+		// Capture all headers
+		for name, values := range r.Header {
+			if len(values) > 0 {
+				mock.capturedHeaders[name] = values[0]
+			}
+		}
+		mock.requestCount++
+
+		// Return mock Prometheus response
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(200)
+		_, _ = w.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[]}}`))
+	}))
+
+	return mock
+}
+
+func (m *MockPrometheusServer) GetCapturedHeaders() map[string]string {
+	m.mutex.RLock()
+	defer m.mutex.RUnlock()
+
+	result := make(map[string]string)
+	for k, v := range m.capturedHeaders {
+		result[k] = v
+	}
+	return result
+}
+
+func (m *MockPrometheusServer) GetRequestCount() int {
+	m.mutex.RLock()
+	defer m.mutex.RUnlock()
+	return m.requestCount
+}
+
+func (m *MockPrometheusServer) Reset() {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	m.capturedHeaders = make(map[string]string)
+	m.requestCount = 0
+}
+
+func (m *MockPrometheusServer) Close() {
+	m.server.Close()
+}
+
+// NewContextHeadersRoundTripper creates a new context-aware RoundTripper for testing
+func NewContextHeadersRoundTripper(rt http.RoundTripper) http.RoundTripper {
+	return &contextHeadersRoundTripper{
+		originalRT: rt,
+	}
+}
+
+// contextHeadersRoundTripper for testing - simplified version of the prometheus package implementation
+type contextHeadersRoundTripper struct {
+	originalRT http.RoundTripper
+}
+
+func (rt *contextHeadersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	// Extract headers from context and inject into request
+	if headers := utilcontext.GetRequestHeadersContext(req.Context()); headers != nil {
+		if headers.IsValid() {
+			req.Header.Set("X-Request-Id", headers.XRequestID)
+		}
+	}
+	return rt.originalRT.RoundTrip(req)
+}
+
+func TestE2E_ContextHeaderPropagation(t *testing.T) {
+	// Setup mock Prometheus server
+	mockPrometheus := setupMockPrometheusServer(t)
+	defer mockPrometheus.Close()
+
+	// Create Prometheus client pointing to mock server
+	// Note: This is a simplified test - in real integration we'd use the full Kiali setup
+
+	t.Run("header propagation through RoundTripper", func(t *testing.T) {
+		// Create a context with X-Request-Id
+		headers := &utilcontext.RequestHeaders{XRequestID: "integration-test-123"}
+		ctx := utilcontext.SetRequestHeadersContext(context.Background(), headers)
+
+		// Create HTTP request with context
+		req, err := http.NewRequest("GET", mockPrometheus.server.URL+"/api/v1/query?query=up", nil)
+		assert.NoError(t, err)
+		req = req.WithContext(ctx)
+
+		// Create RoundTripper and execute request
+		mockRT := &http.Transport{}
+		contextRT := NewContextHeadersRoundTripper(mockRT)
+
+		client := &http.Client{Transport: contextRT}
+		resp, err := client.Do(req)
+
+		// Verify response
+		assert.NoError(t, err)
+		assert.NotNil(t, resp)
+		assert.Equal(t, 200, resp.StatusCode)
+		resp.Body.Close()
+
+		// Verify X-Request-Id was propagated
+		capturedHeaders := mockPrometheus.GetCapturedHeaders()
+		assert.Equal(t, "integration-test-123", capturedHeaders["X-Request-Id"])
+	})
+}
+
+func TestE2E_ConcurrentRequestIsolation(t *testing.T) {
+	// Setup mock Prometheus server
+	mockPrometheus := setupMockPrometheusServer(t)
+	defer mockPrometheus.Close()
+
+	// Test concurrent requests with different X-Request-Id values
+	requestIds := []string{"concurrent-1", "concurrent-2", "concurrent-3"}
+
+	var wg sync.WaitGroup
+	results := make(chan string, len(requestIds))
+
+	for _, requestID := range requestIds {
+		wg.Add(1)
+		go func(id string) {
+			defer wg.Done()
+
+			// Create context with unique X-Request-Id
+			headers := &utilcontext.RequestHeaders{XRequestID: id}
+			ctx := utilcontext.SetRequestHeadersContext(context.Background(), headers)
+
+			// Create HTTP request
+			req, err := http.NewRequest("GET", mockPrometheus.server.URL+"/api/v1/query?query=up", nil)
+			assert.NoError(t, err)
+			req = req.WithContext(ctx)
+
+			// Execute request
+			mockRT := &http.Transport{}
+			contextRT := NewContextHeadersRoundTripper(mockRT)
+			client := &http.Client{Transport: contextRT}
+
+			resp, err := client.Do(req)
+			assert.NoError(t, err)
+			assert.Equal(t, 200, resp.StatusCode)
+			resp.Body.Close()
+
+			results <- id
+		}(requestID)
+	}
+
+	wg.Wait()
+	close(results)
+
+	// Verify all requests completed
+	completedIds := make([]string, 0, len(requestIds))
+	for id := range results {
+		completedIds = append(completedIds, id)
+	}
+	assert.Len(t, completedIds, len(requestIds))
+
+	// Verify multiple requests were made to Prometheus
+	assert.GreaterOrEqual(t, mockPrometheus.GetRequestCount(), len(requestIds))
+}
+
+func TestE2E_HeaderValidation(t *testing.T) {
+	// Setup mock Prometheus server
+	mockPrometheus := setupMockPrometheusServer(t)
+	defer mockPrometheus.Close()
+
+	testCases := []struct {
+		name         string
+		requestID    string
+		expectHeader bool
+	}{
+		{"valid request ID", "valid-integration-456", true},
+		{"empty request ID", "", false},
+		{"too long request ID", strings.Repeat("a", 1025), false},
+		{"valid long request ID", strings.Repeat("a", 1024), true},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Reset mock server for each test case
+			mockPrometheus.Reset()
+
+			// Create context with headers
+			headers := &utilcontext.RequestHeaders{XRequestID: tc.requestID}
+			ctx := utilcontext.SetRequestHeadersContext(context.Background(), headers)
+
+			// Create and execute request
+			req, err := http.NewRequest("GET", mockPrometheus.server.URL+"/api/v1/query", nil)
+			assert.NoError(t, err)
+			req = req.WithContext(ctx)
+
+			mockRT := &http.Transport{}
+			contextRT := NewContextHeadersRoundTripper(mockRT)
+			client := &http.Client{Transport: contextRT}
+
+			resp, err := client.Do(req)
+			assert.NoError(t, err)
+			assert.Equal(t, 200, resp.StatusCode)
+			resp.Body.Close()
+
+			// Verify header behavior
+			capturedHeaders := mockPrometheus.GetCapturedHeaders()
+			if tc.expectHeader {
+				assert.Equal(t, tc.requestID, capturedHeaders["X-Request-Id"])
+			} else {
+				assert.Empty(t, capturedHeaders["X-Request-Id"])
+			}
+		})
+	}
+}