Grafana Tempo, when deployed as a single binary, is less an "all-in-one" solution and more a testament to the power of composability, even when those components are crammed into one executable.
Let’s see Tempo in action. Imagine you’ve got a simple Go application emitting OpenTelemetry traces.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"google.golang.org/grpc"
)
func initTracer() (*sdktrace.TracerProvider, error) {
ctx := context.Background()
// Replace with your Tempo receiver address
endpoint := "localhost:4317" // Default OTLP gRPC port
client := otlptracegrpc.NewClient(
otlptracegrpc.WithInsecure(), // Use WithSecure() for TLS
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithDialOption(grpc.WithBlock()), // Wait for connection
)
traceClient, err := otlptrace.New(ctx, client)
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceClient),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("my-go-app"),
)),
)
return tp, nil
}
func main() {
tp, err := initTracer()
if err != nil {
log.Fatalf("failed to initialize tracer: %v", err)
}
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Printf("Error shutting down tracer provider: %v", err)
}
}()
otel.SetTracerProvider(tp)
ctx := context.Background()
tracer := otel.Tracer("my-app-tracer")
_, span := tracer.Start(ctx, "main operation")
defer span.End()
log.Println("Starting main operation...")
time.Sleep(2 * time.Second) // Simulate work
// Simulate a sub-operation
_, subSpan := tracer.Start(ctx, "sub operation")
time.Sleep(1 * time.Second)
subSpan.End()
log.Println("Main operation finished.")
}
To run this, you’ll need the OpenTelemetry Go SDK. The main function sets up a tracer that sends traces via OTLP gRPC to localhost:4317. The initTracer function configures the exporter to point to your Tempo instance.
Now, let’s spin up a single binary Tempo. Download the latest tempo-<version>-linux-amd64 binary from the Grafana Tempo releases page. Make it executable: chmod +x tempo-linux-amd64.
You can start Tempo with a very minimal configuration file, let’s call it tempo-local.yaml:
server:
http_listen_port: 3100
grpc_listen_port: 9095 # OTLP gRPC listener
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # Tempo will listen for OTLP gRPC here
ingester:
trace:
backend:
local: {} # Use local disk for storage
processor:
spanmetrics: {} # Enable span metrics processor
metrics:
backend:
prometheus:
endpoint: 0.0.0.0:9095 # Expose Tempo's own metrics
Then, run Tempo:
./tempo-linux-amd64 -config.file=tempo-local.yaml
This single binary is now acting as a distributor (receiving traces), an ingester (writing them to local disk), and a processor (generating metrics from spans). The local backend for the ingester means it’s not writing to an external object store like S3 or GCS; it’s just writing to files in a data/ directory that gets created.
Once Tempo is running, execute your Go application. You should see output indicating the trace is being sent. To visualize these traces, you’ll need a Grafana instance. Add Tempo as a data source in Grafana. Use the "Tempo" data source type and set the "Tempo URL" to http://localhost:3100.
In Grafana, navigate to the "Explore" view. Select your Tempo data source. You can then search for traces by service name ("my-go-app" in our example). You should see the traces from your Go application, showing the "main operation" and its nested "sub operation."
The single binary deployment is fantastic for local development or testing because it abstracts away the complexity of setting up separate components like Prometheus, Loki, and a dedicated object store. Tempo’s design philosophy here is that even though it can be deployed as separate microservices (distributor, ingester, querier, etc.), it can also bundle these functionalities for simplicity. The local backend for the ingester is key to this simplicity, as it means you don’t need to configure S3, GCS, or any other external storage. All trace data is stored locally, making cleanup easy for development environments.
What many people overlook is how Tempo’s processor component can be configured even in a single binary. The spanmetrics processor, enabled by default in many configurations, is crucial for generating metrics from your traces. These metrics can then be scraped by Prometheus (which is not included in the Tempo binary itself, but can be configured to scrape Tempo’s /metrics endpoint) and visualized in Grafana dashboards, allowing you to correlate trace data with system metrics.
After exploring your traces, the next logical step is to integrate Tempo with logs and metrics, often using Grafana’s unified observability features.