Introduction
In the complex, distributed landscape of modern applications, understanding how requests flow through microservices can be a monumental challenge. When a user experiences a slow response or an error, pinpointing the exact service or component responsible becomes a high-stakes detective mission. Traditional logging and metrics offer snapshots, but they often lack the crucial context of a request’s journey across multiple services, databases, and external APIs. This is where distributed tracing shines, providing end-to-end visibility into the lifecycle of a request, detailing latency, errors, and the relationships between services.
Grafana Tempo emerges as a powerful, high-scale, and cost-effective distributed tracing backend designed to store traces without the need for an expensive indexing database. Unlike traditional tracing systems that index every span, Tempo focuses on storing trace IDs and their associated spans, relying on Grafana Loki for log correlation and Prometheus for metrics. This unique architectural approach makes it incredibly efficient for handling massive volumes of trace data, allowing you to observe the full story of your application’s behavior in production. This guide will walk you through deploying Grafana Tempo on Kubernetes, configuring your applications to send traces, and visualizing them in Grafana, transforming your debugging and performance optimization efforts.
TL;DR: Grafana Tempo Distributed Tracing on Kubernetes
Deploy Grafana Tempo and Grafana on Kubernetes using Helm. Configure your applications to send OpenTelemetry traces to Tempo. Visualize traces, logs, and metrics in Grafana for end-to-end observability.
Key Commands:
# Add Grafana Helm repository
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
# Install Grafana Tempo (simple mode)
helm install tempo grafana/tempo -n tempo --create-namespace
# Install Grafana
helm install grafana grafana/grafana -n grafana --create-namespace
# Port-forward Grafana to access UI
kubectl port-forward service/grafana -n grafana 3000:80
# Example: Configure an application to send traces to Tempo
# (Assuming OpenTelemetry Collector is used)
# collector-config.yaml excerpt:
# exporters:
# otlp:
# endpoint: "tempo-tempo-distributed-collector.tempo.svc.cluster.local:4317"
# tls:
# insecure: true
# service:
# pipelines:
# traces:
# receivers: [otlp]
# processors: [batch]
# exporters: [otlp]
Prerequisites
To follow this guide, you’ll need:
- A running Kubernetes cluster (v1.20+ recommended).
- Helm (v3.x) installed on your local machine.
- kubectl configured to connect to your cluster.
- Basic understanding of Kubernetes concepts (Pods, Services, Deployments, Namespaces).
- Familiarity with distributed tracing concepts (spans, traces, OpenTelemetry).
- A web browser to access the Grafana UI.
Step-by-Step Guide: Deploying Grafana Tempo and Tracing an Application
1. Add Grafana Helm Repository and Update
First, we need to add the official Grafana Helm chart repository, which contains charts for Grafana, Tempo, Loki, and other related projects. This ensures we can easily install the latest stable versions of these components. After adding the repository, it’s good practice to update your local Helm repositories to fetch the latest chart information.
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
Verify: You should see output indicating the repositories have been updated.
"grafana" has been added to your repositories
Hang tight while we grab the latest from your configured repos...
...Successfully got an update from the "grafana" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
2. Deploy Grafana Tempo to Kubernetes
Grafana Tempo can be deployed in various modes, including a simple all-in-one mode suitable for development and small clusters, or a distributed mode for production environments. For this guide, we’ll start with the simple mode, which bundles all Tempo components into a single pod, making deployment straightforward. We’ll deploy it into its own namespace for better isolation. For production-grade deployments, consider the distributed mode for scalability and resilience.
helm install tempo grafana/tempo -n tempo --create-namespace
Verify: Check that Tempo pods are running and the service is available.
kubectl get pods -n tempo
NAME READY STATUS RESTARTS AGE
tempo-0 1/1 Running 0 2m
kubectl get svc -n tempo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tempo-tempo ClusterIP 10.101.10.10 <none> 3100/TCP,9095/TCP,4317/TCP,14268/TCP 2m
tempo-tempo-distributed-collector ClusterIP 10.101.10.11 <none> 4317/TCP,4318/TCP,14250/TCP,14268/TCP 2m
Note the tempo-tempo-distributed-collector service. This is the primary endpoint for sending traces to Tempo using OTLP (OpenTelemetry Protocol).
3. Deploy Grafana for Visualization
Grafana is the primary interface for visualizing traces stored in Tempo, as well as metrics from Prometheus and logs from Loki. We’ll deploy Grafana into its own namespace and then configure it to connect to our Tempo instance.
helm install grafana grafana/grafana -n grafana --create-namespace
Verify: Ensure Grafana pods are running.
kubectl get pods -n grafana
NAME READY STATUS RESTARTS AGE
grafana-7bb674d8d4-abcde 1/1 Running 0 1m
4. Access Grafana UI and Configure Tempo Data Source
To access the Grafana UI, we’ll use kubectl port-forward. This temporarily exposes the Grafana service on your local machine. Once accessed, we’ll add Tempo as a data source.
kubectl port-forward service/grafana -n grafana 3000:80
Open your web browser and navigate to http://localhost:3000. You’ll need the Grafana admin password. Retrieve it using:
kubectl get secret grafana -n grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
Log in with username admin and the retrieved password.
Once logged in, navigate to Configuration (gear icon) -> Data Sources -> Add data source and select Tempo.
Configure the Tempo data source with the following settings:
- Name:
Tempo(or any descriptive name) - URL:
http://tempo-tempo-distributed-query-frontend.tempo.svc.cluster.local:3100 - Service Name:
tempo-tempo-distributed-query-frontend.tempo.svc.cluster.local
Click “Save & Test”. You should see “Data source is working” or similar confirmation.
For more details on Grafana data sources, refer to the official Grafana documentation.
5. Instrument an Application to Send Traces
Now, let’s deploy a sample application that is instrumented to send traces using OpenTelemetry. We’ll use a simple Python Flask application for this example. The key is to configure the OpenTelemetry exporter to point to our Tempo collector service. We recommend using an OpenTelemetry Collector as an intermediary for robust trace ingestion.
First, create a Kubernetes namespace for our sample application:
kubectl create namespace sample-app
Next, define the OpenTelemetry Collector configuration. This collector will receive traces from our application and forward them to Tempo.
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: sample-app
data:
otel-collector-config.yaml: |
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
send_batch_size: 100
timeout: 10s
exporters:
otlp:
endpoint: "tempo-tempo-distributed-collector.tempo.svc.cluster.local:4317" # Tempo OTLP gRPC endpoint
tls:
insecure: true # Use insecure for simplicity in this demo. For production, secure communication is essential.
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
kubectl apply -f otel-collector-config.yaml
Now, deploy the OpenTelemetry Collector as a Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: sample-app
labels:
app: otel-collector
spec:
replicas: 1
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.96.0 # Use a recent stable version
command:
- "/otelcol-contrib"
- "--config=/conf/otel-collector-config.yaml"
ports:
- name: otlp-grpc
containerPort: 4317
- name: otlp-http
containerPort: 4318
volumeMounts:
- name: otel-collector-config-volume
mountPath: /conf
volumes:
- name: otel-collector-config-volume
configMap:
name: otel-collector-config
---
apiVersion: v1
kind: Service
metadata:
name: otel-collector
namespace: sample-app
labels:
app: otel-collector
spec:
selector:
app: otel-collector
ports:
- name: otlp-grpc
protocol: TCP
port: 4317
targetPort: 4317
- name: otlp-http
protocol: TCP
port: 4318
targetPort: 4318
kubectl apply -f otel-collector-deployment.yaml
Finally, deploy our sample Python Flask application. This application will be configured to send traces to the otel-collector service.
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
namespace: sample-app
labels:
app: flask-app
spec:
replicas: 1
selector:
matchLabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
spec:
containers:
- name: flask-app
image: python:3.9-slim-buster
command: ["/bin/bash", "-c"]
args:
- |
pip install Flask opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests
cat <<EOF > app.py
from flask import Flask, request
import requests
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
# Service name in traces
resource = Resource.create(attributes={"service.name": "flask-sample-app"})
trace.set_tracer_provider(TracerProvider(resource=resource))
tracer = trace.get_tracer(__name__)
# Configure OTLP exporter to send to the OpenTelemetry Collector
otlp_exporter = OTLPSpanExporter(endpoint="otel-collector.sample-app.svc.cluster.local:4317", insecure=True)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
@app.route("/")
def hello():
with tracer.start_as_current_span("hello-request"):
# Make an internal HTTP call to simulate a distributed transaction
response = requests.get("http://localhost:5000/internal")
return f"Hello from Flask! Internal call status: {response.status_code}"
@app.route("/internal")
def internal():
with tracer.start_as_current_span("internal-call"):
return "This is an internal endpoint."
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
EOF
python app.py
env:
- name: OTEL_SERVICE_NAME
value: my-flask-app
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector.sample-app.svc.cluster.local:4318 # OTLP HTTP endpoint of the collector
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: http/protobuf
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: flask-app
namespace: sample-app
spec:
selector:
app: flask-app
ports:
- protocol: TCP
port: 80
targetPort: 5000
type: ClusterIP
kubectl apply -f flask-app-deployment.yaml
Verify: Check that the application pods are running.
kubectl get pods -n sample-app
NAME READY STATUS RESTARTS AGE
otel-collector-798864789-fghij 1/1 Running 0 2m
flask-app-6789abcd-efgh 1/1 Running 0 1m
6. Generate Traffic and View Traces in Grafana
Now that our application is deployed and configured to send traces, we need to generate some traffic to it. We’ll port-forward the Flask application service and then make some requests.
kubectl port-forward service/flask-app -n sample-app 8080:80
In a new terminal, make a few requests:
curl http://localhost:8080
curl http://localhost:8080
curl http://localhost:8080
Now, go back to your Grafana UI (http://localhost:3000).
Navigate to Explore (compass icon). Select the Tempo data source.
In the Tempo query editor, you can search for traces. Since we just generated traffic, you can often click “Run Query” without specific filters to see recent traces. You can also filter by service name (flask-sample-app) or other attributes if you send more complex traces.
You should see a list of traces. Click on one to view its detailed span waterfall diagram, showing the duration of each operation within the request. This provides invaluable insights into latency and dependencies.
For advanced observability, consider integrating Grafana Tempo with eBPF Observability with Hubble to gain deeper network insights, or utilize Istio Ambient Mesh for sidecar-less service mesh capabilities which often include integrated tracing.
Production Considerations
Deploying Grafana Tempo in production requires careful planning for scalability, reliability, and security.
- Scalability: For high-volume environments, deploy Tempo in its distributed mode. This involves separate deployments for ingesters, distributors, compactors, and queriers, allowing independent scaling of each component. Consider using object storage (S3, GCS, Azure Blob Storage) for long-term trace storage, as it’s highly scalable and cost-effective.
- Storage Backend: While the simple mode uses ephemeral storage, production deployments require a persistent and scalable backend. Configure Tempo to use S3, GCS, Azure Blob Storage, or a compatible S3-like object storage. This ensures trace data survives pod restarts and can scale with your needs.
- Resource Limits: Set appropriate CPU and memory limits for Tempo components. Tracing can be resource-intensive, especially for ingesters and compactors. Monitor resource usage closely and adjust limits as needed.
- Security:
- TLS: Secure OTLP endpoints with TLS encryption. In our demo, we used `insecure: true` for simplicity, but this is a critical security vulnerability in production.
- Authentication/Authorization: Implement authentication for Grafana and ensure proper role-based access control (RBAC) for accessing trace data.
- Network Policies: Restrict network access to Tempo components using Kubernetes Network Policies. Only allow necessary services (like OpenTelemetry Collectors) to send traces to Tempo.
- Data Encryption: Ensure data at rest in your object storage backend is encrypted.
- OpenTelemetry Collector: Always use an OpenTelemetry Collector in front of Tempo in production. The collector acts as a buffer, batcher, and can perform various processing steps (like attribute filtering, sampling) before traces hit Tempo, improving efficiency and reliability.
- Sampling: Distributed tracing can generate a huge amount of data. Implement intelligent sampling strategies (head-based, tail-based) at the OpenTelemetry Collector level to manage data volume and cost without losing critical insights.
- Monitoring and Alerting: Monitor Tempo’s health and performance using Prometheus and Grafana. Set up alerts for issues like high error rates, slow trace ingestion, or storage backend problems.
- High Availability: Deploy multiple replicas for critical Tempo components (distributors, queriers) and ensure your object storage backend is highly available.
- Cost Optimization: Consider the storage costs associated with trace data. Sampling, data retention policies, and choosing the right storage backend (e.g., S3 Intelligent-Tiering) can significantly impact costs. For overall Kubernetes cost optimization, tools like Karpenter can help manage node costs.
Troubleshooting
-
Issue: Grafana Tempo pods are not starting or are in a
CrashLoopBackOffstate.Solution:
- Check pod logs:
kubectl logs <tempo-pod-name> -n tempo. Look for error messages related to configuration, storage, or resource issues. - Check pod descriptions:
kubectl describe pod <tempo-pod-name> -n tempo. This can reveal issues like insufficient memory/CPU, persistent volume claim (PVC) problems, or image pull errors. - Verify Helm values: Ensure your Helm `values.yaml` (if used) has correct configurations, especially for storage.
- Check pod logs:
-
Issue: Traces are not appearing in Grafana.
Solution:
- Check application logs: Ensure your application is not reporting errors when sending traces.
- Verify OpenTelemetry Collector:
- Check collector pods:
kubectl get pods -n sample-app -l app=otel-collector. - Check collector logs:
kubectl logs <otel-collector-pod> -n sample-app. Look for errors receiving traces from the app or exporting them to Tempo. - Verify collector configuration: Ensure the
endpointfor the OTLP exporter points to the correct Tempo service (e.g., `tempo-tempo-distributed-collector.tempo.svc.cluster.local:4317`).
- Check collector pods:
- Verify Tempo Ingestion Endpoints:
- Check Tempo pod logs for any ingestion errors:
kubectl logs <tempo-pod-name> -n tempo. - Ensure network connectivity from the OpenTelemetry Collector to Tempo. You can test this by exec’ing into the collector pod and trying to resolve the Tempo service name or even a `curl` if HTTP endpoint is used.
- Check Tempo pod logs for any ingestion errors:
- Grafana Data Source: Double-check the Tempo data source URL in Grafana (e.g., `http://tempo-tempo-distributed-query-frontend.tempo.svc.cluster.local:3100`).
-
Issue: High latency in trace ingestion or querying.
Solution:
- Storage Backend: If using a non-local storage, check the performance of your object storage (S3, GCS). Network latency to the storage can impact performance.
- Tempo Resources: In a distributed Tempo deployment, ensure you have enough resources (CPU, memory) allocated to distributors (ingestion) and queriers (querying).
- Compactor Activity: If traces are slow to appear, the compactor might be overloaded or misconfigured, delaying the availability of traces for querying. Check compactor logs.
- Network Issues: Investigate network latency within your Kubernetes cluster. Tools like Cilium WireGuard Encryption can help secure network traffic, but ensure it’s not introducing unexpected latency.
-
Issue: Grafana UI is not accessible via
port-forward.Solution:
- Ensure the
kubectl port-forwardcommand is still running in its terminal. - Verify the Grafana service name:
kubectl get svc -n grafana. Ensure you are forwarding the correct service. - Check for local port conflicts: Make sure port 3000 isn’t already in use on your local machine. Try forwarding to a different local port (e.g.,
kubectl port-forward service/grafana -n grafana 8080:80).
- Ensure the
-
Issue: Missing spans or incomplete traces in Grafana.
Solution:
- Sampling: Check if your OpenTelemetry Collector or application is configured for sampling. If so, some traces might be intentionally dropped.
- Context Propagation: Ensure trace context (trace ID, span ID) is correctly propagated across service boundaries (e.g., HTTP headers like `traceparent`).
- Instrumentation: Verify that all services involved in a transaction are properly instrumented with OpenTelemetry.
- Batching/Buffering: The OpenTelemetry Collector might be batching traces. Ensure the batching configuration (
send_batch_size,timeout) is appropriate and not causing delays or drops.
-
Issue: High resource usage by OpenTelemetry Collector.
Solution:
- Batch Processor Tuning: Adjust
send_batch_sizeandtimeoutin the collector’s batch processor. Larger batches can reduce CPU overhead but increase latency for traces to appear. - Sampling: Implement head-based or tail-based sampling in the collector to reduce the volume of traces being processed and exported.
- Resource Limits: Increase CPU and memory limits for the collector pods if they are consistently hitting limits and being throttled.
- Horizontal Scaling: For very high trace volumes, consider deploying multiple collector instances behind a load balancer.
- Batch Processor Tuning: Adjust
FAQ Section
-
Q: What is the main difference between Grafana Tempo and other tracing backends like Jaeger or Zipkin?
A: The primary differentiator is Tempo’s trace-ID-based storage model. Unlike Jaeger or Zipkin, which index every span, Tempo only stores the trace ID and the full trace data. This makes Tempo incredibly cost-effective and scalable for high-volume tracing as it avoids the overhead of an expensive indexing database. It relies on logs (Loki) and metrics (Prometheus) for initial context to find trace IDs, rather than indexing span attributes directly.
-
Q: How does Grafana Tempo integrate with Grafana Loki and Prometheus?
A: This is where Tempo’s true power lies. Grafana provides seamless correlation between traces, logs, and metrics. From a trace in Tempo, you can jump directly to relevant logs in Loki (using matching trace IDs) or metrics in Prometheus (using service names or other attributes). This “exemplar” pattern significantly speeds up root cause analysis. For more on correlative observability, consider eBPF Observability with Hubble, which provides deep network metrics.
-
Q: Is OpenTelemetry required to send traces to Tempo?
A: While Tempo supports various trace formats (Jaeger, Zipkin, OpenCensus, OpenTelemetry), OpenTelemetry (OTel) is the recommended and future-proof way to instrument your applications and send traces. OTel provides a standardized, vendor-agnostic set of APIs, SDKs, and collectors for telemetry data (traces, metrics, logs).
-
Q: How can I reduce the amount of trace data sent to Tempo?
A: The most effective way is through sampling. You can implement sampling at the application level (SDK) or, more commonly and flexibly, within an OpenTelemetry Collector. The Collector supports various sampling strategies, including head-based (e.g., probabilistic, always-on for errors) and tail-based (sampling decisions made after a trace is complete, allowing for more intelligent decisions based on errors or latency). This is crucial for managing costs and storage in production. Also, consider Karpenter for overall cost optimization in your cluster.
-
Q: Can Tempo store traces from different programming languages and frameworks?
A: Yes, absolutely. Because Tempo primarily accepts traces via the OpenTelemetry Protocol (OTLP), any application instrumented with OpenTelemetry SDKs for various languages (Python, Java, Go, Node.js, .NET, etc.) can send traces to Tempo. The OTLP format acts as a universal language for telemetry.
Cleanup Commands
To remove all resources deployed in this guide:
# Delete Grafana Tempo
helm uninstall tempo -n tempo
kubectl delete namespace tempo
# Delete Grafana
helm uninstall grafana -n grafana
kubectl delete namespace grafana
# Delete sample application and OpenTelemetry Collector
kubectl delete deployment otel-collector -n sample-app
kubectl delete service otel-collector -n sample-app
kubectl delete configmap otel-collector-config -n sample-app
kubectl delete deployment flask-app -n sample-app
kubectl delete service flask-app -n sample-app
kubectl delete namespace sample-app
# Remove Helm repo (optional)
helm repo remove grafana
Next Steps / Further Reading
- Explore Official Grafana Tempo Documentation for advanced configurations, distributed mode, and storage backends.
- Deep dive into OpenTelemetry Documentation to learn more about instrumenting your applications effectively.
- Learn how to set up Grafana Loki for log aggregation and integrate it with Tempo for seamless log-trace correlation.
- Investigate Kubernetes Gateway API as a modern approach to ingress, which can also be instrumented for tracing at the edge.
- For advanced security in your Kubernetes cluster, consider reading about Securing Container Supply Chains with Sigstore and Kyverno.
- If you’re running machine learning workloads, understand Running LLMs on Kubernetes: GPU Scheduling Best Practices, as tracing can be critical for debugging performance in these environments.
Conclusion
Distributed tracing is no longer a luxury but a necessity for understanding the behavior of complex microservices architectures. Grafana Tempo offers a powerful, cost-effective, and scalable solution for storing and querying trace data. By deploying Tempo on Kubernetes and integrating it with Grafana, you gain unparalleled visibility into your application’s request flows, enabling faster debugging, performance optimization, and a deeper understanding of your system’s health. The combination of traces, logs, and metrics within the Grafana ecosystem provides a holistic observability platform, empowering your teams to build and operate robust, high-performance applications with confidence.
