Repository: kiali/kiali

Issue:
create a NetworkPolicy to restrict Kiali ingress traffic
There should be an option to create a NetworkPolicy that restricts access to/from the Kiali API server and metrics server.

PRs:
* operator: https://github.com/kiali/kiali-operator/pull/930
* helm: https://github.com/kiali/helm-charts/pull/328
* server (trivial tweaks just to make/hack): https://github.com/kiali/kiali/pull/8537


Hints from issue comments:
@jshaughn: @jmazzitelli Is there a reason you did all this but left it all off of the sprint?  Should we move forward?@jmazzitelli: 1. it requires the CRD Schema (this PR is rebased on that work) - and that is not merged yet
2. I'm not sure if we actually need to implement this (downstream does not require it yet, and I'm not sure we want this only upstream)@jmazzitelli: waiting for OLM team to tell us if this is going to be supported on openshift versions before 4.20@jmazzitelli: Actually, I take that back - the OLM support only involves NetworkPolicies for the operator itself. This issue is for the NP's created by the kiali operator to restrict access to the server. This is still Ok to implement now.

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/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/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/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/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/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"])
+			}
+		})
+	}
+}