Repository: kiali/kiali
Pull request #8416: get logging in tracing code
Author: login: jmazzitelli
Created: 2025-05-13T20:13:23Z | Merged: 2025-05-14T12:33:18Z

part of https://github.com/kiali/kiali/issues/8348

This is adding structured logging and metrics to the tracing code for better diagnostics during debugging.

This requires adding context.Context to many functions.


Diff:
diff --git a/business/apps.go b/business/apps.go
index 7d25efce14..2b9cdbc2ea 100644
--- a/business/apps.go
+++ b/business/apps.go
@@ -409,7 +409,7 @@ func (in *AppService) GetAppDetails(ctx context.Context, criteria AppCriteria) (
 	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 {
-			log.Errorf("Error fetching Health in namespace %s for app %s: %s", criteria.Namespace, criteria.AppName, err)
+			log.FromContext(ctx).Error().Msgf("Error fetching Health in namespace [%s] for app [%s]: %v", criteria.Namespace, criteria.AppName, err)
 		}
 	}
 	appInstance.IsAmbient = isAmbient
@@ -528,7 +528,7 @@ func (in *AppService) GetAppTracingName(ctx context.Context, cluster, namespace,
 	// Fetch and build app
 	appDetails, err := in.GetAppDetails(ctx, criteria)
 	if err != nil {
-		log.Errorf("Error for getting tracing name for app %s: %s", app, err)
+		log.FromContext(ctx).Error().Msgf("Error for getting tracing name for app [%s]: %v", app, err)
 		return tracingName
 	}
 	for _, wk := range appDetails.Workloads {
diff --git a/business/istio_status.go b/business/istio_status.go
index 9e52b27361..497b31a172 100644
--- a/business/istio_status.go
+++ b/business/istio_status.go
@@ -392,7 +392,7 @@ func (iss *IstioStatusService) getTracingStatus(cluster string, name string, ena
 
 	status := kubernetes.ComponentHealthy
 
-	accessible, err := iss.tracing.GetStatus()
+	accessible, err := iss.tracing.GetStatus(context.TODO())
 	if !accessible {
 		log.Errorf("Error fetching availability of the tracing service: %v", err)
 		status = kubernetes.ComponentUnreachable
diff --git a/business/istio_status_test.go b/business/istio_status_test.go
index 0ac849b632..c0c1c300f8 100644
--- a/business/istio_status_test.go
+++ b/business/istio_status_test.go
@@ -761,13 +761,13 @@ func assertNotPresent(assert *assert.Assertions, icsl kubernetes.IstioComponentS
 
 func mockJaeger() tracing.ClientInterface {
 	j := new(tracingtest.TracingClientMock)
-	j.On("GetServiceStatus").Return(true, nil)
+	j.On("GetServiceStatus", context.TODO()).Return(true, nil)
 	return j
 }
 
 func mockFailingJaeger() tracing.ClientInterface {
 	j := new(tracingtest.TracingClientMock)
-	j.On("GetServiceStatus").Return(false, errors.New("error connecting with tracing service"))
+	j.On("GetServiceStatus", context.TODO()).Return(false, errors.New("error connecting with tracing service"))
 	return j
 }
 
diff --git a/business/namespaces.go b/business/namespaces.go
index 523412f8ce..1ef779805b 100644
--- a/business/namespaces.go
+++ b/business/namespaces.go
@@ -296,7 +296,7 @@ func (in *NamespaceService) GetClusterNamespace(ctx context.Context, namespace s
 
 	// Refresh namespace in cache since we've just fetched it from the API.
 	if _, err := in.GetClusterNamespaces(ctx, cluster); err != nil {
-		log.Errorf("Unable to refresh cache for cluster [%s]: %s", cluster, err)
+		log.FromContext(ctx).Error().Msgf("Unable to refresh cache for cluster [%s]: %s", cluster, err)
 	}
 
 	return &result, nil
diff --git a/business/tracing.go b/business/tracing.go
index 1b42f0793e..7443d0af83 100644
--- a/business/tracing.go
+++ b/business/tracing.go
@@ -50,12 +50,12 @@ func (in *TracingService) client() (tracing.ClientInterface, error) {
 	return in.tracing, nil
 }
 
-func (in *TracingService) getFilteredSpans(ns string, app models.TracingName, query models.TracingQuery, filter SpanFilter) ([]model.TracingSpan, error) {
-	r, err := in.GetAppTraces(ns, app.Lookup, app.Lookup, query)
+func (in *TracingService) getFilteredSpans(ctx context.Context, ns string, app models.TracingName, query models.TracingQuery, filter SpanFilter) ([]model.TracingSpan, error) {
+	r, err := in.GetAppTraces(ctx, ns, app.Lookup, app.Lookup, query)
 	if err != nil {
 		return []model.TracingSpan{}, err
 	}
-	spans := tracesToSpans(app, r, filter, in.conf)
+	spans := tracesToSpans(ctx, app, r, filter, in.conf)
 
 	return spans, nil
 }
@@ -65,9 +65,9 @@ func (in *TracingService) GetAppSpans(ctx context.Context, cluster, ns, app stri
 	tracingName := in.app.GetAppTracingName(ctx, cluster, ns, app)
 	var waypointFilter SpanFilter
 	if tracingName.Lookup != app {
-		waypointFilter = operationSpanFilter(ns, app)
+		waypointFilter = operationSpanFilter(ctx, ns, app)
 	}
-	return in.getFilteredSpans(ns, tracingName, query, waypointFilter)
+	return in.getFilteredSpans(ctx, ns, tracingName, query, waypointFilter)
 }
 
 func (in *TracingService) GetServiceSpans(ctx context.Context, ns, service string, query models.TracingQuery) ([]model.TracingSpan, error) {
@@ -87,17 +87,17 @@ func (in *TracingService) GetServiceSpans(ctx context.Context, ns, service strin
 	var postFilter SpanFilter
 	// Run post-filter only for service != app
 	if app.Lookup != service {
-		postFilter = operationSpanFilter(ns, service)
+		postFilter = operationSpanFilter(ctx, ns, service)
 	}
-	return in.getFilteredSpans(ns, app, query, postFilter)
+	return in.getFilteredSpans(ctx, ns, app, query, postFilter)
 }
 
-func operationSpanFilter(ns, service string) SpanFilter {
+func operationSpanFilter(ctx context.Context, ns, service string) SpanFilter {
 	fqService := util.BuildNameNSKey(service, ns)
 	// Filter out app spans based on operation name.
 	// For envoy traces, operation name is like "service-name.namespace.svc.cluster.local:8000/*"
 	return func(span *jaegerModels.Span) bool {
-		log.Tracef("[Tracing] operationSpanFilter '%s' has prefix '%s'", span.OperationName, fqService)
+		log.FromContext(ctx).Trace().Msgf("operationSpanFilter [%s] has prefix [%s]", span.OperationName, fqService)
 		return strings.HasPrefix(span.OperationName, fqService)
 	}
 }
@@ -116,28 +116,28 @@ func (in *TracingService) GetWorkloadSpans(ctx context.Context, ns, workload str
 	if err != nil {
 		return nil, err
 	}
-	return in.getFilteredSpans(ns, tracingName, query, wkdSpanFilter(ns, tracingName))
+	return in.getFilteredSpans(ctx, ns, tracingName, query, wkdSpanFilter(ctx, ns, tracingName))
 }
 
-func wkdSpanFilter(ns string, tracingName models.TracingName) SpanFilter {
+func wkdSpanFilter(ctx context.Context, ns string, tracingName models.TracingName) SpanFilter {
 	// Filter out app traces based on the node_id tag, that contains workload information.
 	return func(span *jaegerModels.Span) bool {
-		return spanMatchesWorkload(span, ns, tracingName)
+		return spanMatchesWorkload(ctx, span, ns, tracingName)
 	}
 }
 
 // GetAppTraces returns the traces for an app
 // TracingName is the name to be used to query the tracing backend (Using the waypoint name in Ambient)
 // App name is the name to filter the traces (When different)
-func (in *TracingService) GetAppTraces(ns, tracingName, app string, query models.TracingQuery) (*model.TracingResponse, error) {
+func (in *TracingService) GetAppTraces(ctx context.Context, ns, tracingName, app string, query models.TracingQuery) (*model.TracingResponse, error) {
 	client, err := in.client()
 	if err != nil {
 		return nil, err
 	}
-	r, err := client.GetAppTraces(ns, tracingName, query)
+	r, err := client.GetAppTraces(ctx, ns, tracingName, query)
 	if tracingName != app {
 		// Filter by app
-		filter := operationSpanFilter(ns, app)
+		filter := operationSpanFilter(ctx, ns, app)
 		traces := []jaegerModels.Trace{}
 		for _, trace := range r.Data {
 			for _, span := range trace.Spans {
@@ -171,22 +171,24 @@ func (in *TracingService) GetServiceTraces(ctx context.Context, ns, service stri
 	)
 	defer end()
 
+	zl := log.FromContext(ctx)
+
 	app, err := in.svc.GetServiceTracingName(ctx, query.Cluster, ns, service)
 	if err != nil {
 		return nil, err
 	}
 	if app.Lookup == service {
 		// No post-filtering
-		log.Tracef("[Tracing] GetServiceTraces '%s' for service '%s'", app.Lookup, service)
-		return in.GetAppTraces(ns, service, app.Lookup, query)
+		zl.Trace().Msgf("GetServiceTraces [%s] for service [%s]", app.Lookup, service)
+		return in.GetAppTraces(ctx, ns, service, app.Lookup, query)
 	}
 
-	log.Tracef("[Tracing] GetServiceTraces '%s' for service '%s'", app.Lookup, service)
-	r, err := in.GetAppTraces(ns, app.Lookup, service, query)
+	zl.Trace().Msgf("GetServiceTraces [%s] for service [%s]", app.Lookup, service)
+	r, err := in.GetAppTraces(ctx, ns, app.Lookup, service, query)
 	if r != nil && err == nil {
 		// Filter out app traces based on operation name.
 		// For envoy traces, operation name is like "service-name.namespace.svc.cluster.local:8000/*"
-		filter := operationSpanFilter(ns, service)
+		filter := operationSpanFilter(ctx, ns, service)
 		traces := []jaegerModels.Trace{}
 		for _, trace := range r.Data {
 			for _, span := range trace.Spans {
@@ -220,12 +222,12 @@ func (in *TracingService) GetWorkloadTraces(ctx context.Context, ns, workload st
 		return nil, err
 	}
 
-	r, err := in.GetAppTraces(ns, app.Lookup, app.App, query)
+	r, err := in.GetAppTraces(ctx, ns, app.Lookup, app.App, query)
 	// Filter out app traces based on the node_id tag, that contains workload information.
 	if r != nil && err == nil {
 		traces := []jaegerModels.Trace{}
 		for _, trace := range r.Data {
-			if matchesWorkload(&trace, ns, app) {
+			if matchesWorkload(ctx, &trace, ns, app) {
 				traces = append(traces, trace)
 			}
 		}
@@ -234,36 +236,36 @@ func (in *TracingService) GetWorkloadTraces(ctx context.Context, ns, workload st
 	return r, err
 }
 
-func (in *TracingService) GetTraceDetail(traceID string) (trace *model.TracingSingleTrace, err error) {
+func (in *TracingService) GetTraceDetail(ctx context.Context, traceID string) (trace *model.TracingSingleTrace, err error) {
 	client, err := in.client()
 	if err != nil {
 		return nil, err
 	}
-	return client.GetTraceDetail(traceID)
+	return client.GetTraceDetail(ctx, traceID)
 }
 
-func (in *TracingService) GetErrorTraces(ns, app string, duration time.Duration) (errorTraces int, err error) {
+func (in *TracingService) GetErrorTraces(ctx context.Context, ns, app string, duration time.Duration) (errorTraces int, err error) {
 	client, err := in.client()
 	if err != nil {
 		return 0, err
 	}
-	return client.GetErrorTraces(ns, app, duration)
+	return client.GetErrorTraces(ctx, ns, app, duration)
 }
 
-func (in *TracingService) GetStatus() (accessible bool, err error) {
+func (in *TracingService) GetStatus(ctx context.Context) (accessible bool, err error) {
 	client, err := in.client()
 	if err != nil {
 		return false, err
 	}
-	return client.GetServiceStatus()
+	return client.GetServiceStatus(ctx)
 }
 
-func matchesWorkload(trace *jaegerModels.Trace, namespace string, tracingName models.TracingName) bool {
+func matchesWorkload(ctx context.Context, trace *jaegerModels.Trace, namespace string, tracingName models.TracingName) bool {
 	for _, span := range trace.Spans {
 		if process, ok := trace.Processes[span.ProcessID]; ok {
 			span.Process = &process
 		}
-		if spanMatchesWorkload(&span, namespace, tracingName) {
+		if spanMatchesWorkload(ctx, &span, namespace, tracingName) {
 			return true
 		}
 	}
@@ -272,12 +274,14 @@ func matchesWorkload(trace *jaegerModels.Trace, namespace string, tracingName mo
 
 // spanMatchesWorkload matches a span based on a node id or the hostname
 // For Ambient, as the trace is reported by the Waypoint proxy, a match based on the app is done
-func spanMatchesWorkload(span *jaegerModels.Span, namespace string, tracingName models.TracingName) bool {
+func spanMatchesWorkload(ctx context.Context, span *jaegerModels.Span, namespace string, tracingName models.TracingName) bool {
+	zl := log.FromContext(ctx)
+
 	// If the workload has a waypoint, the span won't match, but the operation name can
 	// When the workload has a waypoint, the operation name is filtered by the service
 	if tracingName.WaypointName != "" {
 		op := fmt.Sprintf("%s.%s", tracingName.App, namespace)
-		log.Tracef("[Tracing] spanMatchesWorkload '%s' has prefix '%s'", span.OperationName, op)
+		zl.Trace().Msgf("spanMatchesWorkload [%s] has prefix [%s] (waypoint name)", span.OperationName, op)
 		return strings.HasPrefix(span.OperationName, op)
 	}
 	// For envoy traces, with a workload named "ai-locals", node_id is like:
@@ -287,7 +291,7 @@ func spanMatchesWorkload(span *jaegerModels.Span, namespace string, tracingName
 			if v, ok := tag.Value.(string); ok {
 				parts := strings.Split(v, "~")
 				if len(parts) >= 3 {
-					log.Tracef("[Tracing] spanMatchesWorkload '%s' HasPrefix '%s' and suffix %s", parts[2], tracingName.Workload, namespace)
+					zl.Trace().Msgf("spanMatchesWorkload [%s] has prefix [%s] and suffix [%s]", parts[2], tracingName.Workload, namespace)
 				}
 				if len(parts) >= 3 && strings.HasPrefix(parts[2], tracingName.Workload) && strings.HasSuffix(parts[2], namespace) {
 					return true
@@ -297,7 +301,7 @@ func spanMatchesWorkload(span *jaegerModels.Span, namespace string, tracingName
 		// For Tempo Traces
 		if tag.Key == "hostname" {
 			if v, ok := tag.Value.(string); ok {
-				log.Tracef("[Tracing] spanMatchesWorkload '%s' HasPrefix '%s'", v, tracingName.Workload)
+				zl.Trace().Msgf("spanMatchesWorkload [%s] has prefix [%s] (a)", v, tracingName.Workload)
 				if strings.HasPrefix(v, tracingName.Workload) {
 					return true
 				}
@@ -309,7 +313,7 @@ func spanMatchesWorkload(span *jaegerModels.Span, namespace string, tracingName
 		for _, tag := range span.Process.Tags {
 			if tag.Key == "hostname" {
 				if v, ok := tag.Value.(string); ok {
-					log.Tracef("[Tracing] spanMatchesWorkload '%s' HasPrefix '%s'", v, tracingName.Workload)
+					zl.Trace().Msgf("spanMatchesWorkload [%s] has prefix [%s] (b)", v, tracingName.Workload)
 					if strings.HasPrefix(v, tracingName.Workload) {
 						return true
 					}
@@ -320,7 +324,7 @@ func spanMatchesWorkload(span *jaegerModels.Span, namespace string, tracingName
 	return false
 }
 
-func tracesToSpans(app models.TracingName, r *model.TracingResponse, filter SpanFilter, conf *config.Config) []model.TracingSpan {
+func tracesToSpans(ctx context.Context, app models.TracingName, r *model.TracingResponse, filter SpanFilter, conf *config.Config) []model.TracingSpan {
 	spans := []model.TracingSpan{}
 	for _, trace := range r.Data {
 		if app.WaypointName != "" {
@@ -371,6 +375,6 @@ func tracesToSpans(app models.TracingName, r *model.TracingResponse, filter Span
 			}
 		}
 	}
-	log.Tracef("[Tracing] Found %d spans in the %d traces for app %s", len(spans), len(r.Data), app)
+	log.FromContext(ctx).Trace().Msgf("Found [%d] spans in the [%d] traces for app [%s]", len(spans), len(r.Data), app)
 	return spans
 }
diff --git a/business/tracing_test.go b/business/tracing_test.go
index 6159766ca6..6d58ed7e4b 100644
--- a/business/tracing_test.go
+++ b/business/tracing_test.go
@@ -1,6 +1,7 @@
 package business
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -124,11 +125,11 @@ var waypointTrace = jaegerModels.Trace{
 func TestMatchingWorkload(t *testing.T) {
 	assert := assert.New(t)
 	tracingName := models.TracingName{App: "some-workload", Workload: "some-workload", Lookup: "some-workload"}
-	assert.False(matchesWorkload(&trace1, "default", tracingName))
+	assert.False(matchesWorkload(context.Background(), &trace1, "default", tracingName))
 	tracingNameReviews := models.TracingName{App: "reviews", Workload: "reviews", Lookup: "reviews"}
-	assert.True(matchesWorkload(&trace1, "default", tracingNameReviews))
+	assert.True(matchesWorkload(context.Background(), &trace1, "default", tracingNameReviews))
 	tracingNamePod := models.TracingName{App: "my-pod", Workload: "my-pod", Lookup: "my-pod"}
-	assert.True(matchesWorkload(&trace1, "default", tracingNamePod))
+	assert.True(matchesWorkload(context.Background(), &trace1, "default", tracingNamePod))
 }
 
 func TestTracesToSpanWithoutFilter(t *testing.T) {
@@ -139,7 +140,7 @@ func TestTracesToSpanWithoutFilter(t *testing.T) {
 		TracingServiceName: "reviews.default",
 	}
 	reviewsTracingName := models.TracingName{App: "reviews", Lookup: "reviews"}
-	spans := tracesToSpans(reviewsTracingName, &r, nil, config.NewConfig())
+	spans := tracesToSpans(context.Background(), reviewsTracingName, &r, nil, config.NewConfig())
 	assert.Len(spans, 2)
 	assert.Equal("t1_process_1", string(spans[0].ProcessID))
 	assert.Equal("t2_process_1", string(spans[1].ProcessID))
@@ -149,7 +150,7 @@ func TestTracesToSpanWithoutFilter(t *testing.T) {
 		TracingServiceName: "rating.default",
 	}
 	ratingsTracingName := models.TracingName{App: "rating", Lookup: "rating"}
-	spans = tracesToSpans(ratingsTracingName, &r, nil, config.NewConfig())
+	spans = tracesToSpans(context.Background(), ratingsTracingName, &r, nil, config.NewConfig())
 	assert.Len(spans, 2)
 	assert.Equal("t2_process_2", string(spans[0].ProcessID))
 	assert.Equal("t2_process_3", string(spans[1].ProcessID))
@@ -163,7 +164,7 @@ func TestTracesToSpanWithServiceFilter(t *testing.T) {
 		TracingServiceName: "reviews.default",
 	}
 	reviewsTracingName := models.TracingName{App: "reviews", Lookup: "reviews"}
-	spans := tracesToSpans(reviewsTracingName, &r, operationSpanFilter("default", "reviews"), config.NewConfig())
+	spans := tracesToSpans(context.Background(), reviewsTracingName, &r, operationSpanFilter(context.Background(), "default", "reviews"), config.NewConfig())
 	assert.Len(spans, 2)
 	assert.Equal("t1_process_1", string(spans[0].ProcessID))
 	assert.Equal("t2_process_1", string(spans[1].ProcessID))
@@ -173,7 +174,7 @@ func TestTracesToSpanWithServiceFilter(t *testing.T) {
 		TracingServiceName: "rating.default",
 	}
 	ratingsTracingName := models.TracingName{App: "rating", Lookup: "rating"}
-	spans = tracesToSpans(ratingsTracingName, &r, operationSpanFilter("default", "rating"), config.NewConfig())
+	spans = tracesToSpans(context.Background(), ratingsTracingName, &r, operationSpanFilter(context.Background(), "default", "rating"), config.NewConfig())
 	assert.Len(spans, 1)
 	assert.Equal("t2_process_2", string(spans[0].ProcessID))
 }
@@ -186,7 +187,7 @@ func TestTracesToSpanWithWorkloadFilter(t *testing.T) {
 		TracingServiceName: "reviews.default",
 	}
 	tracingName := models.TracingName{App: "reviews", Workload: "reviews", Lookup: "reviews"}
-	spans := tracesToSpans(tracingName, &r, wkdSpanFilter("default", tracingName), config.NewConfig())
+	spans := tracesToSpans(context.Background(), tracingName, &r, wkdSpanFilter(context.Background(), "default", tracingName), config.NewConfig())
 	assert.Len(spans, 2)
 	assert.Equal("t1_process_1", string(spans[0].ProcessID))
 	assert.Equal("t2_process_1", string(spans[1].ProcessID))
@@ -196,7 +197,7 @@ func TestTracesToSpanWithWorkloadFilter(t *testing.T) {
 		TracingServiceName: "rating.default",
 	}
 	tracingRatingsName := models.TracingName{App: "rating", Workload: "rating-v2", Lookup: "rating"}
-	spans = tracesToSpans(tracingRatingsName, &r, wkdSpanFilter("default", tracingRatingsName), config.NewConfig())
+	spans = tracesToSpans(context.Background(), tracingRatingsName, &r, wkdSpanFilter(context.Background(), "default", tracingRatingsName), config.NewConfig())
 	assert.Len(spans, 2)
 	assert.Equal("t2_process_2", string(spans[0].ProcessID))
 	assert.Equal("t2_process_3", string(spans[1].ProcessID))
@@ -210,7 +211,7 @@ func TestTracesToSpanWaypointWithWorkloadFilter(t *testing.T) {
 		TracingServiceName: "waypoint.default",
 	}
 	tracingName := models.TracingName{App: "reviews", Workload: "reviews", Lookup: "waypoint", WaypointName: "waypoint"}
-	spans := tracesToSpans(tracingName, &r, wkdSpanFilter("default", tracingName), config.NewConfig())
+	spans := tracesToSpans(context.Background(), tracingName, &r, wkdSpanFilter(context.Background(), "default", tracingName), config.NewConfig())
 	assert.Len(spans, 1)
 	// Process is empty here, because the span we are looking for is the service and the process is the waypoint
 }
diff --git a/handlers/tracing.go b/handlers/tracing.go
index efbd61dc14..97668df04f 100644
--- a/handlers/tracing.go
+++ b/handlers/tracing.go
@@ -16,6 +16,7 @@ import (
 	"github.com/kiali/kiali/grafana"
 	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/tracing"
@@ -60,6 +61,9 @@ func AppTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -77,7 +81,7 @@ func AppTraces(
 		}
 		cluster := clusterNameFromQuery(conf, r.URL.Query())
 		tracingName := business.App.GetAppTracingName(r.Context(), cluster, namespace, app)
-		traces, err := business.Tracing.GetAppTraces(namespace, tracingName.Lookup, app, q)
+		traces, err := business.Tracing.GetAppTraces(r.Context(), namespace, tracingName.Lookup, app, q)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -97,6 +101,9 @@ func ServiceTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -130,6 +137,9 @@ func WorkloadTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -164,6 +174,9 @@ func ErrorTraces(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -179,7 +192,7 @@ func ErrorTraces(
 			RespondWithError(w, http.StatusBadRequest, "Cannot parse parameter 'duration': "+err.Error())
 			return
 		}
-		traces, err := business.Tracing.GetErrorTraces(namespace, app, time.Second*time.Duration(conv))
+		traces, err := business.Tracing.GetErrorTraces(r.Context(), namespace, app, time.Second*time.Duration(conv))
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -199,6 +212,9 @@ func TraceDetails(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -206,7 +222,7 @@ func TraceDetails(
 		}
 		params := mux.Vars(r)
 		traceID := params["traceID"]
-		trace, err := business.Tracing.GetTraceDetail(traceID)
+		trace, err := business.Tracing.GetTraceDetail(r.Context(), traceID)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -232,6 +248,9 @@ func AppSpans(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -269,6 +288,9 @@ func ServiceSpans(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
@@ -306,6 +328,9 @@ func WorkloadSpans(
 	discovery *istio.Discovery,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		// prepare the logger in a context, and replace the request context with ours that has our logger in it
+		r = log.AddGroupToLoggerInRequestContext(r, log.TracingLogName)
+
 		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
 		if err != nil {
 			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
diff --git a/log/log_names.go b/log/log_names.go
index 25432d8e23..53428e23e6 100644
--- a/log/log_names.go
+++ b/log/log_names.go
@@ -8,4 +8,5 @@ var (
 	KialiCacheLogName    = "kialiCache"
 	PromCacheLogName     = "promCache"
 	PrometheusLogName    = "prometheus"
+	TracingLogName       = "tracing"
 )
diff --git a/tracing/client.go b/tracing/client.go
index 091c80ba58..c9b5fbdef3 100644
--- a/tracing/client.go
+++ b/tracing/client.go
@@ -36,24 +36,24 @@ const (
 
 // ClientInterface for mocks (only mocked function are necessary here)
 type ClientInterface interface {
-	GetAppTraces(ns, app string, query models.TracingQuery) (traces *model.TracingResponse, err error)
-	GetTraceDetail(traceId string) (*model.TracingSingleTrace, error)
-	GetErrorTraces(ns, app string, duration time.Duration) (errorTraces int, err error)
-	GetServiceStatus() (available bool, err error)
+	GetAppTraces(ctx context.Context, ns, app string, query models.TracingQuery) (traces *model.TracingResponse, err error)
+	GetTraceDetail(ctx context.Context, traceId string) (*model.TracingSingleTrace, error)
+	GetErrorTraces(ctx context.Context, ns, app string, duration time.Duration) (errorTraces int, err error)
+	GetServiceStatus(ctx context.Context) (available bool, err error)
 }
 
 // HTTPClientInterface for Mocks, also for Tempo or Jaeger
 type HTTPClientInterface interface {
-	GetAppTracesHTTP(client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error)
-	GetTraceDetailHTTP(client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error)
-	GetServiceStatusHTTP(client http.Client, baseURL *url.URL) (bool, error)
+	GetAppTracesHTTP(ctx context.Context, client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error)
+	GetTraceDetailHTTP(ctx context.Context, client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error)
+	GetServiceStatusHTTP(ctx context.Context, client http.Client, baseURL *url.URL) (bool, error)
 }
 
 // GRPCClientInterface for Mocks, also for Tempo or Jaeger
 type GRPCClientInterface interface {
-	FindTraces(context context.Context, app string, q models.TracingQuery) (response *model.TracingResponse, err error)
-	GetTrace(context context.Context, traceID string) (*model.TracingSingleTrace, error)
-	GetServices(context context.Context) (bool, error)
+	FindTraces(ctx context.Context, app string, q models.TracingQuery) (response *model.TracingResponse, err error)
+	GetTrace(ctx context.Context, traceID string) (*model.TracingSingleTrace, error)
+	GetServices(ctx context.Context) (bool, error)
 }
 
 // Client for Tracing API.
@@ -63,7 +63,7 @@ type Client struct {
 	grpcClient        GRPCClientInterface
 	httpClient        http.Client
 	baseURL           *url.URL
-	ctx               context.Context
+	customHeaders     map[string]string
 }
 
 type basicAuth struct {
@@ -87,17 +87,23 @@ func NewClient(ctx context.Context, conf *config.Config, token string) (*Client,
 		client *Client
 		err    error
 	)
+
+	// prepare the client logger and put it in the context
+	// this context and logger is only used during the instantiation process
+	zl := log.WithGroup(log.TracingLogName)
+	ctx = log.ToContext(ctx, zl)
+
 	retryErr := wait.PollUntilContextCancel(ctx, newClientRetryInterval, true, func(ctx context.Context) (bool, error) {
 		client, err = newClient(ctx, conf, token)
 		if err != nil {
-			log.Errorf("Error creating tracing client: %v. Retrying in %s", err, newClientRetryInterval)
+			zl.Error().Msgf("Error creating tracing client: [%v]. Retrying in [%s]", err, newClientRetryInterval)
 			return false, nil
 		}
 
 		return true, nil
 	})
 	if retryErr != nil {
-		log.Errorf("Error creating tracing client: %v. Will not retry.", err)
+		zl.Error().Msgf("Error creating tracing client: [%v]. Will not retry.", err)
 		return nil, err
 	}
 
@@ -109,6 +115,9 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 	if !cfgTracing.Enabled {
 		return nil, errors.New("tracing is not enabled")
 	}
+
+	zl := log.FromContext(ctx)
+
 	var httpTracingClient HTTPClientInterface
 	auth := cfgTracing.Auth
 	if auth.UseKialiToken {
@@ -120,7 +129,7 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 		u, errParse = url.Parse(cfgTracing.ExternalURL)
 	}
 	if errParse != nil {
-		log.Errorf("Error parsing Tracing URL: %s", errParse)
+		zl.Error().Msgf("Error parsing Tracing URL: %s", errParse)
 		return nil, errParse
 	}
 
@@ -131,16 +140,11 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 	}
 	opts, err := grpcutil.GetAuthDialOptions(u.Scheme == "https", &auth)
 	if err != nil {
-		log.Errorf("Error while building GRPC dial options: %v", err)
+		zl.Error().Msgf("Error while building GRPC dial options: %v", err)
 		return nil, err
 	}
 	address := u.Hostname() + ":" + port
-	log.Tracef("%s GRPC client info: address=%s, auth.type=%s", cfgTracing.Provider, address, auth.Type)
-
-	if len(cfgTracing.CustomHeaders) > 0 {
-		log.Tracef("Adding [%v] custom headers to Tracing client", len(cfgTracing.CustomHeaders))
-		ctx = metadata.NewOutgoingContext(ctx, metadata.New(cfgTracing.CustomHeaders))
-	}
+	zl.Trace().Msgf("[%s] GRPC client info: address=[%s], auth.type=[%s]", cfgTracing.Provider, address, auth.Type)
 
 	if cfgTracing.UseGRPC && cfgTracing.Provider != config.TempoProvider {
 
@@ -162,28 +166,28 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 			if err != nil {
 				return nil, err
 			}
-			log.Infof("Create %s GRPC client %s", cfgTracing.Provider, address)
-			return &Client{httpTracingClient: httpTracingClient, grpcClient: client, ctx: ctx}, nil
+			zl.Info().Msgf("Create [%s] GRPC client. address=[%s]", cfgTracing.Provider, address)
+			return &Client{httpTracingClient: httpTracingClient, grpcClient: client, customHeaders: cfgTracing.CustomHeaders}, nil
 		} else {
-			log.Errorf("Error creating client %s", err.Error())
+			zl.Error().Msgf("Error creating client: %v", err)
 			return nil, nil
 		}
 
 	} else {
 		// Legacy HTTP client
-		log.Tracef("Using legacy HTTP client for Tracing: url=%v, auth.type=%s", u, auth.Type)
+		zl.Trace().Msgf("Using legacy HTTP client for Tracing: url=[%v], auth.type=[%s]", u, auth.Type)
 		timeout := time.Duration(config.Get().ExternalServices.Tracing.QueryTimeout) * time.Second
 		transport, err := httputil.CreateTransport(&auth, &http.Transport{}, timeout, cfgTracing.CustomHeaders)
 		if err != nil {
 			return nil, err
 		}
 		client := http.Client{Transport: transport, Timeout: timeout}
-		log.Infof("Create Tracing HTTP client %s", u)
+		zl.Info().Msgf("Create Tracing HTTP client [%s]", u)
 
 		if cfgTracing.Provider == config.TempoProvider {
 			httpTracingClient, err = tempo.NewOtelClient(ctx)
 			if err != nil {
-				log.Errorf("Error creating HTTP client %s", err.Error())
+				zl.Error().Msgf("Error creating HTTP client: %v", err)
 				return nil, err
 			}
 
@@ -203,10 +207,10 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 				clientConn, _ := grpc.NewClient(grpcAddress, dialOps...)
 				streamClient, err := tempo.NewgRPCClient(clientConn)
 				if err != nil {
-					log.Errorf("Error creating gRPC Tempo Client %s", err.Error())
+					zl.Error().Msgf("Error creating gRPC Tempo Client: %v", err)
 					return nil, nil
 				}
-				return &Client{httpTracingClient: httpTracingClient, grpcClient: streamClient, httpClient: client, baseURL: u, ctx: ctx}, nil
+				return &Client{httpTracingClient: httpTracingClient, grpcClient: streamClient, httpClient: client, baseURL: u, customHeaders: cfgTracing.CustomHeaders}, nil
 			}
 		} else {
 			httpTracingClient, err = jaeger.NewJaegerClient(client, u)
@@ -214,34 +218,38 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 				return nil, err
 			}
 		}
-		return &Client{httpTracingClient: httpTracingClient, httpClient: client, baseURL: u, ctx: ctx}, nil
+		return &Client{httpTracingClient: httpTracingClient, httpClient: client, baseURL: u, customHeaders: cfgTracing.CustomHeaders}, nil
 	}
 }
 
 // GetAppTraces fetches traces of an app
-func (in *Client) GetAppTraces(namespace, app string, q models.TracingQuery) (*model.TracingResponse, error) {
+func (in *Client) GetAppTraces(ctx context.Context, namespace, app string, q models.TracingQuery) (*model.TracingResponse, error) {
+	ctx = in.prepareContextForClient(ctx)
+
 	serviceName := BuildTracingServiceName(namespace, app)
 	if in.grpcClient == nil {
-		return in.httpTracingClient.GetAppTracesHTTP(in.httpClient, in.baseURL, serviceName, q)
+		return in.httpTracingClient.GetAppTracesHTTP(ctx, in.httpClient, in.baseURL, serviceName, q)
 	}
-	return in.grpcClient.FindTraces(in.ctx, serviceName, q)
+	return in.grpcClient.FindTraces(ctx, serviceName, q)
 }
 
 // GetTraceDetail fetches a specific trace from its ID
-func (in *Client) GetTraceDetail(strTraceID string) (*model.TracingSingleTrace, error) {
+func (in *Client) GetTraceDetail(ctx context.Context, strTraceID string) (*model.TracingSingleTrace, error) {
+	ctx = in.prepareContextForClient(ctx)
+
 	cfg := config.Get()
 	if in.grpcClient == nil || cfg.ExternalServices.Tracing.Provider == config.TempoProvider {
 		if in.httpTracingClient != nil {
-			return in.httpTracingClient.GetTraceDetailHTTP(in.httpClient, in.baseURL, strTraceID)
+			return in.httpTracingClient.GetTraceDetailHTTP(ctx, in.httpClient, in.baseURL, strTraceID)
 		} else {
 			return nil, fmt.Errorf("error getting trace details")
 		}
 	}
-	return in.grpcClient.GetTrace(in.ctx, strTraceID)
+	return in.grpcClient.GetTrace(ctx, strTraceID)
 }
 
 // GetErrorTraces fetches number of traces in error for the given app
-func (in *Client) GetErrorTraces(ns, app string, duration time.Duration) (int, error) {
+func (in *Client) GetErrorTraces(ctx context.Context, ns, app string, duration time.Duration) (int, error) {
 	// Note: grpc vs http switch is performed in subsequent call 'GetAppTraces'
 	now := time.Now()
 	query := models.TracingQuery{
@@ -253,20 +261,22 @@ func (in *Client) GetErrorTraces(ns, app string, duration time.Duration) (int, e
 		query.Tags[key] = value
 	}
 
-	traces, err := in.GetAppTraces(ns, app, query)
+	traces, err := in.GetAppTraces(ctx, ns, app, query)
 	if err != nil {
 		return 0, err
 	}
 	return len(traces.Data), nil
 }
 
-func (in *Client) GetServiceStatus() (bool, error) {
+func (in *Client) GetServiceStatus(ctx context.Context) (bool, error) {
+	ctx = in.prepareContextForClient(ctx)
+
 	// Check Service Status using HTTP when gRPC is not enabled
 	if in.grpcClient == nil {
-		return in.httpTracingClient.GetServiceStatusHTTP(in.httpClient, in.baseURL)
+		return in.httpTracingClient.GetServiceStatusHTTP(ctx, in.httpClient, in.baseURL)
 	}
 
-	return in.grpcClient.GetServices(in.ctx)
+	return in.grpcClient.GetServices(ctx)
 }
 
 func BuildTracingServiceName(namespace, app string) string {
@@ -276,3 +286,13 @@ func BuildTracingServiceName(namespace, app string) string {
 	}
 	return app
 }
+
+// prepareContextForClient puts things in the given context that will be needed by the client to do its job.
+// For example, the custom headers are added to the context so the clients pass them on to the server when making requests.
+func (in *Client) prepareContextForClient(ctx context.Context) context.Context {
+	if len(in.customHeaders) > 0 {
+		log.FromContext(ctx).Trace().Msgf("Adding [%v] custom headers to Tracing client", len(in.customHeaders))
+		ctx = metadata.NewOutgoingContext(ctx, metadata.New(in.customHeaders))
+	}
+	return ctx
+}
diff --git a/tracing/jaeger/grpc_client.go b/tracing/jaeger/grpc_client.go
index 3b22327441..540936b765 100644
--- a/tracing/jaeger/grpc_client.go
+++ b/tracing/jaeger/grpc_client.go
@@ -13,7 +13,6 @@ import (
 	"google.golang.org/protobuf/types/known/timestamppb"
 
 	"github.com/kiali/kiali/config"
-	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/tracing/jaeger/model"
 	jsonConv "github.com/kiali/kiali/tracing/jaeger/model/converter/json"
@@ -51,7 +50,9 @@ func (jc JaegerGRPCClient) FindTraces(ctx context.Context, serviceName string, q
 		},
 	}
 
-	log.Debugf("Jaeger gRPC FindTraces request: %v", findTracesRQ)
+	zl := getLoggerFromContextGRPCJaeger(ctx)
+
+	zl.Debug().Msgf("Jaeger gRPC FindTraces request: %v", findTracesRQ)
 	tracesMap, err := jc.queryTraces(ctx, findTracesRQ)
 
 	if err != nil {
@@ -86,7 +87,7 @@ func (jc JaegerGRPCClient) GetTrace(ctx context.Context, strTraceID string) (*mo
 	if err != nil {
 		return nil, fmt.Errorf("GetTraceDetail, Tracing GRPC client error: %v", err)
 	}
-	tracesMap, err := readSpansStream(stream)
+	tracesMap, err := readSpansStream(ctx, stream)
 	if err != nil {
 		return nil, err
 	}
@@ -99,8 +100,8 @@ func (jc JaegerGRPCClient) GetTrace(ctx context.Context, strTraceID string) (*mo
 }
 
 // GetServices
-func (jc JaegerGRPCClient) GetServices(ctxSrv context.Context) (bool, error) {
-	ctx, cancel := context.WithTimeout(ctxSrv, 4*time.Second)
+func (jc JaegerGRPCClient) GetServices(ctx context.Context) (bool, error) {
+	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
 	defer cancel()
 
 	_, err := jc.JaegergRPCClient.GetServices(ctx, &model.GetServicesRequest{})
@@ -112,14 +113,16 @@ func (jc JaegerGRPCClient) queryTraces(ctx context.Context, findTracesRQ *model.
 	ctx, cancel := context.WithTimeout(ctx, time.Duration(config.Get().ExternalServices.Tracing.QueryTimeout)*time.Second)
 	defer cancel()
 
+	zl := getLoggerFromContextGRPCJaeger(ctx)
+
 	stream, err := jc.JaegergRPCClient.FindTraces(ctx, findTracesRQ)
 	if err != nil {
 		err = fmt.Errorf("GetAppTraces, Tracing GRPC client error: %v", err)
-		log.Error(err.Error())
+		zl.Error().Msg(err.Error())
 		return nil, err
 	}
 
-	tracesMap, err := readSpansStream(stream)
+	tracesMap, err := readSpansStream(ctx, stream)
 
 	return tracesMap, err
 }
@@ -129,22 +132,24 @@ type SpansStreamer interface {
 	grpc.ClientStream
 }
 
-func readSpansStream(stream SpansStreamer) (map[model.TraceID]*model.Trace, error) {
+func readSpansStream(ctx context.Context, stream SpansStreamer) (map[model.TraceID]*model.Trace, error) {
+	zl := getLoggerFromContextGRPCJaeger(ctx)
+
 	tracesMap := make(map[model.TraceID]*model.Trace)
 	for received, err := stream.Recv(); err != io.EOF; received, err = stream.Recv() {
 		if err != nil {
 			if status.Code(err) == codes.DeadlineExceeded {
-				log.Trace("Tracing GRPC client timeout")
+				zl.Trace().Msgf("Tracing GRPC client timeout")
 				break
 			}
-			log.Errorf("jaeger GRPC client, stream error: %v", err)
+			zl.Error().Msgf("jaeger GRPC client, stream error: %v", err)
 			return nil, fmt.Errorf("Tracing GRPC client, stream error: %v", err)
 		}
 		for i, span := range received.Spans {
 			traceId := model.TraceID{}
 			err := traceId.Unmarshal(span.TraceId)
 			if err != nil {
-				log.Errorf("Tracing TraceId unmarshall error: %v", err)
+				zl.Error().Msgf("Tracing TraceId unmarshall error: %v", err)
 				continue
 			}
 			if trace, ok := tracesMap[traceId]; ok {
diff --git a/tracing/jaeger/http_client.go b/tracing/jaeger/http_client.go
index ea9ac75d1b..3ad8fd6512 100644
--- a/tracing/jaeger/http_client.go
+++ b/tracing/jaeger/http_client.go
@@ -1,6 +1,7 @@
 package jaeger
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -11,7 +12,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/tracing/jaeger/model"
 	"github.com/kiali/kiali/util"
@@ -25,13 +25,13 @@ func NewJaegerClient(client http.Client, baseURL *url.URL) (jaegerClient *Jaeger
 	return &JaegerHTTPClient{}, nil
 }
 
-func (jc JaegerHTTPClient) GetAppTracesHTTP(client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error) {
+func (jc JaegerHTTPClient) GetAppTracesHTTP(ctx context.Context, client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error) {
 	url := *baseURL
 	url.Path = path.Join(url.Path, "/api/traces")
 
 	// if cluster exists in tags, use it
-	prepareQuery(&url, serviceName, q)
-	r, err := queryTracesHTTP(client, &url)
+	prepareQuery(ctx, &url, serviceName, q)
+	r, err := queryTracesHTTP(ctx, client, &url)
 
 	if r != nil {
 		r.TracingServiceName = serviceName
@@ -41,20 +41,20 @@ func (jc JaegerHTTPClient) GetAppTracesHTTP(client http.Client, baseURL *url.URL
 	return r, err
 }
 
-func (jc JaegerHTTPClient) GetTraceDetailHTTP(client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error) {
+func (jc JaegerHTTPClient) GetTraceDetailHTTP(ctx context.Context, client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error) {
 	u := *endpoint
 	// /querier/api/traces/<traceid>?mode=xxxx&blockStart=0000&blockEnd=FFFF&start=<start>&end=<end>
 	u.Path = path.Join(u.Path, "/api/traces/"+traceID)
 	resp, code, reqError := makeRequest(client, u.String(), nil)
 	if reqError != nil {
-		log.Errorf("Jaeger query error: %s [code: %d, URL: %v]", reqError, code, u)
+		getLoggerFromContextHTTPJaeger(ctx).Error().Msgf("Jaeger query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return nil, reqError
 	}
 	// Jaeger would return "200 OK" when trace is not found, with an empty response
 	if len(resp) == 0 {
 		return nil, nil
 	}
-	response, err := unmarshal(resp, &u)
+	response, err := unmarshal(ctx, resp, &u)
 	if err != nil {
 		return nil, err
 	}
@@ -67,14 +67,14 @@ func (jc JaegerHTTPClient) GetTraceDetailHTTP(client http.Client, endpoint *url.
 	}, nil
 }
 
-func (jc JaegerHTTPClient) GetServiceStatusHTTP(client http.Client, baseURL *url.URL) (bool, error) {
+func (jc JaegerHTTPClient) GetServiceStatusHTTP(ctx context.Context, client http.Client, baseURL *url.URL) (bool, error) {
 	url := *baseURL
 	url.Path = path.Join(url.Path, "/api/services")
 	_, _, reqError := makeRequest(client, url.String(), nil)
 	return reqError == nil, reqError
 }
 
-func queryTracesHTTP(client http.Client, u *url.URL) (*model.TracingResponse, error) {
+func queryTracesHTTP(ctx context.Context, client http.Client, u *url.URL) (*model.TracingResponse, error) {
 	// HTTP and GRPC requests co-exist, but when minDuration is present, for HTTP it requires a unit (us)
 	// https://github.com/kiali/kiali/issues/3939
 	minDuration := u.Query().Get("minDuration")
@@ -85,22 +85,24 @@ func queryTracesHTTP(client http.Client, u *url.URL) (*model.TracingResponse, er
 	}
 	resp, code, reqError := makeRequest(client, u.String(), nil)
 	if reqError != nil {
-		log.Errorf("Jaeger query error: %s [code: %d, URL: %v]", reqError, code, u)
+		getLoggerFromContextHTTPJaeger(ctx).Error().Msgf("Jaeger query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return &model.TracingResponse{}, reqError
 	}
-	return unmarshal(resp, u)
+	return unmarshal(ctx, resp, u)
 }
 
-func unmarshal(r []byte, u *url.URL) (*model.TracingResponse, error) {
+func unmarshal(ctx context.Context, r []byte, u *url.URL) (*model.TracingResponse, error) {
 	var response model.TracingResponse
 	if errMarshal := json.Unmarshal(r, &response); errMarshal != nil {
-		log.Errorf("Error unmarshalling Jaeger response: %s [URL: %v]", errMarshal, u)
+		getLoggerFromContextHTTPJaeger(ctx).Error().Msgf("Error unmarshalling Jaeger response: %s [URL: %v]", errMarshal, u)
 		return nil, errMarshal
 	}
 	return &response, nil
 }
 
-func prepareQuery(u *url.URL, jaegerServiceName string, query models.TracingQuery) {
+func prepareQuery(ctx context.Context, u *url.URL, jaegerServiceName string, query models.TracingQuery) {
+	zl := getLoggerFromContextHTTPJaeger(ctx)
+
 	q := url.Values{}
 	q.Set("service", jaegerServiceName)
 	q.Set("start", fmt.Sprintf("%d", query.Start.Unix()*time.Second.Microseconds()))
@@ -111,7 +113,7 @@ func prepareQuery(u *url.URL, jaegerServiceName string, query models.TracingQuer
 		// Tags must be json encoded
 		tagsJson, err := json.Marshal(tags)
 		if err != nil {
-			log.Errorf("Jaeger query: error while marshalling tags to json: %v", err)
+			zl.Error().Msgf("Jaeger query: error while marshalling tags to json: %v", err)
 		}
 		q.Set("tags", string(tagsJson))
 	}
@@ -122,7 +124,7 @@ func prepareQuery(u *url.URL, jaegerServiceName string, query models.TracingQuer
 		q.Set("limit", strconv.Itoa(query.Limit))
 	}
 	u.RawQuery = q.Encode()
-	log.Debugf("Prepared Jaeger query: %v", u)
+	zl.Debug().Msgf("Prepared Jaeger query: %v", u)
 }
 
 func makeRequest(client http.Client, endpoint string, body io.Reader) (response []byte, status int, err error) {
diff --git a/tracing/jaeger/logging.go b/tracing/jaeger/logging.go
new file mode 100644
index 0000000000..e0950918e8
--- /dev/null
+++ b/tracing/jaeger/logging.go
@@ -0,0 +1,21 @@
+package jaeger
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCJaeger returns the logger found in the context and adds the type key to refer to this as the gRPC Jaeger logger
+func getLoggerFromContextGRPCJaeger(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("client-type", "gRPC Jaeger").Logger()
+	return &zl
+}
+
+// getLoggerFromContextHTTPJaeger returns the logger found in the context and adds the type key to refer to this as the HTTP Jaeger logger
+func getLoggerFromContextHTTPJaeger(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("client-type", "HTTP Jaeger").Logger()
+	return &zl
+}
diff --git a/tracing/tempo/grpc_client.go b/tracing/tempo/grpc_client.go
index 45bdc9330f..5bb0dd7f59 100644
--- a/tracing/tempo/grpc_client.go
+++ b/tracing/tempo/grpc_client.go
@@ -11,7 +11,6 @@ import (
 	"google.golang.org/grpc/status"
 
 	"github.com/kiali/kiali/config"
-	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/tracing/jaeger/model"
 	jaegerModels "github.com/kiali/kiali/tracing/jaeger/model/json"
@@ -53,7 +52,9 @@ func (jc TempoGRPCClient) FindTraces(ctx context.Context, serviceName string, q
 	selects := []string{"status", ".service_name", ".node_id", ".component", ".upstream_cluster", ".http.method", ".response_flags", "resource.hostname", "name"}
 	trace := TraceQL{operator1: Subquery{queryPart}, operand: AND, operator2: Subquery{}}
 	queryQL := fmt.Sprintf("%s| %s", printOperator(trace), printSelect(selects))
-	log.Debugf("QueryQL %s", queryQL)
+
+	zl := getLoggerFromContextGRPCTempo(ctx)
+	zl.Debug().Msgf("[gRPC Tempo] QueryQL: %s", queryQL)
 
 	sr.Query = queryQL
 	sr.SpansPerSpanSet = 10
@@ -64,11 +65,11 @@ func (jc TempoGRPCClient) FindTraces(ctx context.Context, serviceName string, q
 
 	if err != nil {
 		err = fmt.Errorf("[gRPC Tempo] GetAppTraces, Tracing gRPC client error: %v", err)
-		log.Error(err.Error())
+		zl.Error().Msg(err.Error())
 		return nil, err
 	}
 
-	traces, err := processStream(stream, serviceName)
+	traces, err := processStream(ctx, stream, serviceName)
 
 	if err != nil {
 		return nil, err
@@ -83,8 +84,7 @@ func (jc TempoGRPCClient) FindTraces(ctx context.Context, serviceName string, q
 
 // GetTrace is not implemented by the streaming client
 func (jc TempoGRPCClient) GetTrace(ctx context.Context, strTraceID string) (*model.TracingSingleTrace, error) {
-
-	log.Errorf("[gRPC Tempo] GetTrace is not implemented by the Tempo streaming client")
+	getLoggerFromContextGRPCTempo(ctx).Error().Msgf("[gRPC Tempo] GetTrace is not implemented by the Tempo streaming client")
 	return nil, nil
 
 }
@@ -100,23 +100,24 @@ func (jc TempoGRPCClient) GetServices(ctx context.Context) (bool, error) {
 }
 
 // processStream
-func processStream(stream tempopb.StreamingQuerier_SearchClient, serviceName string) ([]jaegerModels.Trace, error) {
+func processStream(ctx context.Context, stream tempopb.StreamingQuerier_SearchClient, serviceName string) ([]jaegerModels.Trace, error) {
+	zl := getLoggerFromContextGRPCTempo(ctx)
 
 	tracesMap := []jaegerModels.Trace{}
 
 	for received, err := stream.Recv(); err != io.EOF; received, err = stream.Recv() {
 		if err != nil {
 			if status.Code(err) == codes.DeadlineExceeded {
-				log.Trace("Tracing GRPC client timeout")
+				zl.Trace().Msg("[gRPC Tempo] client timeout")
 				break
 			}
-			log.Errorf("[gRPC Tempo] stream error: %v", err)
+			zl.Error().Msgf("[gRPC Tempo] stream error: %v", err)
 			return nil, fmt.Errorf("[gRPC Tempo] Tracing gRPC client, stream error: %v", err)
 		}
 		for _, trace := range received.Traces {
 			batchTrace, err := converter.ConvertTraceMetadata(*trace, serviceName)
 			if err != nil {
-				log.Errorf("[gRPC Tempo] Error getting trace detail for %s: %s", trace.TraceID, err.Error())
+				zl.Error().Msgf("[gRPC Tempo] Error getting trace detail for %s: %s", trace.TraceID, err.Error())
 			} else {
 				tracesMap = append(tracesMap, *batchTrace)
 			}
diff --git a/tracing/tempo/http_client.go b/tracing/tempo/http_client.go
index 5525635085..0d36ab8e12 100644
--- a/tracing/tempo/http_client.go
+++ b/tracing/tempo/http_client.go
@@ -14,7 +14,6 @@ import (
 	"time"
 
 	"github.com/kiali/kiali/config"
-	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/store"
 	"github.com/kiali/kiali/tracing/jaeger/model"
@@ -49,15 +48,15 @@ func NewOtelClient(ctx context.Context) (otelClient *OtelHTTPClient, err error)
 }
 
 // GetAppTracesHTTP search traces
-func (oc *OtelHTTPClient) GetAppTracesHTTP(client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error) {
+func (oc *OtelHTTPClient) GetAppTracesHTTP(ctx context.Context, client http.Client, baseURL *url.URL, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error) {
 	url := *baseURL
 	url.Path = path.Join(url.Path, "/api/search")
 	if q.End.Before(q.Start) {
 		return nil, fmt.Errorf("end time must be greater than start time")
 	}
-	oc.prepareTraceQL(&url, serviceName, q)
+	oc.prepareTraceQL(ctx, &url, serviceName, q)
 
-	r, err := oc.queryTracesHTTP(client, &url, q.Tags["error"])
+	r, err := oc.queryTracesHTTP(ctx, client, &url, q.Tags["error"])
 
 	if r != nil {
 		r.TracingServiceName = serviceName
@@ -66,22 +65,25 @@ func (oc *OtelHTTPClient) GetAppTracesHTTP(client http.Client, baseURL *url.URL,
 }
 
 // GetTraceDetailHTTP get one trace by trace ID
-func (oc *OtelHTTPClient) GetTraceDetailHTTP(client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error) {
+func (oc *OtelHTTPClient) GetTraceDetailHTTP(ctx context.Context, client http.Client, endpoint *url.URL, traceID string) (*model.TracingSingleTrace, error) {
 	u := *endpoint
 	if config.Get().ExternalServices.Tracing.TempoConfig.CacheEnabled {
 		if result, isCached := oc.TempoCache.Get(traceID); isCached {
 			return result, nil
 		}
 	}
+
+	zl := getLoggerFromContextHTTPTempo(ctx)
+
 	u.Path = path.Join(u.Path, "/api/traces/", traceID)
 	resp, code, reqError := makeRequest(client, u.String(), nil)
 	if reqError != nil {
-		log.Errorf("[HTTP Tempo] API Tempo query error: %s [code: %d, URL: %v]", reqError, code, u)
+		zl.Error().Msgf("[HTTP Tempo] API Tempo query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return nil, reqError
 	}
 	if code != 200 {
 		errorMsg := fmt.Sprintf("[HTTP Tempo] Error returning traces: %s", resp)
-		log.Errorf("%s", errorMsg)
+		zl.Error().Msg(errorMsg)
 		var errorTrace []model.StructuredError
 		errorTrace = append(errorTrace, model.StructuredError{TraceID: traceID, Code: code, Msg: errorMsg})
 		return &model.TracingSingleTrace{Errors: errorTrace}, errors.New(errorMsg)
@@ -91,7 +93,7 @@ func (oc *OtelHTTPClient) GetTraceDetailHTTP(client http.Client, endpoint *url.U
 		return nil, errors.New("[HTTP Tempo] empty body response")
 	}
 
-	responseOtel, _ := unmarshalSingleTrace(resp, &u)
+	responseOtel, _ := unmarshalSingleTrace(ctx, resp, &u)
 
 	response, err := convertSingleTrace(responseOtel, traceID)
 	if err != nil {
@@ -113,7 +115,7 @@ func (oc *OtelHTTPClient) GetTraceDetailHTTP(client http.Client, endpoint *url.U
 }
 
 // GetServiceStatusHTTP get service status
-func (oc *OtelHTTPClient) GetServiceStatusHTTP(client http.Client, baseURL *url.URL) (bool, error) {
+func (oc *OtelHTTPClient) GetServiceStatusHTTP(ctx context.Context, client http.Client, baseURL *url.URL) (bool, error) {
 	var u url.URL
 	healthCheckUrl := config.Get().ExternalServices.Tracing.HealthCheckUrl
 	if healthCheckUrl != "" {
@@ -135,7 +137,10 @@ func (oc *OtelHTTPClient) GetServiceStatusHTTP(client http.Client, baseURL *url.
 }
 
 // queryTracesHTTP
-func (oc *OtelHTTPClient) queryTracesHTTP(client http.Client, u *url.URL, error string) (*model.TracingResponse, error) {
+func (oc *OtelHTTPClient) queryTracesHTTP(ctx context.Context, client http.Client, u *url.URL, error string) (*model.TracingResponse, error) {
+
+	zl := getLoggerFromContextHTTPTempo(ctx)
+
 	// HTTP and GRPC requests co-exist, but when minDuration is present, for HTTP it requires a unit (ms)
 	// https://github.com/kiali/kiali/issues/3939
 	minDuration := u.Query().Get("minDuration")
@@ -146,25 +151,25 @@ func (oc *OtelHTTPClient) queryTracesHTTP(client http.Client, u *url.URL, error
 	}
 	resp, code, reqError := makeRequest(client, u.String(), nil)
 	if reqError != nil {
-		log.Errorf("Tempo API query error: %s [code: %d, URL: %v]", reqError, code, u)
+		zl.Error().Msgf("Tempo API query error: %s [code: %d, URL: %v]", reqError, code, u)
 		return &model.TracingResponse{}, reqError
 	}
 	if code != 200 {
 		errorMsg := fmt.Sprintf("[HTTP Tempo] Tempo API query error: %s [code: %d, URL: %v]", resp, code, u)
-		log.Errorf("%s", errorMsg)
+		zl.Error().Msg(errorMsg)
 		return &model.TracingResponse{}, errors.New(errorMsg)
 	}
 	limit, err := strconv.Atoi(u.Query().Get("limit"))
 	if err != nil {
 		limit = 0
 	}
-	response, _ := unmarshal(resp, u)
+	response, _ := unmarshal(ctx, resp, u)
 
-	return oc.transformTrace(response, error, limit)
+	return oc.transformTrace(ctx, response, error, limit)
 }
 
 // transformTrace processes every trace ID and make a request to get all the spans for that trace
-func (oc *OtelHTTPClient) transformTrace(traces *otel.Traces, error string, limit int) (*model.TracingResponse, error) {
+func (oc *OtelHTTPClient) transformTrace(ctx context.Context, traces *otel.Traces, error string, limit int) (*model.TracingResponse, error) {
 	var response model.TracingResponse
 	serviceName := ""
 
@@ -181,7 +186,8 @@ func (oc *OtelHTTPClient) transformTrace(traces *otel.Traces, error string, limi
 			}
 			batchTrace, err := convertBatchTrace(trace, serviceName)
 			if err != nil {
-				log.Errorf("[HTTP Tempo] Error getting trace detail for %s: %s", trace.TraceID, err.Error())
+				zl := getLoggerFromContextHTTPTempo(ctx)
+				zl.Error().Msgf("[HTTP Tempo] Error getting trace detail for %s: %v", trace.TraceID, err)
 			} else {
 				response.Data = append(response.Data, batchTrace)
 			}
@@ -192,20 +198,20 @@ func (oc *OtelHTTPClient) transformTrace(traces *otel.Traces, error string, limi
 	return &response, nil
 }
 
-func unmarshal(r []byte, u *url.URL) (*otel.Traces, error) {
+func unmarshal(ctx context.Context, r []byte, u *url.URL) (*otel.Traces, error) {
 	var response otel.Traces
 	if errMarshal := json.Unmarshal(r, &response); errMarshal != nil {
-		log.Errorf("[HTTP Tempo] Error unmarshalling Tempo API response: %s [URL: %v]", errMarshal, u)
+		getLoggerFromContextHTTPTempo(ctx).Error().Msgf("[HTTP Tempo] Error unmarshalling Tempo API response: %s [URL: %v]", errMarshal, u)
 		return nil, errMarshal
 	}
 
 	return &response, nil
 }
 
-func unmarshalSingleTrace(r []byte, u *url.URL) (*otelModels.Data, error) {
+func unmarshalSingleTrace(ctx context.Context, r []byte, u *url.URL) (*otelModels.Data, error) {
 	var response otelModels.Data
 	if errMarshal := json.Unmarshal(r, &response); errMarshal != nil {
-		log.Errorf("[HTTP Tempo] Error unmarshalling Tempo API Single trace response: %s [URL: %v]", errMarshal, u)
+		getLoggerFromContextHTTPTempo(ctx).Error().Msgf("[HTTP Tempo] Error unmarshalling Tempo API Single trace response: %s [URL: %v]", errMarshal, u)
 		return nil, errMarshal
 	}
 
@@ -254,7 +260,7 @@ func convertSingleTrace(traces *otelModels.Data, id string) (*model.TracingRespo
 }
 
 // prepareTraceQL set the query in TraceQL format
-func (oc *OtelHTTPClient) prepareTraceQL(u *url.URL, tracingServiceName string, query models.TracingQuery) {
+func (oc *OtelHTTPClient) prepareTraceQL(ctx context.Context, u *url.URL, tracingServiceName string, query models.TracingQuery) {
 	q := url.Values{}
 	q.Set("start", fmt.Sprintf("%d", query.Start.Unix()))
 	q.Set("end", fmt.Sprintf("%d", query.End.Unix()))
@@ -288,12 +294,13 @@ func (oc *OtelHTTPClient) prepareTraceQL(u *url.URL, tracingServiceName string,
 		q.Set("limit", strconv.Itoa(query.Limit))
 	}
 	u.RawQuery = q.Encode()
-	log.Debugf("[HTTP Tempo] Prepared Tempo API query: %v", u)
+
+	getLoggerFromContextHTTPTempo(ctx).Debug().Msgf("[HTTP Tempo] Prepared Tempo API query: %v", u)
 }
 
 // GetTraceQLQuery returns the raw query in TraceQL format
-func (oc *OtelHTTPClient) GetTraceQLQuery(u *url.URL, tracingServiceName string, query models.TracingQuery) string {
-	oc.prepareTraceQL(u, tracingServiceName, query)
+func (oc *OtelHTTPClient) GetTraceQLQuery(ctx context.Context, u *url.URL, tracingServiceName string, query models.TracingQuery) string {
+	oc.prepareTraceQL(ctx, u, tracingServiceName, query)
 	return u.RawQuery
 }
 
diff --git a/tracing/tempo/http_client_test.go b/tracing/tempo/http_client_test.go
index 957bff6aee..61c7b267a4 100644
--- a/tracing/tempo/http_client_test.go
+++ b/tracing/tempo/http_client_test.go
@@ -65,7 +65,7 @@ func TestGetTraces(t *testing.T) {
 		Limit:       0,
 		Cluster:     "",
 	}
-	response, err := tempoClient.GetAppTracesHTTP(httpClient, baseUrl, serviceName, q)
+	response, err := tempoClient.GetAppTracesHTTP(context.Background(), httpClient, baseUrl, serviceName, q)
 	assert.Nil(t, err)
 	assert.NotNil(t, response)
 	assert.Equal(t, response.TracingServiceName, serviceName)
@@ -106,7 +106,7 @@ func TestGetAmbientTraces(t *testing.T) {
 		Limit:       0,
 		Cluster:     "",
 	}
-	response, err := tempoClient.GetAppTracesHTTP(httpClient, baseUrl, ambientServiceName, q)
+	response, err := tempoClient.GetAppTracesHTTP(context.Background(), httpClient, baseUrl, ambientServiceName, q)
 	assert.Nil(t, err)
 	assert.NotNil(t, response)
 	assert.Equal(t, response.TracingServiceName, ambientServiceName)
@@ -139,7 +139,7 @@ func TestGetTrace(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotNil(t, tempoClient)
 
-	response, err := tempoClient.GetTraceDetailHTTP(httpClient, baseUrl, "3ba55609c3cde49649cd77d1f9dcd936")
+	response, err := tempoClient.GetTraceDetailHTTP(context.Background(), httpClient, baseUrl, "3ba55609c3cde49649cd77d1f9dcd936")
 	assert.Nil(t, err)
 	assert.NotNil(t, response)
 	assert.Equal(t, response.Data.TraceID, json.TraceID("3ba55609c3cde49649cd77d1f9dcd936"))
@@ -171,7 +171,7 @@ func TestErrorResponse(t *testing.T) {
 		Limit:       0,
 		Cluster:     "",
 	}
-	response, err := tempoClient.GetAppTracesHTTP(httpClient, baseUrl, serviceName, q)
+	response, err := tempoClient.GetAppTracesHTTP(context.Background(), httpClient, baseUrl, serviceName, q)
 	assert.NotNil(t, err)
 	assert.NotNil(t, response)
 	assert.Nil(t, response.Data)
@@ -194,7 +194,7 @@ func TestQuery(t *testing.T) {
 		Limit:       0,
 		Cluster:     "",
 	}
-	query := tempoClient.GetTraceQLQuery(baseUrl, serviceName, q)
+	query := tempoClient.GetTraceQLQuery(context.Background(), baseUrl, serviceName, q)
 	assert.NotNil(t, query)
 	rawQuery, err := url.QueryUnescape(query)
 	assert.Nil(t, err)
@@ -223,7 +223,7 @@ func TestQuery(t *testing.T) {
 		Limit:       0,
 		Cluster:     "",
 	}
-	query2 := tempoClient.GetTraceQLQuery(baseUrl, serviceName, q2)
+	query2 := tempoClient.GetTraceQLQuery(context.Background(), baseUrl, serviceName, q2)
 	assert.NotNil(t, query2)
 	rawQuery2, err2 := url.QueryUnescape(query2)
 	assert.Nil(t, err2)
@@ -233,7 +233,7 @@ func TestQuery(t *testing.T) {
 	// Should contain Cluster tag
 	assert.Contains(t, rawQuery2, ".istio.cluster_id = \"east\"")
 
-	query3 := tempoClient.GetTraceQLQuery(baseUrl, serviceName, q2)
+	query3 := tempoClient.GetTraceQLQuery(context.Background(), baseUrl, serviceName, q2)
 	assert.NotNil(t, query3)
 	rawQuery3, err3 := url.QueryUnescape(query3)
 	assert.Nil(t, err3)
diff --git a/tracing/tempo/logging.go b/tracing/tempo/logging.go
new file mode 100644
index 0000000000..8e9adf266e
--- /dev/null
+++ b/tracing/tempo/logging.go
@@ -0,0 +1,21 @@
+package tempo
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCTempo returns the logger found in the context and adds the type key to refer to this as the gRPC Tempo logger
+func getLoggerFromContextGRPCTempo(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("client-type", "gRPC Tempo").Logger()
+	return &zl
+}
+
+// getLoggerFromContextHTTPTempo returns the logger found in the context and adds the type key to refer to this as the HTTP Tempo logger
+func getLoggerFromContextHTTPTempo(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("client-type", "HTTP Tempo").Logger()
+	return &zl
+}
diff --git a/tracing/tracingtest/mock.go b/tracing/tracingtest/mock.go
index 7e6271aa63..b43774ee2b 100644
--- a/tracing/tracingtest/mock.go
+++ b/tracing/tracingtest/mock.go
@@ -1,6 +1,7 @@
 package tracingtest
 
 import (
+	"context"
 	"time"
 
 	"github.com/stretchr/testify/mock"
@@ -13,22 +14,22 @@ type TracingClientMock struct {
 	mock.Mock
 }
 
-func (j *TracingClientMock) GetAppTraces(ns, app string, query models.TracingQuery) (traces *model.TracingResponse, err error) {
-	args := j.Called(ns, app, query)
+func (j *TracingClientMock) GetAppTraces(ctx context.Context, ns, app string, query models.TracingQuery) (traces *model.TracingResponse, err error) {
+	args := j.Called(ctx, ns, app, query)
 	return args.Get(0).(*model.TracingResponse), args.Error(1)
 }
 
-func (j *TracingClientMock) GetTraceDetail(traceId string) (trace *model.TracingSingleTrace, err error) {
-	args := j.Called(traceId)
+func (j *TracingClientMock) GetTraceDetail(ctx context.Context, traceId string) (trace *model.TracingSingleTrace, err error) {
+	args := j.Called(ctx, traceId)
 	return args.Get(0).(*model.TracingSingleTrace), args.Error(1)
 }
 
-func (j *TracingClientMock) GetErrorTraces(ns string, app string, duration time.Duration) (errorTraces int, err error) {
-	args := j.Called(ns, app, duration)
+func (j *TracingClientMock) GetErrorTraces(ctx context.Context, ns string, app string, duration time.Duration) (errorTraces int, err error) {
+	args := j.Called(ctx, ns, app, duration)
 	return args.Get(0).(int), args.Error(1)
 }
 
-func (j *TracingClientMock) GetServiceStatus() (available bool, err error) {
-	args := j.Called()
+func (j *TracingClientMock) GetServiceStatus(ctx context.Context) (available bool, err error) {
+	args := j.Called(ctx)
 	return args.Get(0).(bool), args.Error(1)
 }


Reviews:
user: login: nrfox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T00:18:56Z
user: login: jmazzitelli
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T07:22:19Z
user: login: nrfox
body: Looks good to me. Just one question about the logging metadata for specific tracing clients.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:37:20Z
user: login: jmazzitelli
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T11:44:44Z
user: login: jmazzitelli
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T11:51:23Z
user: login: nrfox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:11:52Z
user: login: nrfox
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:12:03Z

Review comments:
diff_hunk: @@ -31,7 +31,7 @@ func NewGRPCJaegerClient(cc model.QueryServiceClient) (jaegerClient *JaegerGRPCC
 }
 
 // FindTraces
-func (jc JaegerGRPCClient) FindTraces(ctx context.Context, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error) {
+func (jc JaegerGRPCClient) FindTraces(reqCtx context.Context, clientCtx context.Context, serviceName string, q models.TracingQuery) (response *model.TracingResponse, err error) {
path: tracing/jaeger/grpc_client.go
user: login: nrfox
body: Why are there two contexts?
created_at: 2025-05-14T00:01:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -203,45 +212,45 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 				clientConn, _ := grpc.NewClient(grpcAddress, dialOps...)
 				streamClient, err := tempo.NewgRPCClient(clientConn)
 				if err != nil {
-					log.Errorf("Error creating gRPC Tempo Client %s", err.Error())
+					zl.Error().Msgf("Error creating gRPC Tempo Client: %v", err)
 					return nil, nil
 				}
-				return &Client{httpTracingClient: httpTracingClient, grpcClient: streamClient, httpClient: client, baseURL: u, ctx: ctx}, nil
+				return &Client{httpTracingClient: httpTracingClient, grpcClient: streamClient, httpClient: client, baseURL: u, clientCtx: clientCtx}, nil
 			}
 		} else {
 			httpTracingClient, err = jaeger.NewJaegerClient(client, u)
 			if err != nil {
 				return nil, err
 			}
 		}
-		return &Client{httpTracingClient: httpTracingClient, httpClient: client, baseURL: u, ctx: ctx}, nil
+		return &Client{httpTracingClient: httpTracingClient, httpClient: client, baseURL: u, clientCtx: clientCtx}, nil
 	}
 }
 
 // GetAppTraces fetches traces of an app
-func (in *Client) GetAppTraces(namespace, app string, q models.TracingQuery) (*model.TracingResponse, error) {
+func (in *Client) GetAppTraces(ctx context.Context, namespace, app string, q models.TracingQuery) (*model.TracingResponse, error) {
 	serviceName := BuildTracingServiceName(namespace, app)
 	if in.grpcClient == nil {
-		return in.httpTracingClient.GetAppTracesHTTP(in.httpClient, in.baseURL, serviceName, q)
+		return in.httpTracingClient.GetAppTracesHTTP(ctx, in.httpClient, in.baseURL, serviceName, q)
 	}
-	return in.grpcClient.FindTraces(in.ctx, serviceName, q)
+	return in.grpcClient.FindTraces(ctx, in.clientCtx, serviceName, q)
path: tracing/client.go
user: login: nrfox
body: This would be simpler if we didn't save the context on the struct. The rub here is in trying to combine two loggers right? idk if there's an easy way to do that with zerolog but I'd rather we pass one context and have some kind of `getLogger(ctx)` method that pulls the logger from the context and adds the tracing group each time we want a logger in the `Client` instead of having to pass two contexts. Context is also used for cancellation, timeout, etc. Which one do you use for that and which one do you use for the logger? IMO it's confusing.
created_at: 2025-05-14T00:18:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -203,45 +212,45 @@ func newClient(ctx context.Context, conf *config.Config, token string) (*Client,
 				clientConn, _ := grpc.NewClient(grpcAddress, dialOps...)
 				streamClient, err := tempo.NewgRPCClient(clientConn)
 				if err != nil {
-					log.Errorf("Error creating gRPC Tempo Client %s", err.Error())
+					zl.Error().Msgf("Error creating gRPC Tempo Client: %v", err)
 					return nil, nil
 				}
-				return &Client{httpTracingClient: httpTracingClient, grpcClient: streamClient, httpClient: client, baseURL: u, ctx: ctx}, nil
+				return &Client{httpTracingClient: httpTracingClient, grpcClient: streamClient, httpClient: client, baseURL: u, clientCtx: clientCtx}, nil
 			}
 		} else {
 			httpTracingClient, err = jaeger.NewJaegerClient(client, u)
 			if err != nil {
 				return nil, err
 			}
 		}
-		return &Client{httpTracingClient: httpTracingClient, httpClient: client, baseURL: u, ctx: ctx}, nil
+		return &Client{httpTracingClient: httpTracingClient, httpClient: client, baseURL: u, clientCtx: clientCtx}, nil
 	}
 }
 
 // GetAppTraces fetches traces of an app
-func (in *Client) GetAppTraces(namespace, app string, q models.TracingQuery) (*model.TracingResponse, error) {
+func (in *Client) GetAppTraces(ctx context.Context, namespace, app string, q models.TracingQuery) (*model.TracingResponse, error) {
 	serviceName := BuildTracingServiceName(namespace, app)
 	if in.grpcClient == nil {
-		return in.httpTracingClient.GetAppTracesHTTP(in.httpClient, in.baseURL, serviceName, q)
+		return in.httpTracingClient.GetAppTracesHTTP(ctx, in.httpClient, in.baseURL, serviceName, q)
 	}
-	return in.grpcClient.FindTraces(in.ctx, serviceName, q)
+	return in.grpcClient.FindTraces(ctx, in.clientCtx, serviceName, q)
path: tracing/client.go
user: login: jmazzitelli
body: I agree, this is what my earlier comments were talking about.

I was going to remove the client ctx, but it looks like this context is getting created at startup (the client is not created per request) AND is used for storing custom headers.

See:

* creation of global context: https://github.com/kiali/kiali/blob/v2.9.0/kiali.go#L132-L134
* creation of global shared client: https://github.com/kiali/kiali/blob/v2.9.0/kiali.go#L146-L158
* use of that global context for custom headers: https://github.com/kiali/kiali/blob/v2.9.0/tracing/client.go#L140-L143

I'm going to see if I can somehow get rid of that client ctx later. Still thinking about that problem.

(BTW: we have the same thing in the Prometheus client struct)

> The rub here is in trying to combine two loggers right? idk if there's an easy way to do that with zerolog 

Right. I was thinking combining contexts (not loggers) but that's what I was talking about [here](https://github.com/kiali/kiali/pull/8416#issuecomment-2877927626). There's no correct way to combine contexts in Go at all. Not sure if combining loggers is the way to go - the way to go would be to try to get rid of that second client context and just have a request context.

> Context is also used for cancellation, timeout, etc. Which one do you use for that and which one do you use for the logger? IMO it's confusing.

Exactly. Which, again, is what I mentioned [here](https://github.com/kiali/kiali/pull/8416#issuecomment-2878026782):
```
 If a request ctx and client ctx are both available in a function, the logger from the request ctx will be used; otherwise, the client ctx will have a logger that will be used (but it will only have the group name, it won't have any information about the request). The hope is we will have the request context for most of the time and thus we can get good structured content for the logs for most of the request path.
```
created_at: 2025-05-14T07:22:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,21 @@
+package jaeger
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCJaeger returns the logger found in the context and adds the type key to refer to this as the gRPC Jaeger logger
+func getLoggerFromContextGRPCJaeger(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("impl", "gRPC Jaeger").Logger()
path: tracing/jaeger/logging.go
user: login: nrfox
body: `impl` is kind of vague. I'm guessing this is shorthand for `implementation` but a user parsing the logs may not know what this means. Maybe "tracing-type"? Or just "client" since we already have "group = tracing" from the parent context? Do we even need this context added or is "group = tracing" from the parent good enough? 
created_at: 2025-05-14T11:34:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+package jaeger
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCJaeger returns the logger found in the context and adds the type key to refer to this as the gRPC Jaeger logger
+func getLoggerFromContextGRPCJaeger(ctx context.Context) *zerolog.Logger {
path: tracing/jaeger/logging.go
user: login: nrfox
body: nit that you don't have to change: these are a little verbose. Could just be a `getLogger` method on the client itself `func (jc JaegerHTTPClient) getLogger( ... ) ... `.
created_at: 2025-05-14T11:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+package jaeger
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCJaeger returns the logger found in the context and adds the type key to refer to this as the gRPC Jaeger logger
+func getLoggerFromContextGRPCJaeger(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("impl", "gRPC Jaeger").Logger()
path: tracing/jaeger/logging.go
user: login: jmazzitelli
body: group=tracing is not enough.. this shows the log came from one of our implementations (there are 4 of them - jaeger (http, grpc) and tempo (http, grpc).). Does a user care? These are for us, not them :) 
created_at: 2025-05-14T11:44:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,21 @@
+package jaeger
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCJaeger returns the logger found in the context and adds the type key to refer to this as the gRPC Jaeger logger
+func getLoggerFromContextGRPCJaeger(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("impl", "gRPC Jaeger").Logger()
path: tracing/jaeger/logging.go
user: login: jmazzitelli
body: renamed `impl` to `client-type` which better describes what this is
created_at: 2025-05-14T11:51:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,21 @@
+package jaeger
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+
+	"github.com/kiali/kiali/log"
+)
+
+// getLoggerFromContextGRPCJaeger returns the logger found in the context and adds the type key to refer to this as the gRPC Jaeger logger
+func getLoggerFromContextGRPCJaeger(ctx context.Context) *zerolog.Logger {
+	zl := log.FromContext(ctx).With().Str("impl", "gRPC Jaeger").Logger()
path: tracing/jaeger/logging.go
user: login: nrfox
body: > Does a user care? These are for us, not them

It's for both. Users should be able to more easily parse their logs with the added context _and_ be able to submit those logs in issues for us to more easily analyze.
created_at: 2025-05-14T12:11:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: jmazzitelli
created_at: 2025-05-13T20:58:55Z
body: This code is currently wrong because there is already a context (for canceling, apparently) in the tracing client struct. This is annoying because it means we somehow have to join two contexts (the one in the client, and the one getting passed down from the incoming request) which is not how things are done using Go contexts. Need to figure out how refactor this code.

(note: we have the same thing going on in the prometheus client)
author_association: COLLABORATOR
user: login: jmazzitelli
created_at: 2025-05-13T21:45:13Z
body: I updated the code to work with two contexts - one in the client struct (which appears to be only used for the grpc client) and one that is passed around with the request. The client ctx will only be used where necessary. If a request ctx and client ctx are both available in a function, the logger from the request ctx will be used; otherwise, the client ctx will have a logger that will be used (but it will only have the group name, it won't have any information about the request). The hope is we will have the request context for most of the time and thus we can get good structured content for the logs for most of the request path.
author_association: COLLABORATOR
user: login: jmazzitelli
created_at: 2025-05-14T07:44:48Z
body: Note: this client context (the one that was originally in the code) is ONLY used for the gRPC clients.

There are ultimately three places where it begins to be passed around - all in `tracing/client.go`:

* [GetAppTraces](https://github.com/kiali/kiali/blob/v2.9.0/tracing/client.go#L227)
* [GetTraceDetail](https://github.com/kiali/kiali/blob/v2.9.0/tracing/client.go#L240)
* [GetServiceStatus](https://github.com/kiali/kiali/blob/v2.9.0/tracing/client.go#L269)

One way to do this "correctly" is to extract the logger out of the request context (since we know what key in the context that is stored, and we know that's the only piece of content we care about in the request context), and store it in the context that gets passed along so there is just a single context that is passed in the call chain.

UPDATE: latest commit does that logger extraction - there is now no longer APIs with two contexts getting passed in
author_association: COLLABORATOR
user: login: jmazzitelli
created_at: 2025-05-14T09:13:26Z
body: plumbing so far looks good... try to get traces for something and logs like this will be found (notice the keys "route", "request-id", "group" and "impl")

```json
{
  "level": "debug",
  "route": "WorkloadTraces",
  "request-id": "d0i5st4g3hgs73emafug",
  "group": "tracing",
  "impl": "gRPC Jaeger",
  "ts": "2025-05-14T09:12:20Z",
  "msg": "Jaeger gRPC FindTraces request: query:{service_name:\"kiali.istio-system\" tags:{key:\"istio.cluster_id\" value:\"Kubernetes\"} start_time_min:{seconds:1747213340 nanos:951000000} start_time_max:{seconds:1747213940 nanos:956885453} duration_min:{} search_depth:100}"
}
```
author_association: COLLABORATOR
user: login: jmazzitelli
created_at: 2025-05-14T09:56:38Z
body: I'm going to stop here with this PR. I was also going to add metric collection - but I'll do that in a followup PR. This PR is here just to get the new logging plumbing in place. Once we review this and ensure its all working, let's merge this now. This stuff touched a lot of little things and I want to make sure it doesn't languish here and start getting git conflicts as time goes on. So I'm breaking this up into logging only, and then I'll move on to the metrics and timing stuff in another PR.
author_association: COLLABORATOR
user: login: jmazzitelli
created_at: 2025-05-14T11:08:55Z
body: Removed that client ctx from the client struct.

If you get some traces in the UI, then run this `kubectl logs -n istio-system deployments/kiali | jq -R 'fromjson? | select(.group == "tracing")` you will see logs like this:

```json
{
  "level": "trace",
  "group": "tracing",
  "ts": "2025-05-14T11:04:32Z",
  "msg": "[jaeger] GRPC client info: address=[tracing.istio-system:16685], auth.type=[none]"
}
{
  "level": "info",
  "group": "tracing",
  "ts": "2025-05-14T11:04:32Z",
  "msg": "Create [jaeger] GRPC client. address=[tracing.istio-system:16685]"
}
{
  "level": "debug",
  "route": "AppTraces",
  "request-id": "d0i7i1jbfsss73apj3qg",
  "group": "tracing",
  "impl": "gRPC Jaeger",
  "ts": "2025-05-14T11:05:42Z",
  "msg": "Jaeger gRPC FindTraces request: query:{service_name:\"grafana.istio-system\" tags:{key:\"istio.cluster_id\" value:\"Kubernetes\"} start_time_min:{seconds:1747220142 nanos:302000000} start_time_max:{seconds:1747220742 nanos:309043767} duration_min:{} search_depth:100}"
}
...
```

author_association: COLLABORATOR